svelte-docsmith 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/bin/svelte-docsmith.mjs +13 -158
  2. package/dist/buildtime/archives.d.ts +18 -0
  3. package/dist/buildtime/archives.js +43 -0
  4. package/dist/buildtime/cli/archive-version.d.ts +18 -0
  5. package/dist/buildtime/cli/archive-version.js +104 -0
  6. package/dist/buildtime/cli/error.d.ts +9 -0
  7. package/dist/buildtime/cli/error.js +9 -0
  8. package/dist/buildtime/cli/run.d.ts +13 -0
  9. package/dist/buildtime/cli/run.js +62 -0
  10. package/dist/buildtime/{vite/git.d.ts → git.d.ts} +1 -1
  11. package/dist/buildtime/{vite/git.js → git.js} +15 -3
  12. package/dist/buildtime/paths.d.ts +7 -0
  13. package/dist/buildtime/paths.js +15 -0
  14. package/dist/buildtime/vite/collect.js +1 -1
  15. package/dist/buildtime/vite/index.js +9 -4
  16. package/dist/buildtime/vite/releases.js +1 -1
  17. package/dist/components/changelog/changelog-entry.svelte +6 -1
  18. package/dist/components/chrome/search.svelte +8 -5
  19. package/dist/components/chrome/version-banner.svelte +5 -20
  20. package/dist/components/chrome/version-banner.svelte.d.ts +3 -3
  21. package/dist/components/chrome/version-switcher.svelte +39 -49
  22. package/dist/components/chrome/version-switcher.svelte.d.ts +3 -9
  23. package/dist/components/docs-page-context.d.ts +20 -0
  24. package/dist/components/docs-page-context.js +41 -0
  25. package/dist/components/layouts/docs-header.svelte +9 -23
  26. package/dist/components/layouts/docs-header.svelte.d.ts +1 -9
  27. package/dist/components/layouts/docs-mobile-header.svelte +6 -21
  28. package/dist/components/layouts/docs-mobile-header.svelte.d.ts +1 -9
  29. package/dist/components/layouts/docs-shell.svelte +29 -28
  30. package/dist/core/changelog.d.ts +4 -1
  31. package/dist/core/content.d.ts +6 -1
  32. package/dist/core/docs-page.d.ts +15 -0
  33. package/dist/core/docs-page.js +20 -3
  34. package/dist/core/index.d.ts +1 -1
  35. package/dist/core/version.d.ts +36 -0
  36. package/dist/core/version.js +101 -0
  37. package/dist/search/context.svelte.d.ts +0 -2
  38. package/dist/search/context.svelte.js +0 -2
  39. package/package.json +1 -1
@@ -1,12 +1,7 @@
1
1
  <script lang="ts">
2
2
  import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
3
3
  import ArrowRight from '@lucide/svelte/icons/arrow-right';
4
- import {
5
- mapPathToVersion,
6
- scopeContent,
7
- type ResolvedVersion,
8
- type DocsContentItem
9
- } from '../../core/index.js';
4
+ import type { ResolvedVersion } from '../../core/index.js';
10
5
 
11
6
  // Rendered by DocsShell only on an archived version, to warn a reader who
12
7
  // landed there (usually from a search engine) and give them a one-click path
@@ -15,23 +10,13 @@
15
10
  const {
16
11
  active,
17
12
  current,
18
- pathname,
19
- content
13
+ href
20
14
  }: {
21
15
  active: ResolvedVersion;
22
16
  current: ResolvedVersion;
23
- pathname: string;
24
- content: DocsContentItem[];
17
+ /** This page under the current version, from the resolved page view. */
18
+ href: string;
25
19
  } = $props();
26
-
27
- const currentHref = $derived(
28
- mapPathToVersion(
29
- pathname,
30
- active,
31
- current,
32
- scopeContent(content, current.id).map((c) => c.path)
33
- )
34
- );
35
20
  </script>
36
21
 
