From 70a32a1736c776febb34cf0ca3014f375ff9fec8 Mon Sep 17 00:00:00 2001 From: Kevin Zuniga Cuellar Date: Tue, 29 Aug 2023 04:52:54 -0400 Subject: feat: add badge to sidebar link (#516) Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com> Co-authored-by: Kevin Zuniga Cuellar <46791833+kevinzunigacuellar@users.noreply.github.com> Co-authored-by: Chris Swithinbank --- .changeset/twenty-ladybugs-hope.md | 5 + docs/src/content/docs/reference/configuration.md | 14 +- docs/src/content/docs/reference/frontmatter.md | 29 +++- .../__tests__/basics/navigation-labels.test.ts | 4 + .../__tests__/basics/navigation-order.test.ts | 4 + .../starlight/__tests__/basics/navigation.test.ts | 162 +++++++++++---------- .../__tests__/i18n/navigation-order.test.ts | 10 ++ .../__tests__/sidebar/navigation-badges.test.ts | 95 ++++++++++++ .../__tests__/sidebar/navigation-hidden.test.ts | 92 ++++++------ .../__tests__/sidebar/navigation-order.test.ts | 105 +++++++------ .../starlight/__tests__/sidebar/navigation.test.ts | 105 +++++++------ .../starlight/__tests__/sidebar/vitest.config.ts | 11 +- packages/starlight/components/Badge.astro | 86 +++++++++++ packages/starlight/components/SidebarSublist.astro | 13 +- packages/starlight/schema.ts | 8 + packages/starlight/schemas/badge.ts | 20 +++ packages/starlight/utils/navigation.ts | 11 +- packages/starlight/utils/user-config.ts | 3 + 18 files changed, 557 insertions(+), 220 deletions(-) create mode 100644 .changeset/twenty-ladybugs-hope.md create mode 100644 packages/starlight/__tests__/sidebar/navigation-badges.test.ts create mode 100644 packages/starlight/components/Badge.astro create mode 100644 packages/starlight/schemas/badge.ts diff --git a/.changeset/twenty-ladybugs-hope.md b/.changeset/twenty-ladybugs-hope.md new file mode 100644 index 00000000..026f4327 --- /dev/null +++ b/.changeset/twenty-ladybugs-hope.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': minor +--- + +Support adding badges to sidebar links from config file and frontmatter diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md index 738ad98e..19c877db 100644 --- a/docs/src/content/docs/reference/configuration.md +++ b/docs/src/content/docs/reference/configuration.md @@ -184,7 +184,10 @@ type SidebarItem = { label: string; translations?: Record; } & ( - | { link: string } + | { + link: string; + badge?: string | BadgeConfig; + } | { items: SidebarItem[]; collapsed?: boolean } | { autogenerate: { directory: string; collapsed?: boolean }; @@ -193,6 +196,15 @@ type SidebarItem = { ); ``` +#### `BadgeConfig` + +```ts +interface BadgeConfig { + text: string; + variant: 'note' | 'tip' | 'caution' | 'danger' | 'success' | 'default'; +} +``` + ### `locales` **type:** { \[dir: string\]: [LocaleConfig](#localeconfig) } diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index e8229958..7ceb8344 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -210,7 +210,7 @@ next: false ### `sidebar` -**type:** `{ label?: string; order?: number; hidden?: boolean }` +**type:** `{ label?: string; order?: number; hidden?: boolean; badge?: string | BadgeConfig }` Control how this page is displayed in the [sidebar](/reference/configuration/#sidebar), when using an autogenerated link group. @@ -258,3 +258,30 @@ sidebar: hidden: true --- ``` + +#### `badge` + +**type:** string | BadgeConfig + +Add a badge to the page in the sidebar when displayed in an autogenerated group of links. +When using a string, the badge will be displayed with a default accent color. +Optionally, pass a [`BadgeConfig` object](/reference/configuration/#badgeconfig) with `text` and `variant` fields to customize the badge. + +```md +--- +title: Page with a badge +sidebar: + # Uses the default variant matching your site’s accent color + badge: New +--- +``` + +```md +--- +title: Page with a badge +sidebar: + badge: + text: Experimental + variant: caution +--- +``` diff --git a/packages/starlight/__tests__/basics/navigation-labels.test.ts b/packages/starlight/__tests__/basics/navigation-labels.test.ts index eae89687..f51cac35 100644 --- a/packages/starlight/__tests__/basics/navigation-labels.test.ts +++ b/packages/starlight/__tests__/basics/navigation-labels.test.ts @@ -20,12 +20,14 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "badge": undefined, "href": "/", "isCurrent": true, "label": "Home Page", "type": "link", }, { + "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, "label": "Environmental impact", @@ -35,12 +37,14 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, "label": "Authoring Markdown", "type": "link", }, { + "badge": undefined, "href": "/guides/components/", "isCurrent": false, "label": "Components", diff --git a/packages/starlight/__tests__/basics/navigation-order.test.ts b/packages/starlight/__tests__/basics/navigation-order.test.ts index 8b751230..2eb2c88d 100644 --- a/packages/starlight/__tests__/basics/navigation-order.test.ts +++ b/packages/starlight/__tests__/basics/navigation-order.test.ts @@ -20,12 +20,14 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "badge": undefined, "href": "/guides/components/", "isCurrent": false, "label": "Components", "type": "link", }, { + "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, "label": "Authoring Markdown", @@ -36,12 +38,14 @@ describe('getSidebar', () => { "type": "group", }, { + "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, "label": "Eco-friendly docs", "type": "link", }, { + "badge": undefined, "href": "/", "isCurrent": true, "label": "Home Page", diff --git a/packages/starlight/__tests__/basics/navigation.test.ts b/packages/starlight/__tests__/basics/navigation.test.ts index 29da09df..023edb76 100644 --- a/packages/starlight/__tests__/basics/navigation.test.ts +++ b/packages/starlight/__tests__/basics/navigation.test.ts @@ -16,40 +16,44 @@ vi.mock('astro:content', async () => describe('getSidebar', () => { test('returns an array of sidebar entries', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` - [ - { - "href": "/", - "isCurrent": true, - "label": "Home Page", - "type": "link", - }, - { - "href": "/environmental-impact/", - "isCurrent": false, - "label": "Eco-friendly docs", - "type": "link", - }, - { - "collapsed": false, - "entries": [ - { - "href": "/guides/authoring-content/", - "isCurrent": false, - "label": "Authoring Markdown", - "type": "link", - }, - { - "href": "/guides/components/", - "isCurrent": false, - "label": "Components", - "type": "link", - }, - ], - "label": "guides", - "type": "group", - }, - ] - `); + [ + { + "badge": undefined, + "href": "/", + "isCurrent": true, + "label": "Home Page", + "type": "link", + }, + { + "badge": undefined, + "href": "/environmental-impact/", + "isCurrent": false, + "label": "Eco-friendly docs", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "badge": undefined, + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + { + "badge": undefined, + "href": "/guides/components/", + "isCurrent": false, + "label": "Components", + "type": "link", + }, + ], + "label": "guides", + "type": "group", + }, + ] + `); }); test('marks current path with isCurrent', () => { @@ -97,33 +101,37 @@ describe('flattenSidebar', () => { expect(flattened.every((item) => item.type === 'link')).toBe(true); expect(flattened).toMatchInlineSnapshot(` - [ - { - "href": "/", - "isCurrent": true, - "label": "Home Page", - "type": "link", - }, - { - "href": "/environmental-impact/", - "isCurrent": false, - "label": "Eco-friendly docs", - "type": "link", - }, - { - "href": "/guides/authoring-content/", - "isCurrent": false, - "label": "Authoring Markdown", - "type": "link", - }, - { - "href": "/guides/components/", - "isCurrent": false, - "label": "Components", - "type": "link", - }, - ] - `); + [ + { + "badge": undefined, + "href": "/", + "isCurrent": true, + "label": "Home Page", + "type": "link", + }, + { + "badge": undefined, + "href": "/environmental-impact/", + "isCurrent": false, + "label": "Eco-friendly docs", + "type": "link", + }, + { + "badge": undefined, + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + { + "badge": undefined, + "href": "/guides/components/", + "isCurrent": false, + "label": "Components", + "type": "link", + }, + ] + `); }); }); @@ -132,21 +140,23 @@ describe('getPrevNextLinks', () => { const sidebar = getSidebar('/environmental-impact/', undefined); const links = getPrevNextLinks(sidebar, true, {}); expect(links).toMatchInlineSnapshot(` - { - "next": { - "href": "/guides/authoring-content/", - "isCurrent": false, - "label": "Authoring Markdown", - "type": "link", - }, - "prev": { - "href": "/", - "isCurrent": false, - "label": "Home Page", - "type": "link", - }, - } - `); + { + "next": { + "badge": undefined, + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + "prev": { + "badge": undefined, + "href": "/", + "isCurrent": false, + "label": "Home Page", + "type": "link", + }, + } + `); }); test('returns no links when pagination is disabled', () => { diff --git a/packages/starlight/__tests__/i18n/navigation-order.test.ts b/packages/starlight/__tests__/i18n/navigation-order.test.ts index a9c39eb9..989f8a99 100644 --- a/packages/starlight/__tests__/i18n/navigation-order.test.ts +++ b/packages/starlight/__tests__/i18n/navigation-order.test.ts @@ -30,12 +30,14 @@ describe('getSidebar', () => { expect(getSidebar('/en/', 'en')).toMatchInlineSnapshot(` [ { + "badge": undefined, "href": "/en/", "isCurrent": true, "label": "Home page", "type": "link", }, { + "badge": undefined, "href": "/en/404/", "isCurrent": false, "label": "Not found", @@ -45,6 +47,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "badge": undefined, "href": "/en/guides/authoring-content/", "isCurrent": false, "label": "Authoring Markdown", @@ -62,12 +65,14 @@ describe('getSidebar', () => { expect(getSidebar('/fr/', 'fr')).toMatchInlineSnapshot(` [ { + "badge": undefined, "href": "/fr/", "isCurrent": true, "label": "Accueil", "type": "link", }, { + "badge": undefined, "href": "/fr/404/", "isCurrent": false, "label": "Not found", @@ -77,6 +82,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "badge": undefined, "href": "/fr/guides/authoring-content/", "isCurrent": false, "label": "Authoring Markdown", @@ -90,12 +96,14 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "badge": undefined, "href": "/fr/référence/bénéfice/", "isCurrent": false, "label": "Bénéfice", "type": "link", }, { + "badge": undefined, "href": "/fr/référence/bricolage/", "isCurrent": false, "label": "Bricolage", @@ -109,12 +117,14 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "badge": undefined, "href": "/fr/route/décoder/", "isCurrent": false, "label": "Décoder", "type": "link", }, { + "badge": undefined, "href": "/fr/route/distribuer/", "isCurrent": false, "label": "Distribuer", diff --git a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts new file mode 100644 index 00000000..6c4b1863 --- /dev/null +++ b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getSidebar } from '../../utils/navigation'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['environmental-impact.md', { title: 'Eco-friendly docs' }], + [ + 'reference/configuration.md', + { + title: 'Config Reference', + sidebar: { + badge: { + text: 'Experimental', + variant: 'tip', + }, + }, + }, + ], + ['reference/frontmatter.md', { title: 'Frontmatter Reference', sidebar: { badge: 'New' } }], + ['guides/components.mdx', { title: 'Components' }], + ], + }) +); + +describe('getSidebar', () => { + test('adds a badge object to the sidebar when using a "string" or "object"', () => { + expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` + [ + { + "badge": undefined, + "href": "/", + "isCurrent": true, + "label": "Home", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "badge": { + "text": "New", + "variant": "success", + }, + "href": "/intro/", + "isCurrent": false, + "label": "Introduction", + "type": "link", + }, + { + "badge": { + "text": "Deprecated", + "variant": "default", + }, + "href": "/next-steps/", + "isCurrent": false, + "label": "Next Steps", + "type": "link", + }, + ], + "label": "Start Here", + "type": "group", + }, + { + "collapsed": false, + "entries": [ + { + "badge": { + "text": "Experimental", + "variant": "tip", + }, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "badge": { + "text": "New", + "variant": "default", + }, + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + ], + "label": "Reference", + "type": "group", + }, + ] + `); + }); +}); diff --git a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts index 7f133941..df539ef7 100644 --- a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts @@ -16,46 +16,56 @@ vi.mock('astro:content', async () => describe('getSidebar', () => { test('excludes sidebar entries with hidden: true in frontmatter', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` - [ - { - "href": "/", - "isCurrent": true, - "label": "Home", - "type": "link", - }, - { - "collapsed": false, - "entries": [ - { - "href": "/intro/", - "isCurrent": false, - "label": "Introduction", - "type": "link", - }, - { - "href": "/next-steps/", - "isCurrent": false, - "label": "Next Steps", - "type": "link", - }, - ], - "label": "Start Here", - "type": "group", - }, - { - "collapsed": false, - "entries": [ - { - "href": "/reference/configuration/", - "isCurrent": false, - "label": "Config Reference", - "type": "link", - }, - ], - "label": "Reference", - "type": "group", - }, - ] - `); + [ + { + "badge": undefined, + "href": "/", + "isCurrent": true, + "label": "Home", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "badge": { + "text": "New", + "variant": "success", + }, + "href": "/intro/", + "isCurrent": false, + "label": "Introduction", + "type": "link", + }, + { + "badge": { + "text": "Deprecated", + "variant": "default", + }, + "href": "/next-steps/", + "isCurrent": false, + "label": "Next Steps", + "type": "link", + }, + ], + "label": "Start Here", + "type": "group", + }, + { + "collapsed": false, + "entries": [ + { + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + ], + "label": "Reference", + "type": "group", + }, + ] + `); }); }); diff --git a/packages/starlight/__tests__/sidebar/navigation-order.test.ts b/packages/starlight/__tests__/sidebar/navigation-order.test.ts index 9f003c67..f0257bc5 100644 --- a/packages/starlight/__tests__/sidebar/navigation-order.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-order.test.ts @@ -16,52 +16,63 @@ vi.mock('astro:content', async () => describe('getSidebar', () => { test('returns sidebar entries sorted by frontmatter order', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` - [ - { - "href": "/", - "isCurrent": true, - "label": "Home", - "type": "link", - }, - { - "collapsed": false, - "entries": [ - { - "href": "/intro/", - "isCurrent": false, - "label": "Introduction", - "type": "link", - }, - { - "href": "/next-steps/", - "isCurrent": false, - "label": "Next Steps", - "type": "link", - }, - ], - "label": "Start Here", - "type": "group", - }, - { - "collapsed": false, - "entries": [ - { - "href": "/reference/frontmatter/", - "isCurrent": false, - "label": "Frontmatter Reference", - "type": "link", - }, - { - "href": "/reference/configuration/", - "isCurrent": false, - "label": "Config Reference", - "type": "link", - }, - ], - "label": "Reference", - "type": "group", - }, - ] - `); + [ + { + "badge": undefined, + "href": "/", + "isCurrent": true, + "label": "Home", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "badge": { + "text": "New", + "variant": "success", + }, + "href": "/intro/", + "isCurrent": false, + "label": "Introduction", + "type": "link", + }, + { + "badge": { + "text": "Deprecated", + "variant": "default", + }, + "href": "/next-steps/", + "isCurrent": false, + "label": "Next Steps", + "type": "link", + }, + ], + "label": "Start Here", + "type": "group", + }, + { + "collapsed": false, + "entries": [ + { + "badge": undefined, + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + { + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + ], + "label": "Reference", + "type": "group", + }, + ] + `); }); }); diff --git a/packages/starlight/__tests__/sidebar/navigation.test.ts b/packages/starlight/__tests__/sidebar/navigation.test.ts index 462c04ff..dd580740 100644 --- a/packages/starlight/__tests__/sidebar/navigation.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation.test.ts @@ -16,52 +16,63 @@ vi.mock('astro:content', async () => describe('getSidebar', () => { test('returns an array of sidebar entries', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` - [ - { - "href": "/", - "isCurrent": true, - "label": "Home", - "type": "link", - }, - { - "collapsed": false, - "entries": [ - { - "href": "/intro/", - "isCurrent": false, - "label": "Introduction", - "type": "link", - }, - { - "href": "/next-steps/", - "isCurrent": false, - "label": "Next Steps", - "type": "link", - }, - ], - "label": "Start Here", - "type": "group", - }, - { - "collapsed": false, - "entries": [ - { - "href": "/reference/configuration/", - "isCurrent": false, - "label": "Config Reference", - "type": "link", - }, - { - "href": "/reference/frontmatter/", - "isCurrent": false, - "label": "Frontmatter Reference", - "type": "link", - }, - ], - "label": "Reference", - "type": "group", - }, - ] - `); + [ + { + "badge": undefined, + "href": "/", + "isCurrent": true, + "label": "Home", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "badge": { + "text": "New", + "variant": "success", + }, + "href": "/intro/", + "isCurrent": false, + "label": "Introduction", + "type": "link", + }, + { + "badge": { + "text": "Deprecated", + "variant": "default", + }, + "href": "/next-steps/", + "isCurrent": false, + "label": "Next Steps", + "type": "link", + }, + ], + "label": "Start Here", + "type": "group", + }, + { + "collapsed": false, + "entries": [ + { + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "badge": undefined, + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + ], + "label": "Reference", + "type": "group", + }, + ] + `); }); }); diff --git a/packages/starlight/__tests__/sidebar/vitest.config.ts b/packages/starlight/__tests__/sidebar/vitest.config.ts index 75c446b7..31860cae 100644 --- a/packages/starlight/__tests__/sidebar/vitest.config.ts +++ b/packages/starlight/__tests__/sidebar/vitest.config.ts @@ -9,8 +9,15 @@ export default defineVitestConfig({ { label: 'Start Here', items: [ - { label: 'Introduction', link: '/intro' }, - { label: 'Next Steps', link: '/next-steps' }, + { + label: 'Introduction', + link: '/intro', + badge: { + variant: 'success', + text: 'New', + }, + }, + { label: 'Next Steps', link: '/next-steps', badge: 'Deprecated' }, ], }, // A group linking to all pages in the reference directory. diff --git a/packages/starlight/components/Badge.astro b/packages/starlight/components/Badge.astro new file mode 100644 index 00000000..dcfe7046 --- /dev/null +++ b/packages/starlight/components/Badge.astro @@ -0,0 +1,86 @@ +--- +import type { Badge } from '../schemas/badge'; + +interface Props { + variant?: Badge['variant'] | 'outline'; + text?: string; +} +const { variant = 'default', text } = Astro.props; +--- + + + + diff --git a/packages/starlight/components/SidebarSublist.astro b/packages/starlight/components/SidebarSublist.astro index 76015484..fe731ff7 100644 --- a/packages/starlight/components/SidebarSublist.astro +++ b/packages/starlight/components/SidebarSublist.astro @@ -1,6 +1,7 @@ --- import { flattenSidebar, type SidebarEntry } from '../utils/navigation'; import Icon from '../user-components/Icon.astro'; +import Badge from './Badge.astro'; interface Props { sublist: SidebarEntry[]; @@ -18,7 +19,13 @@ interface Props { aria-current={entry.isCurrent && 'page'} class:list={{ large: !Astro.props.nested }} > - {entry.label} + {entry.label} + {entry.badge && ( + + )} ) : (
*:not(:last-child) { + margin-inline-end: 0.25em; + } + @media (min-width: 50rem) { .top-level > li + li { margin-top: 0.5rem; diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index 19a0c659..aae1cdd4 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -3,6 +3,7 @@ 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'; export { i18nSchema } from './schemas/i18n'; type IconName = keyof typeof Icons; @@ -147,6 +148,13 @@ export function docsSchema() { * Prevents this page from being included in autogenerated sidebar groups. */ hidden: z.boolean().default(false), + /** + * Adds a badge to the sidebar link. + * Can be a string or an object with a variant and text. + * Variants include 'note', 'tip', 'caution', 'danger', 'success', and 'default'. + * Passing only a string defaults to the 'default' variant which uses the site accent color. + */ + badge: BadgeConfigSchema(), }) .default({}), diff --git a/packages/starlight/schemas/badge.ts b/packages/starlight/schemas/badge.ts new file mode 100644 index 00000000..dc6fdb67 --- /dev/null +++ b/packages/starlight/schemas/badge.ts @@ -0,0 +1,20 @@ +import { z } from 'astro/zod'; + +const badgeSchema = () => + z.object({ + variant: z.enum(['note', 'danger', 'success', 'caution', 'tip', 'default']).default('default'), + text: z.string(), + }); + +export const BadgeConfigSchema = () => + z + .union([z.string(), badgeSchema()]) + .transform((badge) => { + if (typeof badge === 'string') { + return { variant: 'default' as const, text: badge }; + } + return badge; + }) + .optional(); + +export type Badge = z.output>; diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index d6e1bf2f..88d9e3e1 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -7,6 +7,7 @@ import { getLocaleRoutes, type Route } from './routing'; import { localeToLang, slugToPathname } from './slugs'; import type { AutoSidebarGroup, SidebarItem, SidebarLinkItem } from './user-config'; import { ensureLeadingAndTrailingSlashes, ensureTrailingSlash } from './path'; +import type { Badge } from '../schemas/badge'; const DirKey = Symbol('DirKey'); @@ -15,6 +16,7 @@ export interface Link { label: string; href: string; isCurrent: boolean; + badge: Badge | undefined; } interface Group { @@ -112,14 +114,14 @@ function linkFromConfig( if (locale) href = '/' + locale + href; } const label = pickLang(item.translations, localeToLang(locale)) || item.label; - return makeLink(href, label, currentPathname); + return makeLink(href, label, currentPathname, item.badge); } /** Create a link entry. */ -function makeLink(href: string, label: string, currentPathname: string): Link { +function makeLink(href: string, label: string, currentPathname: string, badge?: Badge): Link { if (!isAbsolute(href)) href = pathWithBase(href); const isCurrent = href === ensureTrailingSlash(currentPathname); - return { type: 'link', label, href, isCurrent }; + return { type: 'link', label, href, isCurrent, badge }; } /** Get the segments leading to a page. */ @@ -168,7 +170,8 @@ function linkFromRoute(route: Route, currentPathname: string): Link { return makeLink( slugToPathname(route.slug), route.entry.data.sidebar.label || route.entry.data.title, - currentPathname + currentPathname, + route.entry.data.sidebar.badge ); } diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index 979f6c47..dcc69ee4 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -4,6 +4,7 @@ import { HeadConfigSchema } from '../schemas/head'; import { LogoConfigSchema } from '../schemas/logo'; import { TableOfContentsSchema } from '../schemas/tableOfContents'; import { FaviconSchema } from '../schemas/favicon'; +import { BadgeConfigSchema } from '../schemas/badge'; const LocaleSchema = z.object({ /** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */ @@ -42,6 +43,8 @@ const SidebarGroupSchema = SidebarBaseSchema.extend({ const SidebarLinkItemSchema = SidebarBaseSchema.extend({ /** The link to this item’s content. Can be a relative link to local files or the full URL of an external page. */ link: z.string(), + /** Adds a badge to the link item */ + badge: BadgeConfigSchema(), }); export type SidebarLinkItem = z.infer; -- cgit