summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Zuniga Cuellar2023-08-29 04:52:54 -0400
committerGitHub2023-08-29 10:52:54 +0200
commit70a32a1736c776febb34cf0ca3014f375ff9fec8 (patch)
tree92adf8ad282b003036ece23c7159e4862d521f7e
parent5726353f1a4815df9ab5a7acd7aea6af53383adc (diff)
downloadIT.starlight-70a32a1736c776febb34cf0ca3014f375ff9fec8.tar.gz
IT.starlight-70a32a1736c776febb34cf0ca3014f375ff9fec8.tar.bz2
IT.starlight-70a32a1736c776febb34cf0ca3014f375ff9fec8.zip
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 <swithinbank@gmail.com>
-rw-r--r--.changeset/twenty-ladybugs-hope.md5
-rw-r--r--docs/src/content/docs/reference/configuration.md14
-rw-r--r--docs/src/content/docs/reference/frontmatter.md29
-rw-r--r--packages/starlight/__tests__/basics/navigation-labels.test.ts4
-rw-r--r--packages/starlight/__tests__/basics/navigation-order.test.ts4
-rw-r--r--packages/starlight/__tests__/basics/navigation.test.ts162
-rw-r--r--packages/starlight/__tests__/i18n/navigation-order.test.ts10
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-badges.test.ts95
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-hidden.test.ts92
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-order.test.ts105
-rw-r--r--packages/starlight/__tests__/sidebar/navigation.test.ts105
-rw-r--r--packages/starlight/__tests__/sidebar/vitest.config.ts11
-rw-r--r--packages/starlight/components/Badge.astro86
-rw-r--r--packages/starlight/components/SidebarSublist.astro13
-rw-r--r--packages/starlight/schema.ts8
-rw-r--r--packages/starlight/schemas/badge.ts20
-rw-r--r--packages/starlight/utils/navigation.ts11
-rw-r--r--packages/starlight/utils/user-config.ts3
18 files changed, 557 insertions, 220 deletions
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<string, string>;
} & (
- | { 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:** <code>{ \[dir: string\]: [LocaleConfig](#localeconfig) }</code>
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:** <code>string | <a href="/reference/configuration/#badgeconfig">BadgeConfig</a></code>
+
+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;
+---
+
+<span class:list={['sl-badge', variant]} set:html={text} />
+
+<style>
+ .sl-badge {
+ display: inline-block;
+ border: 1px solid var(--sl-color-border-badge);
+ border-radius: 0.25rem;
+ font-family: var(--sl-font-system-mono);
+ font-size: var(--sl-text-xs);
+ font-weight: 400;
+ padding: 0.125rem 0.375rem;
+ line-height: 1;
+ color: #fff;
+ background-color: var(--sl-color-bg-badge);
+ }
+
+ .outline {
+ --sl-color-bg-badge: transparent;
+ --sl-color-border-badge: currentColor;
+ color: inherit;
+ }
+
+ .default {
+ --sl-color-bg-badge: var(--sl-color-accent-low);
+ --sl-color-border-badge: var(--sl-color-accent);
+ }
+
+ .note {
+ --sl-color-bg-badge: var(--sl-color-blue-low);
+ --sl-color-border-badge: var(--sl-color-blue);
+ }
+
+ .danger {
+ --sl-color-bg-badge: var(--sl-color-red-low);
+ --sl-color-border-badge: var(--sl-color-red);
+ }
+
+ .success {
+ --sl-color-bg-badge: var(--sl-color-green-low);
+ --sl-color-border-badge: var(--sl-color-green);
+ }
+
+ .caution {
+ --sl-color-bg-badge: var(--sl-color-orange-low);
+ --sl-color-border-badge: var(--sl-color-orange);
+ }
+
+ .tip {
+ --sl-color-bg-badge: var(--sl-color-purple-low);
+ --sl-color-border-badge: var(--sl-color-purple);
+ }
+
+ :global([data-theme='light']) .default {
+ --sl-color-bg-badge: var(--sl-color-accent-high);
+ }
+
+ :global([data-theme='light']) .note {
+ --sl-color-bg-badge: var(--sl-color-blue-high);
+ }
+
+ :global([data-theme='light']) .danger {
+ --sl-color-bg-badge: var(--sl-color-red-high);
+ }
+
+ :global([data-theme='light']) .success {
+ --sl-color-bg-badge: var(--sl-color-green-high);
+ }
+
+ :global([data-theme='light']) .caution {
+ --sl-color-bg-badge: var(--sl-color-orange-high);
+ }
+
+ :global([data-theme='light']) .tip {
+ --sl-color-bg-badge: var(--sl-color-purple-high);
+ }
+</style>
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}
+ <span>{entry.label}</span>
+ {entry.badge && (
+ <Badge
+ text={entry.badge.text}
+ variant={entry.isCurrent ? 'outline' : entry.badge.variant}
+ />
+ )}
</a>
) : (
<details
@@ -104,6 +111,10 @@ interface Props {
background-color: var(--sl-color-text-accent);
}
+ a > *: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<ReturnType<typeof badgeSchema>>;
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<typeof SidebarLinkItemSchema>;