37
22
  <div class="docsmith-version-banner" role="note">
@@ -39,7 +24,7 @@
39
24
  <p>
40
25
  You're reading the <strong>{active.label}</strong> docs. The current version is
41
26
  <strong>{current.label}</strong>.
42
- <a href={currentHref}>View this page in {current.label} <ArrowRight size={14} /></a>
27
+ <a {href}>View this page in {current.label} <ArrowRight size={14} /></a>
43
28
  </p>
44
29
  </div>
45
30
 
@@ -1,9 +1,9 @@
1
- import { type ResolvedVersion, type DocsContentItem } from '../../core/index.js';
1
+ import type { ResolvedVersion } from '../../core/index.js';
2
2
  type $$ComponentProps = {
3
3
  active: ResolvedVersion;
4
4
  current: ResolvedVersion;
5
- pathname: string;
6
- content: DocsContentItem[];
5
+ /** This page under the current version, from the resolved page view. */
6
+ href: string;
7
7
  };
8
8
  declare const VersionBanner: import("svelte").Component<$$ComponentProps, {}, "">;
9
9
  type VersionBanner = ReturnType<typeof VersionBanner>;
@@ -4,60 +4,50 @@
4
4
  import * as DropdownMenu from '../shadcn/dropdown-menu/index.js';
5
5
  import Check from '@lucide/svelte/icons/check';
6
6
  import ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down';
7
- import {
8
- mapPathToVersion,
9
- scopeContent,
10
- type ResolvedVersion,
11
- type DocsContentItem
12
- } from '../../core/index.js';
7
+ import type { VersionLink } from '../../core/index.js';
13
8
 
9
+ // Purely presentational: where each version leads is resolved once, with the
10
+ // rest of the page view, in `core/docs-page.ts`.
14
11
  const {
15
- versions,
16
- active,
17
- pathname,
18
- content
12
+ links
19
13
  }: {
20
- /** All declared versions, in switcher order. */
21
- versions: ResolvedVersion[];
22
- /** The version the current page belongs to. */
23
- active: ResolvedVersion;
24
- /** The current normalized pathname, e.g. `/docs/v2/intro`. */
25
- pathname: string;
26
- /** Full content index, to map the current page into the chosen version. */
27
- content: DocsContentItem[];
14
+ /** Every declared version and its destination, in switcher order. */
15
+ links: VersionLink[];
28
16
  } = $props();
29
17
 
