From 72cca2d07644f00595da6ebf7d603adb282f359d Mon Sep 17 00:00:00 2001 From: Cédric Bontems Date: Wed, 1 Nov 2023 23:16:01 +0100 Subject: feat: add support for light / dark hero images (#280) Co-authored-by: Lorenzo Lewis Co-authored-by: Chris Swithinbank --- .changeset/violet-berries-mix.md | 12 +++++ docs/src/content/docs/reference/frontmatter.md | 40 ++++++++++++--- packages/starlight/components/Hero.astro | 30 ++++++++--- packages/starlight/components/SiteTitle.astro | 10 +--- packages/starlight/schema.ts | 58 ++------------------- packages/starlight/schemas/hero.ts | 70 ++++++++++++++++++++++++++ packages/starlight/style/util.css | 6 +++ 7 files changed, 147 insertions(+), 79 deletions(-) create mode 100644 .changeset/violet-berries-mix.md create mode 100644 packages/starlight/schemas/hero.ts diff --git a/.changeset/violet-berries-mix.md b/.changeset/violet-berries-mix.md new file mode 100644 index 00000000..e93a59f8 --- /dev/null +++ b/.changeset/violet-berries-mix.md @@ -0,0 +1,12 @@ +--- +'@astrojs/starlight': minor +--- + +Support light & dark variants of the hero image. + +⚠️ **Potentially breaking change:** The `hero.image` schema is now slightly stricter than previously. + +The `hero.image.html` property can no longer be used alongside the `hero.image.alt` or `hero.image.file` properties. +Previously, `html` was ignored when used with `file` and `alt` was ignored when used with `html`. +Now, those combinations will throw errors. +If you encounter errors, remove the `image.hero` property that is not in use. \ No newline at end of file diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index 9b401383..e1adddb5 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -111,20 +111,44 @@ hero: --- ``` +You can display different versions of the hero image in light and dark modes. + +```md +--- +hero: + image: + alt: A glittering, brightly colored logo + dark: ../../assets/logo-dark.png + light: ../../assets/logo-light.png +--- +``` + #### `HeroConfig` ```ts interface HeroConfig { title?: string; tagline?: string; - image?: { - alt?: string; - // Relative path to an image in your repository. - file?: string; - // Raw HTML to use in the image slot. - // Could be a custom `` tag or inline ``. - html?: string; - }; + image?: + | { + // Relative path to an image in your repository. + file: string; + // Alt text to make the image accessible to assistive technology + alt?: string; + } + | { + // Relative path to an image in your repository to be used for dark mode. + dark: string; + // Relative path to an image in your repository to be used for light mode. + light: string; + // Alt text to make the image accessible to assistive technology + alt?: string; + } + | { + // Raw HTML to use in the image slot. + // Could be a custom `` tag or inline ``. + html: string; + }; actions?: Array<{ text: string; link: string; diff --git a/packages/starlight/components/Hero.astro b/packages/starlight/components/Hero.astro index bd9e8202..f763ce22 100644 --- a/packages/starlight/components/Hero.astro +++ b/packages/starlight/components/Hero.astro @@ -14,20 +14,34 @@ const imageAttrs = { height: 400, alt: image?.alt || '', }; + +let darkImage: ImageMetadata | undefined; +let lightImage: ImageMetadata | undefined; +let rawHtml: string | undefined; +if (image) { + if ('file' in image) { + darkImage = image.file; + } else if ('dark' in image) { + darkImage = image.dark; + lightImage = image.light; + } else { + rawHtml = image.html; + } +} ---
{ - image?.file ? ( - image.file.format === 'svg' ? ( - - ) : ( - - ) - ) : ( - image?.html &&
+ darkImage && ( + ) } + {lightImage && } + {rawHtml &&
}

diff --git a/packages/starlight/components/SiteTitle.astro b/packages/starlight/components/SiteTitle.astro index bb1c2038..afb09c80 100644 --- a/packages/starlight/components/SiteTitle.astro +++ b/packages/starlight/components/SiteTitle.astro @@ -12,7 +12,7 @@ const href = pathWithBase(Astro.props.locale || '/'); config.logo && logos.dark && ( <> {config.logo.alt} diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index 5f73dfdc..cece5949 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -3,16 +3,13 @@ import type { SchemaContext } from 'astro:content'; import { HeadConfigSchema } from './schemas/head'; import { PrevNextLinkConfigSchema } from './schemas/prevNextLink'; import { TableOfContentsSchema } from './schemas/tableOfContents'; -import { Icons } from './components/Icons'; import { BadgeConfigSchema } from './schemas/badge'; +import { HeroSchema } from './schemas/hero'; import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar'; export { i18nSchema } from './schemas/i18n'; -type IconName = keyof typeof Icons; -const iconNames = Object.keys(Icons) as [IconName, ...IconName[]]; - export function docsSchema() { - return ({ image }: SchemaContext) => + return (context: SchemaContext) => z.object({ /** The title of the current page. Required. */ title: z.string(), @@ -45,56 +42,7 @@ export function docsSchema() { template: z.enum(['doc', 'splash']).default('doc'), /** Display a hero section on this page. */ - hero: z - .object({ - /** - * The large title text to show. If not provided, will default to the top-level `title`. - * Can include HTML. - */ - title: z.string().optional(), - /** - * A short bit of text about your project. - * Will be displayed in a smaller size below the title. - */ - tagline: z.string().optional(), - /** The image to use in the hero. You can provide either a relative `file` path or raw `html`. */ - image: z - .object({ - /** Alt text for screenreaders and other assistive technologies describing your hero image. */ - alt: z.string().default(''), - /** Relative path to an image file in your repo, e.g. `../../assets/hero.png`. */ - file: image().optional(), - /** Raw HTML string instead of an image file. Useful for inline SVGs or more complex hero content. */ - html: z.string().optional(), - }) - .optional(), - /** An array of call-to-action links displayed at the bottom of the hero. */ - actions: z - .object({ - /** Text label displayed in the link. */ - text: z.string(), - /** Value for the link’s `href` attribute, e.g. `/page` or `https://mysite.com`. */ - link: z.string(), - /** Button style to use. One of `primary`, `secondary`, or `minimal` (the default). */ - variant: z.enum(['primary', 'secondary', 'minimal']).default('minimal'), - /** - * An optional icon to display alongside the link text. - * Can be an inline `` or the name of one of Starlight’s built-in icons. - */ - icon: z - .union([z.enum(iconNames), z.string().startsWith(' { - const parsedIcon = z.enum(iconNames).safeParse(icon); - return parsedIcon.success - ? ({ type: 'icon', name: parsedIcon.data } as const) - : ({ type: 'raw', html: icon } as const); - }) - .optional(), - }) - .array() - .default([]), - }) - .optional(), + hero: HeroSchema(context).optional(), /** * The last update date of the current page. diff --git a/packages/starlight/schemas/hero.ts b/packages/starlight/schemas/hero.ts new file mode 100644 index 00000000..e1b5611c --- /dev/null +++ b/packages/starlight/schemas/hero.ts @@ -0,0 +1,70 @@ +import { z } from 'astro/zod'; +import type { SchemaContext } from 'astro:content'; +import { Icons } from '../components/Icons'; + +type IconName = keyof typeof Icons; +const iconNames = Object.keys(Icons) as [IconName, ...IconName[]]; + +export const HeroSchema = ({ image }: SchemaContext) => + z.object({ + /** + * The large title text to show. If not provided, will default to the top-level `title`. + * Can include HTML. + */ + title: z.string().optional(), + /** + * A short bit of text about your project. + * Will be displayed in a smaller size below the title. + */ + tagline: z.string().optional(), + /** The image to use in the hero. You can provide either a relative `file` path or raw `html`. */ + image: z + .union([ + z.object({ + /** Alt text for screenreaders and other assistive technologies describing your hero image. */ + alt: z.string().default(''), + /** Relative path to an image file in your repo, e.g. `../../assets/hero.png`. */ + file: image(), + }), + z.object({ + /** Alt text for screenreaders and other assistive technologies describing your hero image. */ + alt: z.string().default(''), + /** Relative path to an image file in your repo to use in dark mode, e.g. `../../assets/hero-dark.png`. */ + dark: image(), + /** Relative path to an image file in your repo to use in light mode, e.g. `../../assets/hero-light.png`. */ + light: image(), + }), + z + .object({ + /** Raw HTML string instead of an image file. Useful for inline SVGs or more complex hero content. */ + html: z.string(), + }) + .transform(({ html }) => ({ html, alt: '' })), + ]) + .optional(), + /** An array of call-to-action links displayed at the bottom of the hero. */ + actions: z + .object({ + /** Text label displayed in the link. */ + text: z.string(), + /** Value for the link’s `href` attribute, e.g. `/page` or `https://mysite.com`. */ + link: z.string(), + /** Button style to use. One of `primary`, `secondary`, or `minimal` (the default). */ + variant: z.enum(['primary', 'secondary', 'minimal']).default('minimal'), + /** + * An optional icon to display alongside the link text. + * Can be an inline `` or the name of one of Starlight’s built-in icons. + */ + icon: z + .union([z.enum(iconNames), z.string().startsWith(' { + const parsedIcon = z.enum(iconNames).safeParse(icon); + return parsedIcon.success + ? ({ type: 'icon', name: parsedIcon.data } as const) + : ({ type: 'raw', html: icon } as const); + }) + .optional(), + }) + .array() + .default([]), + }); diff --git a/packages/starlight/style/util.css b/packages/starlight/style/util.css index b041a9c2..74868532 100644 --- a/packages/starlight/style/util.css +++ b/packages/starlight/style/util.css @@ -41,6 +41,12 @@ display: block; } } +[data-theme='light'] .light\:sl-hidden { + display: none; +} +[data-theme='dark'] .dark\:sl-hidden { + display: none; +} /* Flip an element around the y-axis when in an RTL context. -- cgit