@axerity/cli 0.1.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 (186) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/axerity.default.json +135 -0
  4. package/axerity.schema.json +268 -0
  5. package/bin/axerity.js +305 -0
  6. package/mdsvex.config.js +261 -0
  7. package/package.json +103 -0
  8. package/scripts/prepare-engine.mjs +20 -0
  9. package/src/app.d.ts +17 -0
  10. package/src/app.html +39 -0
  11. package/src/content/demo/api/meta.json +5 -0
  12. package/src/content/demo/api/pet/add-pet.md +105 -0
  13. package/src/content/demo/api/pet/delete-pet.md +70 -0
  14. package/src/content/demo/api/pet/find-by-status.md +72 -0
  15. package/src/content/demo/api/pet/find-by-tags.md +64 -0
  16. package/src/content/demo/api/pet/get-pet.md +99 -0
  17. package/src/content/demo/api/pet/meta.json +15 -0
  18. package/src/content/demo/api/pet/pet-object.md +112 -0
  19. package/src/content/demo/api/pet/update-pet-with-form.md +69 -0
  20. package/src/content/demo/api/pet/update-pet.md +79 -0
  21. package/src/content/demo/api/pet/upload-image.md +79 -0
  22. package/src/content/demo/api/store/delete-order.md +62 -0
  23. package/src/content/demo/api/store/get-order.md +70 -0
  24. package/src/content/demo/api/store/inventory.md +54 -0
  25. package/src/content/demo/api/store/meta.json +5 -0
  26. package/src/content/demo/api/store/order-object.md +77 -0
  27. package/src/content/demo/api/store/place-order.md +83 -0
  28. package/src/content/demo/api/user/create-user.md +69 -0
  29. package/src/content/demo/api/user/create-with-list.md +57 -0
  30. package/src/content/demo/api/user/delete-user.md +61 -0
  31. package/src/content/demo/api/user/get-user.md +69 -0
  32. package/src/content/demo/api/user/login.md +80 -0
  33. package/src/content/demo/api/user/logout.md +45 -0
  34. package/src/content/demo/api/user/meta.json +14 -0
  35. package/src/content/demo/api/user/update-user.md +69 -0
  36. package/src/content/demo/api/user/user-object.md +85 -0
  37. package/src/content/demo/changelog.md +44 -0
  38. package/src/content/demo/components/accordion.md +70 -0
  39. package/src/content/demo/components/api.md +185 -0
  40. package/src/content/demo/components/badge.md +34 -0
  41. package/src/content/demo/components/callout.md +83 -0
  42. package/src/content/demo/components/cards.md +88 -0
  43. package/src/content/demo/components/code-group.md +55 -0
  44. package/src/content/demo/components/columns.md +42 -0
  45. package/src/content/demo/components/frame.md +51 -0
  46. package/src/content/demo/components/icon.md +54 -0
  47. package/src/content/demo/components/kbd.md +28 -0
  48. package/src/content/demo/components/meta.json +26 -0
  49. package/src/content/demo/components/roadmap.md +86 -0
  50. package/src/content/demo/components/steps.md +72 -0
  51. package/src/content/demo/components/tabs.md +146 -0
  52. package/src/content/demo/components/tooltip.md +44 -0
  53. package/src/content/demo/components/tree.md +83 -0
  54. package/src/content/demo/components/type-table.md +77 -0
  55. package/src/content/demo/components/update.md +48 -0
  56. package/src/content/demo/components/video.md +56 -0
  57. package/src/content/demo/components/webhooks.md +109 -0
  58. package/src/content/demo/components/websockets.md +101 -0
  59. package/src/content/demo/configuration/ai.md +40 -0
  60. package/src/content/demo/configuration/cli.md +46 -0
  61. package/src/content/demo/configuration/deployment.md +105 -0
  62. package/src/content/demo/configuration/index.md +92 -0
  63. package/src/content/demo/configuration/layouts.md +51 -0
  64. package/src/content/demo/configuration/meta.json +5 -0
  65. package/src/content/demo/configuration/navigation.md +167 -0
  66. package/src/content/demo/configuration/openapi.md +103 -0
  67. package/src/content/demo/configuration/search.md +36 -0
  68. package/src/content/demo/index.md +59 -0
  69. package/src/content/demo/installation.md +49 -0
  70. package/src/content/demo/meta.json +15 -0
  71. package/src/content/demo/quick-start.md +47 -0
  72. package/src/content/demo/theming/advanced.md +116 -0
  73. package/src/content/demo/theming/code.md +66 -0
  74. package/src/content/demo/theming/colors.md +103 -0
  75. package/src/content/demo/theming/index.md +88 -0
  76. package/src/content/demo/theming/layout.md +71 -0
  77. package/src/content/demo/theming/meta.json +5 -0
  78. package/src/content/demo/theming/themes.md +99 -0
  79. package/src/content/demo/theming/typography.md +83 -0
  80. package/src/content/demo/writing/code-blocks.md +154 -0
  81. package/src/content/demo/writing/diagrams.md +44 -0
  82. package/src/content/demo/writing/frontmatter.md +33 -0
  83. package/src/content/demo/writing/markdown.md +62 -0
  84. package/src/content/demo/writing/meta.json +5 -0
  85. package/src/hooks.server.ts +49 -0
  86. package/src/lib/assets/favicon.svg +1 -0
  87. package/src/lib/base.ts +12 -0
  88. package/src/lib/components/DynamicIcon.svelte +26 -0
  89. package/src/lib/components/docs/Analytics.svelte +38 -0
  90. package/src/lib/components/docs/Banner.svelte +44 -0
  91. package/src/lib/components/docs/Breadcrumbs.svelte +38 -0
  92. package/src/lib/components/docs/CopyPageMenu.svelte +119 -0
  93. package/src/lib/components/docs/DocsLayout.svelte +192 -0
  94. package/src/lib/components/docs/Footer.svelte +60 -0
  95. package/src/lib/components/docs/Mermaid.svelte +39 -0
  96. package/src/lib/components/docs/Navbar.svelte +144 -0
  97. package/src/lib/components/docs/PageMeta.svelte +35 -0
  98. package/src/lib/components/docs/PageNav.svelte +44 -0
  99. package/src/lib/components/docs/SearchDialog.svelte +182 -0
  100. package/src/lib/components/docs/Sidebar.svelte +85 -0
  101. package/src/lib/components/docs/SidebarDropdown.svelte +56 -0
  102. package/src/lib/components/docs/SidebarFooterLinks.svelte +19 -0
  103. package/src/lib/components/docs/SidebarGroup.svelte +54 -0
  104. package/src/lib/components/docs/SidebarLink.svelte +67 -0
  105. package/src/lib/components/docs/TableOfContents.svelte +77 -0
  106. package/src/lib/components/docs/ThemeToggle.svelte +19 -0
  107. package/src/lib/components/docs/VersionSwitcher.svelte +80 -0
  108. package/src/lib/components/kit/Accordion.svelte +60 -0
  109. package/src/lib/components/kit/AccordionGroup.svelte +13 -0
  110. package/src/lib/components/kit/Badge.svelte +32 -0
  111. package/src/lib/components/kit/Callout.svelte +51 -0
  112. package/src/lib/components/kit/Card.svelte +72 -0
  113. package/src/lib/components/kit/CardGroup.svelte +21 -0
  114. package/src/lib/components/kit/CodeGroup.svelte +65 -0
  115. package/src/lib/components/kit/Columns.svelte +26 -0
  116. package/src/lib/components/kit/Event.svelte +23 -0
  117. package/src/lib/components/kit/EventList.svelte +9 -0
  118. package/src/lib/components/kit/File.svelte +15 -0
  119. package/src/lib/components/kit/Folder.svelte +46 -0
  120. package/src/lib/components/kit/Frame.svelte +81 -0
  121. package/src/lib/components/kit/Icon.svelte +17 -0
  122. package/src/lib/components/kit/Kbd.svelte +11 -0
  123. package/src/lib/components/kit/Roadmap.svelte +15 -0
  124. package/src/lib/components/kit/RoadmapItem.svelte +109 -0
  125. package/src/lib/components/kit/Step.svelte +63 -0
  126. package/src/lib/components/kit/Steps.svelte +16 -0
  127. package/src/lib/components/kit/Tab.svelte +27 -0
  128. package/src/lib/components/kit/Tabs.svelte +75 -0
  129. package/src/lib/components/kit/Tooltip.svelte +33 -0
  130. package/src/lib/components/kit/Tree.svelte +11 -0
  131. package/src/lib/components/kit/TypeTable.svelte +187 -0
  132. package/src/lib/components/kit/Update.svelte +32 -0
  133. package/src/lib/components/kit/Video.svelte +64 -0
  134. package/src/lib/components/kit/accordion-context.ts +1 -0
  135. package/src/lib/components/kit/api/Api.svelte +80 -0
  136. package/src/lib/components/kit/api/ApiExamplePanel.svelte +100 -0
  137. package/src/lib/components/kit/api/ApiField.svelte +124 -0
  138. package/src/lib/components/kit/api/Channel.svelte +121 -0
  139. package/src/lib/components/kit/api/Endpoint.svelte +116 -0
  140. package/src/lib/components/kit/api/Enum.svelte +44 -0
  141. package/src/lib/components/kit/api/EnumValues.svelte +35 -0
  142. package/src/lib/components/kit/api/Expandable.svelte +70 -0
  143. package/src/lib/components/kit/api/Message.svelte +67 -0
  144. package/src/lib/components/kit/api/ObjectExample.svelte +11 -0
  145. package/src/lib/components/kit/api/RequestExample.svelte +11 -0
  146. package/src/lib/components/kit/api/ResponseExample.svelte +11 -0
  147. package/src/lib/components/kit/api/Webhook.svelte +115 -0
  148. package/src/lib/components/kit/api/api-context.ts +15 -0
  149. package/src/lib/components/kit/tabs-context.ts +8 -0
  150. package/src/lib/components/kit/tabs-store.svelte.ts +28 -0
  151. package/src/lib/config/site.ts +34 -0
  152. package/src/lib/content/index.ts +50 -0
  153. package/src/lib/content/raw.ts +21 -0
  154. package/src/lib/content/tree.ts +169 -0
  155. package/src/lib/index.ts +79 -0
  156. package/src/lib/nav-match.ts +23 -0
  157. package/src/lib/openapi/generate.ts +629 -0
  158. package/src/lib/server/og.ts +140 -0
  159. package/src/lib/state/search.svelte.ts +9 -0
  160. package/src/lib/state/theme.svelte.ts +58 -0
  161. package/src/lib/types.ts +216 -0
  162. package/src/params/docpage.ts +3 -0
  163. package/src/params/mdfile.ts +3 -0
  164. package/src/routes/+error.svelte +46 -0
  165. package/src/routes/+layout.svelte +25 -0
  166. package/src/routes/[...path=mdfile]/+server.ts +21 -0
  167. package/src/routes/[...slug=docpage]/+page.svelte +63 -0
  168. package/src/routes/[...slug=docpage]/+page.ts +44 -0
  169. package/src/routes/layout.css +897 -0
  170. package/src/routes/llms-full.txt/+server.ts +22 -0
  171. package/src/routes/llms.txt/+server.ts +20 -0
  172. package/src/routes/og/[...slug]/+server.ts +77 -0
  173. package/src/routes/rss.xml/+server.ts +65 -0
  174. package/src/routes/search.json/+server.ts +54 -0
  175. package/src/routes/sitemap.xml/+server.ts +21 -0
  176. package/static/favicon-dark.svg +6 -0
  177. package/static/favicon-light.svg +6 -0
  178. package/static/favicon.svg +14 -0
  179. package/static/fonts/geist-400.ttf +0 -0
  180. package/static/fonts/geist-600.ttf +0 -0
  181. package/static/fonts/geist-700.ttf +0 -0
  182. package/static/og-image.png +0 -0
  183. package/static/robots.txt +4 -0
  184. package/svelte.config.js +35 -0
  185. package/tsconfig.json +20 -0
  186. package/vite.config.ts +46 -0
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation';
3
+ import { fade, scale } from 'svelte/transition';
4
+ import { create, insertMultiple, search } from '@orama/orama';
5
+ import SearchIcon from '@lucide/svelte/icons/search';
6
+ import FileText from '@lucide/svelte/icons/file-text';
7
+ import CornerDownLeft from '@lucide/svelte/icons/corner-down-left';
8
+ import { searchState } from '$lib/state/search.svelte';
9
+
10
+ interface Doc {
11
+ title: string;
12
+ section: string;
13
+ description: string;
14
+ href: string;
15
+ }
16
+
17
+ let term = $state('');
18
+ let results = $state<Doc[]>([]);
19
+ let activeIndex = $state(0);
20
+ let loaded = $state(false);
21
+ let input = $state<HTMLInputElement>();
22
+
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ const orama: { db: any } = { db: null };
25
+
26
+ const schema = {
27
+ title: 'string',
28
+ section: 'string',
29
+ description: 'string',
30
+ content: 'string',
31
+ href: 'string'
32
+ } as const;
33
+
34
+ async function ensureLoaded() {
35
+ if (orama.db) return;
36
+ const docs = await (await fetch('/search.json')).json();
37
+ orama.db = create({ schema });
38
+ await insertMultiple(orama.db, docs);
39
+ loaded = true;
40
+ }
41
+
42
+ $effect(() => {
43
+ function onKey(event: KeyboardEvent) {
44
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
45
+ event.preventDefault();
46
+ searchState.toggle();
47
+ } else if (event.key === 'Escape' && searchState.open) {
48
+ searchState.open = false;
49
+ }
50
+ }
51
+ window.addEventListener('keydown', onKey);
52
+ return () => window.removeEventListener('keydown', onKey);
53
+ });
54
+
55
+ $effect(() => {
56
+ if (searchState.open) {
57
+ ensureLoaded();
58
+ setTimeout(() => input?.focus(), 0);
59
+ } else {
60
+ term = '';
61
+ results = [];
62
+ activeIndex = 0;
63
+ }
64
+ });
65
+
66
+ $effect(() => {
67
+ const query = term.trim();
68
+ if (!query || !loaded || !orama.db) {
69
+ results = [];
70
+ return;
71
+ }
72
+ Promise.resolve(
73
+ search(orama.db, {
74
+ term: query,
75
+ properties: ['title', 'description', 'content', 'section'],
76
+ boost: { title: 4, description: 2 },
77
+ tolerance: 1,
78
+ limit: 8
79
+ })
80
+ ).then((response) => {
81
+ results = response.hits.map((hit: { document: Doc }) => hit.document);
82
+ activeIndex = 0;
83
+ });
84
+ });
85
+
86
+ function go(href: string) {
87
+ searchState.open = false;
88
+ goto(href);
89
+ }
90
+
91
+ function onInputKey(event: KeyboardEvent) {
92
+ if (event.key === 'ArrowDown') {
93
+ event.preventDefault();
94
+ activeIndex = Math.min(activeIndex + 1, results.length - 1);
95
+ } else if (event.key === 'ArrowUp') {
96
+ event.preventDefault();
97
+ activeIndex = Math.max(activeIndex - 1, 0);
98
+ } else if (event.key === 'Enter' && results[activeIndex]) {
99
+ event.preventDefault();
100
+ go(results[activeIndex].href);
101
+ }
102
+ }
103
+ </script>
104
+
105
+ {#if searchState.open}
106
+ <div class="fixed inset-0 z-60 flex items-start justify-center px-4 pt-[12vh]">
107
+ <button
108
+ type="button"
109
+ class="absolute inset-0 cursor-default bg-black/50 backdrop-blur-sm"
110
+ aria-label="Close search"
111
+ onclick={() => (searchState.open = false)}
112
+ transition:fade={{ duration: 120 }}
113
+ ></button>
114
+
115
+ <div
116
+ class="relative w-full max-w-xl overflow-hidden rounded-xl border border-border bg-surface shadow-2xl"
117
+ transition:scale={{ duration: 130, start: 0.97 }}
118
+ >
119
+ <div class="flex items-center gap-3 border-b border-border px-4">
120
+ <SearchIcon size={18} class="shrink-0 text-fg-subtle" />
121
+ <input
122
+ bind:this={input}
123
+ bind:value={term}
124
+ onkeydown={onInputKey}
125
+ type="text"
126
+ placeholder="Search documentation…"
127
+ class="w-full border-0 bg-transparent py-3.5 text-sm text-fg shadow-none outline-none placeholder:text-fg-subtle focus:border-0 focus:ring-0 focus:outline-none"
128
+ />
129
+ <kbd
130
+ class="rounded border border-border bg-bg-subtle px-1.5 py-0.5 font-mono text-[10px] text-fg-subtle"
131
+ >
132
+ Esc
133
+ </kbd>
134
+ </div>
135
+
136
+ <div class="max-h-[60vh] overflow-y-auto p-2">
137
+ {#if term.trim() && results.length === 0}
138
+ <p class="px-3 py-8 text-center text-sm text-fg-subtle">
139
+ No results for “{term.trim()}”.
140
+ </p>
141
+ {:else if !term.trim()}
142
+ <p class="px-3 py-8 text-center text-sm text-fg-subtle">
143
+ Start typing to search the docs.
144
+ </p>
145
+ {:else}
146
+ <ul class="flex flex-col gap-0.5">
147
+ {#each results as result, index (result.href)}
148
+ <li>
149
+ <button
150
+ type="button"
151
+ onmouseenter={() => (activeIndex = index)}
152
+ onclick={() => go(result.href)}
153
+ class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition
154
+ {activeIndex === index ? 'bg-accent/10' : ''}"
155
+ >
156
+ <FileText size={16} class="shrink-0 text-fg-subtle" />
157
+ <span class="min-w-0 flex-1">
158
+ <span class="block truncate text-sm font-medium text-fg">{result.title}</span>
159
+ <span class="block truncate text-xs text-fg-subtle">
160
+ {result.section}{result.description ? ` · ${result.description}` : ''}
161
+ </span>
162
+ </span>
163
+ {#if activeIndex === index}
164
+ <CornerDownLeft size={14} class="shrink-0 text-fg-subtle" />
165
+ {/if}
166
+ </button>
167
+ </li>
168
+ {/each}
169
+ </ul>
170
+ {/if}
171
+ </div>
172
+
173
+ <div
174
+ class="flex items-center gap-3 border-t border-border px-4 py-2 text-[11px] text-fg-subtle"
175
+ >
176
+ <span><kbd class="font-mono">↑↓</kbd> navigate</span>
177
+ <span><kbd class="font-mono">↵</kbd> select</span>
178
+ <span class="ml-auto">Search by Orama</span>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ {/if}
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import type { Dropdown, NavEntry, NavSection, TopNavLink } from '$lib/types';
4
+ import { activeFor } from '$lib/nav-match';
5
+ import DynamicIcon from '$lib/components/DynamicIcon.svelte';
6
+ import SidebarLink from './SidebarLink.svelte';
7
+ import SidebarGroup from './SidebarGroup.svelte';
8
+ import SidebarDropdown from './SidebarDropdown.svelte';
9
+
10
+ let { sections, dropdowns }: { sections: NavSection[]; dropdowns?: Dropdown[] } = $props();
11
+
12
+ const activeDropdown = $derived(
13
+ dropdowns && dropdowns.length
14
+ ? (activeFor(page.url.pathname, dropdowns) ?? dropdowns[0])
15
+ : undefined
16
+ );
17
+
18
+ const tabs = $derived(activeDropdown?.tabs ?? []);
19
+ const activeTab = $derived(
20
+ tabs.length ? (activeFor(page.url.pathname, tabs) ?? tabs[0]) : undefined
21
+ );
22
+
23
+ function firstHref(entries: NavEntry[]): string | undefined {
24
+ for (const entry of entries) {
25
+ const href = 'href' in entry ? entry.href : firstHref(entry.items);
26
+ if (href) return href;
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ function pruneByTab(entries: NavEntry[], tabList: TopNavLink[], target: TopNavLink): NavEntry[] {
32
+ const result: NavEntry[] = [];
33
+ for (const entry of entries) {
34
+ if ('href' in entry) {
35
+ if (activeFor(entry.href, tabList) === target) result.push(entry);
36
+ } else {
37
+ const items = pruneByTab(entry.items, tabList, target);
38
+ if (items.length) result.push({ ...entry, items });
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
44
+ const visibleSections = $derived.by(() => {
45
+ let result = sections;
46
+ if (dropdowns && activeDropdown) {
47
+ result = result.filter((section) => {
48
+ const href = firstHref(section.items);
49
+ return !href || activeFor(href, dropdowns) === activeDropdown;
50
+ });
51
+ }
52
+ if (tabs.length && activeTab) {
53
+ result = result
54
+ .map((section) => ({ ...section, items: pruneByTab(section.items, tabs, activeTab) }))
55
+ .filter((section) => section.items.length);
56
+ }
57
+ return result;
58
+ });
59
+ </script>
60
+
61
+ <nav class="flex flex-col gap-6" aria-label="Documentation">
62
+ {#if dropdowns && dropdowns.length && activeDropdown}
63
+ <SidebarDropdown {dropdowns} active={activeDropdown} />
64
+ {/if}
65
+
66
+ {#each visibleSections as section (section.title)}
67
+ <div class="flex flex-col gap-0.5">
68
+ <h2
69
+ class="mb-1 flex items-center gap-2 px-3 text-xs font-semibold tracking-wide text-fg-subtle uppercase"
70
+ >
71
+ {#if section.icon}
72
+ <DynamicIcon name={section.icon} size={14} />
73
+ {/if}
74
+ {section.title}
75
+ </h2>
76
+ {#each section.items as entry ('href' in entry ? entry.href : entry.title)}
77
+ {#if 'href' in entry}
78
+ <SidebarLink item={entry} />
79
+ {:else}
80
+ <SidebarGroup group={entry} />
81
+ {/if}
82
+ {/each}
83
+ </div>
84
+ {/each}
85
+ </nav>
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import { fade } from 'svelte/transition';
3
+ import ChevronDown from '@lucide/svelte/icons/chevron-down';
4
+ import Check from '@lucide/svelte/icons/check';
5
+ import DynamicIcon from '$lib/components/DynamicIcon.svelte';
6
+ import type { Dropdown } from '$lib/types';
7
+
8
+ let { dropdowns, active }: { dropdowns: Dropdown[]; active: Dropdown } = $props();
9
+
10
+ let open = $state(false);
11
+ </script>
12
+
13
+ <div class="relative mb-5">
14
+ <button
15
+ type="button"
16
+ onclick={() => (open = !open)}
17
+ aria-expanded={open}
18
+ class="flex w-full items-center gap-2.5 rounded-lg border border-border bg-surface px-3 py-2.5 text-sm font-medium text-fg transition hover:border-border-strong"
19
+ >
20
+ {#if active.icon}
21
+ <DynamicIcon name={active.icon} size={16} class="text-fg-muted" />
22
+ {/if}
23
+ <span class="flex-1 text-left">{active.label}</span>
24
+ <ChevronDown size={15} class="text-fg-subtle transition-transform {open ? 'rotate-180' : ''}" />
25
+ </button>
26
+
27
+ {#if open}
28
+ <button
29
+ type="button"
30
+ class="fixed inset-0 z-40 cursor-default"
31
+ aria-label="Close"
32
+ onclick={() => (open = false)}
33
+ ></button>
34
+ <div
35
+ class="absolute right-0 left-0 z-50 mt-1.5 overflow-hidden rounded-lg border border-border bg-surface-raised p-1 shadow-lg"
36
+ transition:fade={{ duration: 100 }}
37
+ >
38
+ {#each dropdowns as dropdown (dropdown.label)}
39
+ <a
40
+ href={dropdown.href}
41
+ onclick={() => (open = false)}
42
+ class="flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm transition hover:bg-bg-subtle
43
+ {dropdown === active ? 'font-medium text-fg' : 'text-fg-muted'}"
44
+ >
45
+ {#if dropdown.icon}
46
+ <DynamicIcon name={dropdown.icon} size={16} />
47
+ {/if}
48
+ <span class="flex-1">{dropdown.label}</span>
49
+ {#if dropdown === active}
50
+ <Check size={15} class="text-fg-subtle" />
51
+ {/if}
52
+ </a>
53
+ {/each}
54
+ </div>
55
+ {/if}
56
+ </div>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import ArrowUpRight from '@lucide/svelte/icons/arrow-up-right';
3
+
4
+ let { links }: { links: { title: string; href: string }[] } = $props();
5
+ </script>
6
+
7
+ <div class="flex flex-col gap-0.5 border-t border-border pt-3">
8
+ {#each links as link (link.href)}
9
+ <a
10
+ href={link.href}
11
+ target="_blank"
12
+ rel="noreferrer"
13
+ class="group flex items-center justify-between gap-2 rounded-md px-3 py-1.5 text-sm text-fg-muted transition hover:bg-bg-subtle hover:text-fg"
14
+ >
15
+ <span>{link.title}</span>
16
+ <ArrowUpRight size={14} class="text-fg-subtle transition group-hover:text-fg-muted" />
17
+ </a>
18
+ {/each}
19
+ </div>
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { slide } from 'svelte/transition';
4
+ import { page } from '$app/state';
5
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
6
+ import type { NavEntry, NavGroup } from '$lib/types';
7
+ import DynamicIcon from '$lib/components/DynamicIcon.svelte';
8
+ import SidebarLink from './SidebarLink.svelte';
9
+ import Self from './SidebarGroup.svelte';
10
+
11
+ let { group, depth = 0 }: { group: NavGroup; depth?: number } = $props();
12
+
13
+ function containsActive(items: NavEntry[], path: string): boolean {
14
+ return items.some((entry) =>
15
+ 'href' in entry ? entry.href === path : containsActive(entry.items, path)
16
+ );
17
+ }
18
+
19
+ // Open when the meta sets `defaultOpen`, or when a descendant is the current
20
+ // page (so the active page is never hidden).
21
+ let open = $state(
22
+ untrack(() => group.defaultOpen === true || containsActive(group.items, page.url.pathname))
23
+ );
24
+
25
+ const key = (entry: NavEntry) => ('href' in entry ? entry.href : entry.title);
26
+ </script>
27
+
28
+ <div>
29
+ <button
30
+ type="button"
31
+ onclick={() => (open = !open)}
32
+ aria-expanded={open}
33
+ style="padding-left: {0.75 + depth * 0.75}rem"
34
+ class="group flex w-full items-center gap-2.5 rounded-md py-1.5 pr-3 text-sm font-medium text-fg-muted transition hover:bg-bg-subtle hover:text-fg"
35
+ >
36
+ {#if group.icon}
37
+ <DynamicIcon name={group.icon} size={16} class="text-fg-subtle" />
38
+ {/if}
39
+ <span class="flex-1 text-left">{group.title}</span>
40
+ <ChevronRight size={14} class="text-fg-subtle transition-transform {open ? 'rotate-90' : ''}" />
41
+ </button>
42
+
43
+ {#if open}
44
+ <div transition:slide={{ duration: 150 }} class="mt-0.5 flex flex-col gap-0.5">
45
+ {#each group.items as entry (key(entry))}
46
+ {#if 'href' in entry}
47
+ <SidebarLink item={entry} depth={depth + 1} />
48
+ {:else}
49
+ <Self group={entry} depth={depth + 1} />
50
+ {/if}
51
+ {/each}
52
+ </div>
53
+ {/if}
54
+ </div>
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import type { NavLink } from '$lib/types';
4
+ import DynamicIcon from '$lib/components/DynamicIcon.svelte';
5
+
6
+ let { item, depth = 0 }: { item: NavLink; depth?: number } = $props();
7
+
8
+ const active = $derived(page.url.pathname === item.href);
9
+ </script>
10
+
11
+ <a
12
+ href={item.href}
13
+ aria-current={active ? 'page' : undefined}
14
+ style="padding-left: {0.75 + depth * 0.75}rem"
15
+ class="group flex items-center gap-2.5 rounded-md py-1.5 pr-3 text-sm transition
16
+ {active
17
+ ? 'bg-accent/10 font-medium text-accent'
18
+ : 'text-fg-muted hover:bg-bg-subtle hover:text-fg'}"
19
+ >
20
+ {#if item.method}
21
+ <span class="method-badge" data-method={item.method.toUpperCase()}>
22
+ {item.method.toUpperCase()}
23
+ </span>
24
+ {:else if item.icon}
25
+ <DynamicIcon
26
+ name={item.icon}
27
+ size={16}
28
+ class={active ? 'text-accent' : 'text-fg-subtle group-hover:text-fg-muted'}
29
+ />
30
+ {/if}
31
+ <span class="flex-1">{item.title}</span>
32
+ {#if item.badge}
33
+ <span
34
+ class="rounded-full bg-accent/15 px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-accent uppercase"
35
+ >
36
+ {item.badge}
37
+ </span>
38
+ {/if}
39
+ </a>
40
+
41
+ <style>
42
+ /* Color-coded HTTP method label, mirroring the <Endpoint> badge. */
43
+ .method-badge {
44
+ flex-shrink: 0;
45
+ width: 2.75rem;
46
+ font-family: var(--font-mono);
47
+ font-size: 0.6rem;
48
+ font-weight: 700;
49
+ letter-spacing: 0.02em;
50
+ color: var(--m);
51
+ }
52
+ .method-badge[data-method='GET'] {
53
+ --m: oklch(0.6 0.15 155);
54
+ }
55
+ .method-badge[data-method='POST'] {
56
+ --m: oklch(0.6 0.16 250);
57
+ }
58
+ .method-badge[data-method='PUT'] {
59
+ --m: oklch(0.68 0.15 75);
60
+ }
61
+ .method-badge[data-method='PATCH'] {
62
+ --m: oklch(0.62 0.19 300);
63
+ }
64
+ .method-badge[data-method='DELETE'] {
65
+ --m: oklch(0.62 0.21 25);
66
+ }
67
+ </style>
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { SvelteSet } from 'svelte/reactivity';
4
+ import type { TocEntry } from '$lib/types';
5
+
6
+ // Where to read headings from — the rendered markdown container.
7
+ let { containerId = 'doc-content' }: { containerId?: string } = $props();
8
+
9
+ let entries = $state<TocEntry[]>([]);
10
+ let activeId = $state<string | null>(null);
11
+
12
+ // Extract h2/h3 headings from the rendered content, re-running on navigation.
13
+ $effect(() => {
14
+ // Reading the pathname makes this effect re-run (re-extract) on navigation.
15
+ const pathname = page.url.pathname;
16
+ const container = document.getElementById(containerId);
17
+ if (!container || !pathname) {
18
+ entries = [];
19
+ return;
20
+ }
21
+ const headings = container.querySelectorAll<HTMLElement>('h2[id], h3[id]');
22
+ entries = Array.from(headings).map((h) => ({
23
+ id: h.id,
24
+ title: h.textContent?.trim() ?? '',
25
+ depth: h.tagName === 'H3' ? 3 : 2
26
+ }));
27
+ });
28
+
29
+ // Scroll-spy: highlight the heading nearest the top of the viewport.
30
+ $effect(() => {
31
+ if (entries.length === 0) return;
32
+
33
+ const elements = entries
34
+ .map((e) => document.getElementById(e.id))
35
+ .filter((el): el is HTMLElement => el !== null);
36
+
37
+ const visible = new SvelteSet<string>();
38
+ const observer = new IntersectionObserver(
39
+ (records) => {
40
+ for (const record of records) {
41
+ if (record.isIntersecting) visible.add(record.target.id);
42
+ else visible.delete(record.target.id);
43
+ }
44
+ const firstVisible = entries.find((e) => visible.has(e.id));
45
+ if (firstVisible) activeId = firstVisible.id;
46
+ },
47
+ { rootMargin: '0px 0px -70% 0px', threshold: 0 }
48
+ );
49
+
50
+ for (const el of elements) observer.observe(el);
51
+ return () => observer.disconnect();
52
+ });
53
+ </script>
54
+
55
+ {#if entries.length > 0}
56
+ <nav class="flex flex-col gap-2 text-sm" aria-label="On this page">
57
+ <p class="px-3 text-xs font-semibold tracking-wide text-fg-subtle uppercase">On this page</p>
58
+ <ul class="flex flex-col gap-0.5">
59
+ {#each entries as entry (entry.id)}
60
+ {@const active = activeId === entry.id}
61
+ <li>
62
+ <a
63
+ href="#{entry.id}"
64
+ aria-current={active ? 'location' : undefined}
65
+ class="block border-l-2 py-1 pr-3 transition
66
+ {entry.depth === 3 ? 'pl-6' : 'pl-3'}
67
+ {active
68
+ ? 'border-accent font-medium text-accent'
69
+ : 'border-transparent text-fg-subtle hover:text-fg'}"
70
+ >
71
+ {entry.title}
72
+ </a>
73
+ </li>
74
+ {/each}
75
+ </ul>
76
+ </nav>
77
+ {/if}
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import Sun from '@lucide/svelte/icons/sun';
3
+ import Moon from '@lucide/svelte/icons/moon';
4
+ import { theme } from '$lib/state/theme.svelte';
5
+ </script>
6
+
7
+ <button
8
+ type="button"
9
+ onclick={() => theme.toggle()}
10
+ class="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-border text-fg-muted transition hover:border-border-strong hover:bg-bg-subtle hover:text-fg"
11
+ aria-label="Toggle theme"
12
+ title={theme.resolved === 'dark' ? 'Switch to light' : 'Switch to dark'}
13
+ >
14
+ {#if theme.resolved === 'dark'}
15
+ <Moon size={17} />
16
+ {:else}
17
+ <Sun size={17} />
18
+ {/if}
19
+ </button>
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { base } from '$app/paths';
4
+ import { fade } from 'svelte/transition';
5
+ import ChevronDown from '@lucide/svelte/icons/chevron-down';
6
+ import Check from '@lucide/svelte/icons/check';
7
+ import type { Version } from '$lib/types';
8
+
9
+ let {
10
+ versions,
11
+ resolveVersion
12
+ }: {
13
+ versions: Version[];
14
+ resolveVersion?: (pathname: string, versionPath: string) => string;
15
+ } = $props();
16
+
17
+ let open = $state(false);
18
+
19
+ const stripBase = (path: string): string =>
20
+ base && path.startsWith(base) ? path.slice(base.length) : path;
21
+
22
+ const sorted = $derived(
23
+ [...versions].sort((a, b) => stripBase(b.href).length - stripBase(a.href).length)
24
+ );
25
+
26
+ const current = $derived(
27
+ sorted.find((version) => {
28
+ const vp = stripBase(version.href);
29
+ const path = stripBase(page.url.pathname);
30
+ return vp === '/' ? true : path === vp || path.startsWith(`${vp}/`);
31
+ }) ?? versions[0]
32
+ );
33
+
34
+ const hrefFor = (version: Version): string => {
35
+ const vp = stripBase(version.href);
36
+ if (resolveVersion) return resolveVersion(page.url.pathname, vp);
37
+ const cur = stripBase(current.href);
38
+ const rest = vp === '/' || cur === '/' ? '' : stripBase(page.url.pathname).slice(cur.length);
39
+ return base + (vp === '/' ? rest || '/' : vp + rest);
40
+ };
41
+ </script>
42
+
43
+ <div class="relative">
44
+ <button
45
+ type="button"
46
+ onclick={() => (open = !open)}
47
+ aria-expanded={open}
48
+ class="flex items-center gap-1.5 rounded-md border border-border bg-surface px-2.5 py-1.5 text-xs font-medium text-fg-muted transition hover:border-border-strong hover:text-fg"
49
+ >
50
+ {current.label}
51
+ <ChevronDown size={13} class="text-fg-subtle transition-transform {open ? 'rotate-180' : ''}" />
52
+ </button>
53
+
54
+ {#if open}
55
+ <button
56
+ type="button"
57
+ class="fixed inset-0 z-40 cursor-default"
58
+ aria-label="Close versions"
59
+ onclick={() => (open = false)}
60
+ ></button>
61
+ <div
62
+ class="absolute left-0 z-50 mt-1.5 w-36 overflow-hidden rounded-lg border border-border bg-surface-raised p-1 shadow-lg"
63
+ transition:fade={{ duration: 100 }}
64
+ >
65
+ {#each versions as version (version.label)}
66
+ <a
67
+ href={hrefFor(version)}
68
+ onclick={() => (open = false)}
69
+ class="flex items-center justify-between rounded-md px-2.5 py-1.5 text-sm transition hover:bg-bg-subtle
70
+ {version === current ? 'text-fg' : 'text-fg-muted'}"
71
+ >
72
+ {version.label}
73
+ {#if version === current}
74
+ <Check size={14} class="text-fg-subtle" />
75
+ {/if}
76
+ </a>
77
+ {/each}
78
+ </div>
79
+ {/if}
80
+ </div>