30
- function select(target: ResolvedVersion) {
31
- if (target.id === active.id) return;
32
- const paths = scopeContent(content, target.id).map((c) => c.path);
33
- goto(mapPathToVersion(pathname, active, target, paths));
18
+ const active = $derived(links.find((link) => link.active));
19
+
20
+ function select(link: VersionLink) {
21
+ if (link.active) return;
22
+ goto(link.href);
34
23
  }
35
24
  </script>
36
25
 
37
- <DropdownMenu.Root>
38
- <DropdownMenu.Trigger>
39
- {#snippet child({ props })}
40
- <Button
41
- variant="outline"
42
- size="sm"
43
- class="h-8 gap-1.5 px-2.5 font-medium"
44
- aria-label={`Documentation version: ${active.label}. Change version`}
45
- {...props}
46
- >
47
- <span class="truncate">{active.label}</span>
48
- <ChevronsUpDown class="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
49
- </Button>
50
- {/snippet}
51
- </DropdownMenu.Trigger>
52
- <DropdownMenu.Content align="start" class="w-44">
53
- {#each versions as version (version.id)}
54
- <DropdownMenu.Item onSelect={() => select(version)} class="gap-2">
55
- <Check
56
- class="size-4 {version.id === active.id ? 'opacity-100' : 'opacity-0'}"
57
- aria-hidden="true"
58
- />
59
- <span class="flex-1 truncate">{version.label}</span>
60
- </DropdownMenu.Item>
61
- {/each}
62
- </DropdownMenu.Content>
63
- </DropdownMenu.Root>
26
+ <!-- Nothing to switch from without an active version, which only happens on an
27
+ unversioned site, where the header doesn't render this at all. -->
28
+ {#if active}
29
+ <DropdownMenu.Root>
30
+ <DropdownMenu.Trigger>
31
+ {#snippet child({ props })}
32
+ <Button
33
+ variant="outline"
34
+ size="sm"
35
+ class="h-8 gap-1.5 px-2.5 font-medium"
36
+ aria-label={`Documentation version: ${active.label}. Change version`}
37
+ {...props}
38
+ >
39
+ <span class="truncate">{active.label}</span>
40
+ <ChevronsUpDown class="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
41
+ </Button>
42
+ {/snippet}
43
+ </DropdownMenu.Trigger>
44
+ <DropdownMenu.Content align="start" class="w-44">
45
+ {#each links as link (link.id)}
46
+ <DropdownMenu.Item onSelect={() => select(link)} class="gap-2">
47
+ <Check class="size-4 {link.active ? 'opacity-100' : 'opacity-0'}" aria-hidden="true" />
48
+ <span class="flex-1 truncate">{link.label}</span>
49
+ </DropdownMenu.Item>
50
+ {/each}
51
+ </DropdownMenu.Content>
52
+ </DropdownMenu.Root>
53
+ {/if}
@@ -1,13 +1,7 @@
1
- import { type ResolvedVersion, type DocsContentItem } from '../../core/index.js';
1
+ import type { VersionLink } from '../../core/index.js';
2
2
  type $$ComponentProps = {
3
- /** All declared versions, in switcher order. */
4
- versions: ResolvedVersion[];
5
- /** The version the current page belongs to. */
6
- active: ResolvedVersion;
7
- /** The current normalized pathname, e.g. `/docs/v2/intro`. */
8
- pathname: string;
9
- /** Full content index, to map the current page into the chosen version. */
10
- content: DocsContentItem[];
3
+ /** Every declared version and its destination, in switcher order. */
4
+ links: VersionLink[];
11
5
  };
12
6
  declare const VersionSwitcher: import("svelte").Component<$$ComponentProps, {}, "">;
13
7
  type VersionSwitcher = ReturnType<typeof VersionSwitcher>;
@@ -0,0 +1,20 @@
1
+ import type { DocsPageView } from '../core/index.js';
2
+ /** A live read of the page view: `view` re-reads the shell's `$derived`. */
3
+ export type DocsPageContext = {
4
+ readonly view: DocsPageView;
5
+ };
6
+ /**
7
+ * Publish the page view. `DocsShell` calls this once, with a getter rather than
8
+ * a value, so readers see the current `$derived` rather than the one that
9
+ * existed at init.
10
+ */
11
+ export declare function setDocsPage(view: () => DocsPageView): void;
12
+ /**
13
+ * Read the page view. `getContext` only works during init, so this hands back
14
+ * an accessor rather than a snapshot: hold it at init, read `.view` where the
15
+ * value is used, and the read stays reactive.
16
+ *
17
+ * Throws outside `DocsShell`. Every caller is internal chrome the shell renders
18
+ * itself, so an absent context is a wiring bug, not a supported mode.
19
+ */
20
+ export declare function useDocsPage(): DocsPageContext;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The resolved page view, on context.
3
+ *
4
+ * `core/docs-page.ts` decides what the page is; this is the single route that
5
+ * decision travels to the chrome. Publishing it once is what keeps the headers
6
+ * from taking props described in terms of their grandchildren, and the search
7
+ * scope from being pushed into shared mutable state by an effect.
8
+ *
9
+ * Sits at the root of `components/` rather than in a concern folder of its own:
10
+ * `core/` stays free of any Svelte import, and both `layouts/` and `chrome/`
11
+ * read this, so it can't live in either without one importing the other.
12
+ */
13
+ import { getContext, setContext } from 'svelte';
14
+ const KEY = Symbol('docsmith-page');
15
+ /**
16
+ * Publish the page view. `DocsShell` calls this once, with a getter rather than
17
+ * a value, so readers see the current `$derived` rather than the one that
18
+ * existed at init.
19
+ */
20
+ export function setDocsPage(view) {
21
+ setContext(KEY, view);
22
+ }
23
+ /**
24
+ * Read the page view. `getContext` only works during init, so this hands back
25
+ * an accessor rather than a snapshot: hold it at init, read `.view` where the
26
+ * value is used, and the read stays reactive.
27
+ *
28
+ * Throws outside `DocsShell`. Every caller is internal chrome the shell renders
29
+ * itself, so an absent context is a wiring bug, not a supported mode.
30
+ */
31
+ export function useDocsPage() {
32
+ const get = getContext(KEY);
33
+ if (!get) {
34
+ throw new Error('[svelte-docsmith] this component must be rendered inside <DocsShell>.');
35
+ }
36
+ return {
37
+ get view() {
38
+ return get();
39
+ }
40
+ };
41
+ }
@@ -7,46 +7,32 @@
7
7
  import { useSearch } from '../../search/context.svelte.js';
8
8
  import ThemeToggle from '../chrome/theme-toggle.svelte';
9
9
  import VersionSwitcher from '../chrome/version-switcher.svelte';
10
- import type {
11
- DocsmithConfig,
12
- DocsmithLink,
13
- DocsContentItem,
14
- ResolvedVersion
15
- } from '../../core/index.js';
10
+ import type { DocsmithConfig, DocsmithLink } from '../../core/index.js';
16
11
  import BookOpenText from '@lucide/svelte/icons/book-open-text';
17
12
  import type { Snippet } from 'svelte';
18
- import { page } from '$app/state';
19
13
  import { cn } from '../../utils/cn.js';
20
14
  import { normalizePath } from '../../utils/normalize-path.js';
15
+ import { useDocsPage } from '../docs-page-context.js';
21
16
 
22
17
  const {
23
18
  config,
24
19
  logo,
25
- actions,
26
- versions = [],
27
- active,
28
- content = [],
29
- pathname = ''
20
+ actions
30
21
  }: {
31
22
  config: DocsmithConfig;
32
23
  /** Custom logo mark; defaults to a book icon in a primary-tinted chip. */
33
24
  logo?: Snippet;
34
25
  /** Extra header controls, rendered before the theme toggle. */
35
26
  actions?: Snippet;
36
- /** Declared versions; renders the switcher once there's more than one. */
37
- versions?: ResolvedVersion[];
38
- /** The active version, for the switcher's current selection. */
39
- active?: ResolvedVersion;
40
- /** Content index, so the switcher can map the current page across versions. */
41
- content?: DocsContentItem[];
42
- /** Current normalized pathname, for the switcher's page mapping. */
43
- pathname?: string;
44
27
  } = $props();
45
28
 
46
29
  // Present only when the consumer passed a `search` loader to DocsShell.
47
30
  const search = useSearch();
48
31
 
49
- const current = $derived(normalizePath(page.url.pathname));
32
+ // Everything about the page being read comes off the shell's one resolved
33
+ // view, including the normalized path the nav highlights against.
34
+ const docsPage = useDocsPage();
35
+ const current = $derived(docsPage.view.pathname);
50
36
 
51
37
  /**
52
38
  * A header link is usually the entry point to a whole section rather than a
@@ -109,9 +95,9 @@
109
95
  </nav>
110
96
  {/if}
111
97
 
112
- {#if versions.length > 1 && active}
98
+ {#if docsPage.view.versionLinks.length > 1}
113
99
  <div class="px-1">
114
- <VersionSwitcher {versions} {active} {content} {pathname} />
100
+ <VersionSwitcher links={docsPage.view.versionLinks} />
115
101
  </div>
116
102
  {:else if config.version}
117
103
  <div class="px-2">
@@ -1,4 +1,4 @@
1
- import type { DocsmithConfig, DocsContentItem, ResolvedVersion } from '../../core/index.js';
1
+ import type { DocsmithConfig } from '../../core/index.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  type $$ComponentProps = {
4
4
  config: DocsmithConfig;
@@ -6,14 +6,6 @@ type $$ComponentProps = {
6
6
  logo?: Snippet;
7
7
  /** Extra header controls, rendered before the theme toggle. */
8
8
  actions?: Snippet;
9
- /** Declared versions; renders the switcher once there's more than one. */
10
- versions?: ResolvedVersion[];
11
- /** The active version, for the switcher's current selection. */
12
- active?: ResolvedVersion;
13
- /** Content index, so the switcher can map the current page across versions. */
14
- content?: DocsContentItem[];
15
- /** Current normalized pathname, for the switcher's page mapping. */
16
- pathname?: string;
17
9
  };
18
10
  declare const DocsHeader: import("svelte").Component<$$ComponentProps, {}, "">;
19
11
  type DocsHeader = ReturnType<typeof DocsHeader>;
@@ -1,11 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { page } from '$app/state';
3
- import type {
4
- DocsmithConfig,
5
- NavGroup,
6
- DocsContentItem,
7
- ResolvedVersion
8
- } from '../../core/index.js';
3
+ import type { DocsmithConfig, NavGroup } from '../../core/index.js';
9
4
  import type { TocItem } from '../../toc/index.js';
10
5
  import DocsSidebar from './docs-sidebar.svelte';
11
6
  import TableOfContents from '../chrome/table-of-contents.svelte';
@@ -17,6 +12,7 @@
17
12
  import { Separator } from '../shadcn/separator/index.js';
18
13
  import * as Sheet from '../shadcn/sheet/index.js';
19
14
  import { useSearch } from '../../search/context.svelte.js';
15
+ import { useDocsPage } from '../docs-page-context.js';
20
16
  import BookOpenText from '@lucide/svelte/icons/book-open-text';
21
17
  import Menu from '@lucide/svelte/icons/menu';
22
18
  import PanelRight from '@lucide/svelte/icons/panel-right';
@@ -30,11 +26,7 @@
30
26
  tocItems = [],
31
27
  tocActiveId = null,
32
28
  logo,
33
- actions,
34
- versions = [],
35
- active,
36
- content = [],
37
- pathname = ''
29
+ actions
38
30
  }: {
39
31
  config: DocsmithConfig;
40
32
  /** Sidebar groups; omit on non-doc pages (the landing/`page` layout). */
@@ -47,17 +39,10 @@
47
39
  tocActiveId?: string | null;
48
40
  logo?: Snippet;
49
41
  actions?: Snippet;
50
- /** Declared versions; renders the switcher in the drawer once there's more than one. */
51
- versions?: ResolvedVersion[];
52
- /** The active version, for the switcher's current selection. */
53
- active?: ResolvedVersion;
54
- /** Content index, so the switcher can map the current page across versions. */
55
- content?: DocsContentItem[];
56
- /** Current normalized pathname, for the switcher's page mapping. */
57
- pathname?: string;
58
42
  } = $props();
59
43
 
60
44
  const search = useSearch();
45
+ const docsPage = useDocsPage();
61
46
 
62
47
  let isMenuOpen = $state(false);
63
48
  let isTocOpen = $state(false);
@@ -95,9 +80,9 @@
95
80
  <span class="inline-block">{config.title}</span>
96
81
  </a>
97
82
  </div>
98
- {#if versions.length > 1 && active}
83
+ {#if docsPage.view.versionLinks.length > 1}
99
84
  <div class="px-7 pb-3">
100
- <VersionSwitcher {versions} {active} {content} {pathname} />
85
+ <VersionSwitcher links={docsPage.view.versionLinks} />
101
86
  </div>
102
87
  {/if}
103
88
  <ScrollArea class="my-4 h-[calc(100vh-8rem)] pb-10 pl-2">
@@ -1,4 +1,4 @@
1
- import type { DocsmithConfig, NavGroup, DocsContentItem, ResolvedVersion } from '../../core/index.js';
1
+ import type { DocsmithConfig, NavGroup } from '../../core/index.js';
2
2
  import type { TocItem } from '../../toc/index.js';
3
3
  import type { Snippet } from 'svelte';
4
4
  type $$ComponentProps = {
@@ -13,14 +13,6 @@ type $$ComponentProps = {
13
13
  tocActiveId?: string | null;
14
14
  logo?: Snippet;
15
15
  actions?: Snippet;
16
- /** Declared versions; renders the switcher in the drawer once there's more than one. */
17
- versions?: ResolvedVersion[];
18
- /** The active version, for the switcher's current selection. */
19
- active?: ResolvedVersion;
20
- /** Content index, so the switcher can map the current page across versions. */
21
- content?: DocsContentItem[];
22
- /** Current normalized pathname, for the switcher's page mapping. */
23
- pathname?: string;
24
16
  };
25
17
  declare const DocsMobileHeader: import("svelte").Component<$$ComponentProps, {}, "">;
26
18
  type DocsMobileHeader = ReturnType<typeof DocsMobileHeader>;
@@ -10,6 +10,7 @@
10
10
  } from '../../core/index.js';
11
11
  import { createToc } from '../../toc/index.js';
12
12
  import { createSearchState } from '../../search/context.svelte.js';
13
+ import { setDocsPage } from '../docs-page-context.js';
13
14
  import Search from '../chrome/search.svelte';
14
15
  import BackgroundPattern from '../chrome/background-pattern.svelte';
15
16
  import ThemeProvider from '../chrome/theme-provider.svelte';
@@ -107,7 +108,7 @@
107
108
  // `search` loader is static for the shell's lifetime, so reading it once at
108
109
  // init is intentional.
109
110
  // svelte-ignore state_referenced_locally
110
- const searchState = search ? createSearchState() : undefined;
111
+ if (search) createSearchState();
111
112
 
112
113
  // Every rule about the page being read is resolved in one pure module
113
114
  // (`core/docs-page.ts`): the active version and the content scoped to it, the
@@ -125,16 +126,23 @@
125
126
  })
126
127
  );
127
128
 
128
- // Keep the search palette scoped to the version currently being read.
129
- $effect(() => {
130
- if (searchState) searchState.version = view.activeVersionId;
131
- });
129
+ // One route to the chrome. The headers, the switcher and the search palette
130
+ // read what they need off this instead of taking it as props through two
131
+ // intermediate layers.
132
+ setDocsPage(() => view);
132
133
 
134
+ // `timeZone: 'UTC'` is not cosmetic. `lastUpdated` is a calendar day, which
135
+ // parses to UTC midnight, so formatting in the ambient zone shifts it a day
136
+ // west of UTC — and shifts it on the client but not on the server, so the
137
+ // rendered date changes under hydration.
133
138
  const lastUpdatedLabel = $derived.by(() =>
134
139
  view.lastUpdated
135
- ? new Intl.DateTimeFormat('en', { year: 'numeric', month: 'short', day: 'numeric' }).format(
136
- view.lastUpdated
137
- )
140
+ ? new Intl.DateTimeFormat('en', {
141
+ year: 'numeric',
142
+ month: 'short',
143
+ day: 'numeric',
144
+ timeZone: 'UTC'
145
+ }).format(view.lastUpdated)
138
146
  : undefined
139
147
  );
140
148
  const showFooterMeta = $derived(Boolean(view.editHref || lastUpdatedLabel));
@@ -145,6 +153,16 @@
145
153
 
146
154
  const breadcrumbs = $derived(view.breadcrumbs.map((title) => ({ title })));
147
155
 
156
+ // The archived-version banner, or nothing. Its "view this page in <current>"
157
+ // link is the current version's entry in the same list the switcher renders,
158
+ // so the two can't disagree about where the reader lands.
159
+ const archivedBanner = $derived.by(() => {
160
+ const { isArchived, activeVersion, currentVersion, versionLinks } = view;
161
+ if (!isArchived || !activeVersion || !currentVersion) return undefined;
162
+ const href = versionLinks.find((link) => link.id === currentVersion.id)?.href;
163
+ return href ? { active: activeVersion, current: currentVersion, href } : undefined;
164
+ });
165
+
148
166
  // In-page TOC, scanned from the rendered content and re-scanned after every
149
167
  // navigation (client-side included) so it never goes stale.
150
168
  let contentEl = $state<HTMLElement | null>(null);
@@ -194,15 +212,7 @@
194
212
 
195
213
  <!-- One header system everywhere: DocsHeader on desktop, DocsMobileHeader
196
214
  below lg. The `page` layout just omits the sidebar nav and in-page TOC. -->
197
- <DocsHeader
198
- {config}
199
- {logo}
200
- {actions}
201
- {versions}
202
- active={view.activeVersion}
203
- {content}
204
- pathname={view.pathname}
205
- />
215
+ <DocsHeader {config} {logo} {actions} />
206
216
 
207
217
  {#if layout === 'page'}
208
218
  <DocsMobileHeader {config} {logo} {actions} />
@@ -219,10 +229,6 @@
219
229
  tocActiveId={toc.activeId}
220
230
  {logo}
221
231
  {actions}
222
- {versions}
223
- active={view.activeVersion}
224
- {content}
225
- pathname={view.pathname}
226
232
  />
227
233
 
228
234
  <div class="mx-auto flex w-full max-w-7xl flex-1 gap-12 px-4 md:px-6 lg:px-8 lg:pt-10">
@@ -234,13 +240,8 @@
234
240
  tabindex="-1"
235
241
  class="min-w-0 flex-1 py-6 lg:py-0"
236
242
  >
237
- {#if view.isArchived && view.activeVersion && view.currentVersion}
238
- <VersionBanner
239
- active={view.activeVersion}
240
- current={view.currentVersion}
241
- pathname={view.pathname}
242
- {content}
243
- />
243
+ {#if archivedBanner}
244
+ <VersionBanner {...archivedBanner} />
244
245
  {/if}
245
246
 
246
247
  <div class="flex items-start justify-between gap-4">
@@ -19,7 +19,10 @@ export type ChangelogGroup = {
19
19
  export type ChangelogRelease = {
20
20
  /** Version string as it appears in the changelog, e.g. `0.8.0`. */
21
21
  version: string;
22
- /** Release date (ISO) from the git history, when it can be determined. */
22
+ /**
23
+ * Release day (`YYYY-MM-DD`) from the git history, when it can be determined.
24
+ * Rendered as a UTC calendar day, so it reads the same for every reader.
25
+ */
23
26
  date?: string;
24
27
  groups: ChangelogGroup[];
25
28
  /**
@@ -23,7 +23,12 @@ export type DocsContentItem = {
23
23
  description?: string;
24
24
  /** Source file path relative to the app cwd, for the "Edit this page" link. */
25
25
  sourcePath?: string;
26
- /** Last git commit date (ISO) for the page's source file. */
26
+ /**
27
+ * The day (`YYYY-MM-DD`) the page's source file last changed, from git, or
28
+ * from the page's own `lastUpdated` frontmatter when it has one. A calendar
29
+ * day rather than a timestamp: it is rendered as a UTC day, so it reads the
30
+ * same for every reader and matches on the server and the client.
31
+ */
27
32
  lastUpdated?: string;
28
33
  /** Estimated reading time in whole minutes (at ~200 wpm), min 1. */
29
34
  readingTime?: number;
@@ -13,6 +13,15 @@ import { type NavGroup, type NavItem } from './nav.js';
13
13
  import { type ResolvedVersion } from './version.js';
14
14
  /** The page's build-time headings, as extracted into the content index. */
15
15
  type PageToc = NonNullable<DocsContentItem['toc']>;
16
+ /** One declared version, and where switching to it should take the reader. */
17
+ export type VersionLink = {
18
+ id: string;
19
+ label: string;
20
+ /** This page under that version, or its landing when the page isn't there. */
21
+ href: string;
22
+ /** Whether this is the version being read. */
23
+ active: boolean;
24
+ };
16
25
  /** Everything `DocsShell` renders about the current page. */
17
26
  export type DocsPageView = {
18
27
  /** The current path, trailing slash stripped. Match content against this. */
@@ -53,6 +62,12 @@ export type DocsPageView = {
53
62
  breadcrumbs: string[];
54
63
  /** The page's server-rendered TOC, from {@link entry}. */
55
64
  toc: PageToc;
65
+ /**
66
+ * Every declared version with the URL that keeps the reader on this page, in
67
+ * switcher order. Empty on an unversioned site, so the switcher and the
68
+ * archived-version banner both render off this one list.
69
+ */
70
+ versionLinks: VersionLink[];
56
71
  };
57
72
  /**
58
73
  * Resolve the page being read. `pathname` is taken raw (normalized here, so the
@@ -1,5 +1,5 @@
1
1
  import { navFromContent, flattenNav, navTrail } from './nav.js';
2
- import { activeVersion, currentVersion, scopeContent } from './version.js';
2
+ import { activeVersion, currentVersion, mapPathToVersion, scopeContent } from './version.js';
3
3
  import { normalizePath } from '../utils/normalize-path.js';
4
4
  /**
5
5
  * Resolve the page being read. `pathname` is taken raw (normalized here, so the
@@ -43,10 +43,27 @@ export function resolveDocsPage(input) {
43
43
  next: pageIndex >= 0 && pageIndex < flatNav.length - 1 ? flatNav[pageIndex + 1] : undefined,
44
44
  title,
45
45
  breadcrumbs,
46
- toc: entry?.toc ?? []
46
+ toc: entry?.toc ?? [],
47
+ versionLinks: active
48
+ ? versions.map((version) => ({
49
+ id: version.id,
50
+ label: version.label,
51
+ // Switching to the version already being read is a no-op, so its
52
+ // link is this page. Mapping it like the others would send a reader
53
+ // who is off the docs tree to the docs landing instead.
54
+ href: version.id === active.id
55
+ ? pathname
56
+ : mapPathToVersion(pathname, active, version, scopeContent(content, version.id).map((item) => item.path)),
57
+ active: version.id === active.id
58
+ }))
59
+ : []
47
60
  };
48
61
  }
49
- /** An ISO stamp as a Date, or `undefined` when absent or unparseable. */
62
+ /**
63
+ * A `YYYY-MM-DD` day as a Date, or `undefined` when absent or unparseable. The
64
+ * result is UTC midnight, so it must be formatted in UTC to read back as the
65
+ * same day.
66
+ */
50
67
  function parseDate(iso) {
51
68
  if (!iso)
52
69
  return undefined;
@@ -2,4 +2,4 @@ export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFoot
2
2
  export type { DocsContentItem, SearchDoc, LlmsDoc } from './content.js';
3
3
  export { resolveVersions, activeVersion, currentVersion, currentOnly, scopeContent, mapPathToVersion, type DocsVersion, type DocsVersions, type ResolvedVersion } from './version.js';
4
4
  export { navFromContent, flattenNav, navTrail, isNavGroup, type NavItem, type NavGroup, type NavNode } from './nav.js';
5
- export { resolveDocsPage, type DocsPageView } from './docs-page.js';
5
+ export { resolveDocsPage, type DocsPageView, type VersionLink } from './docs-page.js';