svelte-docsmith 0.6.0 → 0.8.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.
- package/README.md +6 -2
- package/dist/buildtime/vite/collect.js +31 -3
- package/dist/components/chrome/announcement-bar.svelte +188 -0
- package/dist/components/chrome/announcement-bar.svelte.d.ts +7 -0
- package/dist/components/docs/tabs-sync.svelte.d.ts +10 -0
- package/dist/components/docs/tabs-sync.svelte.js +59 -0
- package/dist/components/docs/tabs.svelte +42 -6
- package/dist/components/docs/tabs.svelte.d.ts +6 -0
- package/dist/components/layouts/docs-shell.svelte +14 -8
- package/dist/components/layouts/docs-sidebar.svelte +52 -18
- package/dist/components/layouts/docs-sidebar.svelte.d.ts +1 -1
- package/dist/core/config.d.ts +21 -0
- package/dist/core/config.js +19 -0
- package/dist/core/content.d.ts +6 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/nav.d.ts +23 -6
- package/dist/core/nav.js +94 -15
- package/dist/theme.css +57 -57
- package/dist/themes/darkmatter.css +6 -4
- package/dist/themes/tangerine.css +3 -6
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/svelte-docsmith)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="https://raw.githubusercontent.com/geodask/svelte-docsmith/master/.github/assets/hero.png" alt="Svelte DocSmith: craft documentation worthy of legend" width="100%" />
|
|
8
|
+
</p>
|
|
9
|
+
|
|
6
10
|
The documentation framework for Svelte 5 library authors whose interactive
|
|
7
11
|
examples need to live inside one real, stateful SvelteKit app, not sandboxed as
|
|
8
12
|
isolated islands.
|
|
@@ -169,8 +173,8 @@ plugin you already added:
|
|
|
169
173
|
|
|
170
174
|
## Themes
|
|
171
175
|
|
|
172
|
-
Eleven presets ship in the box.
|
|
173
|
-
|
|
176
|
+
Eleven presets ship in the box. **Darkmatter** is the default, applied by
|
|
177
|
+
`theme.css`; import any other preset after it to switch:
|
|
174
178
|
|
|
175
179
|
```css
|
|
176
180
|
@import 'svelte-docsmith/theme.css';
|
|
@@ -4,6 +4,34 @@ import { listPageFiles } from './pages.js';
|
|
|
4
4
|
import { parseFrontmatter } from './frontmatter.js';
|
|
5
5
|
import { extractLlmsContent, extractSearchText, extractToc, readingMinutes } from './extract.js';
|
|
6
6
|
import { lastCommitDate } from './git.js';
|
|
7
|
+
/**
|
|
8
|
+
* Read a page's `section` frontmatter: a string is one level, an array is a
|
|
9
|
+
* nested group path. Non-string array members are dropped; anything else is
|
|
10
|
+
* treated as no section.
|
|
11
|
+
*/
|
|
12
|
+
function readSection(value) {
|
|
13
|
+
if (typeof value === 'string')
|
|
14
|
+
return value;
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
const segs = value.filter((s) => typeof s === 'string');
|
|
17
|
+
return segs.length ? segs : undefined;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/** The most-specific (last) group segment, for the compact search-result pill. */
|
|
22
|
+
function sectionLabel(value) {
|
|
23
|
+
const section = readSection(value);
|
|
24
|
+
if (section === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
return Array.isArray(section) ? section[section.length - 1] : section;
|
|
27
|
+
}
|
|
28
|
+
/** The full group path joined, so nested pages group under one llms.txt heading. */
|
|
29
|
+
function sectionKey(value) {
|
|
30
|
+
const section = readSection(value);
|
|
31
|
+
if (section === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
return Array.isArray(section) ? section.join(' / ') : section;
|
|
34
|
+
}
|
|
7
35
|
/**
|
|
8
36
|
* Walk every nav-worthy page under `contentDir` once: a page is nav-worthy when
|
|
9
37
|
* its frontmatter has a string `title`. Yields the raw source, parsed
|
|
@@ -39,7 +67,7 @@ export function collectDocs(contentDir, routesDir) {
|
|
|
39
67
|
title,
|
|
40
68
|
path: url,
|
|
41
69
|
description: typeof front.description === 'string' ? front.description : undefined,
|
|
42
|
-
section:
|
|
70
|
+
section: readSection(front.section),
|
|
43
71
|
order: typeof front.order === 'number' ? front.order : undefined,
|
|
44
72
|
sourcePath: path.relative(process.cwd(), file).split(path.sep).join('/'),
|
|
45
73
|
lastUpdated: lastCommitDate(file),
|
|
@@ -69,7 +97,7 @@ export function collectSearchDocs(contentDir, routesDir) {
|
|
|
69
97
|
docs.push({
|
|
70
98
|
path: url,
|
|
71
99
|
title,
|
|
72
|
-
section:
|
|
100
|
+
section: sectionLabel(front.section),
|
|
73
101
|
description: typeof front.description === 'string' ? front.description : undefined,
|
|
74
102
|
headings: extractToc(source).map((entry) => entry.title),
|
|
75
103
|
text: extractSearchText(source)
|
|
@@ -91,7 +119,7 @@ export function collectLlmsDocs(contentDir, routesDir) {
|
|
|
91
119
|
docs.push({
|
|
92
120
|
path: url,
|
|
93
121
|
title,
|
|
94
|
-
section:
|
|
122
|
+
section: sectionKey(front.section),
|
|
95
123
|
order: typeof front.order === 'number' ? front.order : undefined,
|
|
96
124
|
description: typeof front.description === 'string' ? front.description : undefined,
|
|
97
125
|
content: extractLlmsContent(source, title)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { BROWSER } from 'esm-env';
|
|
3
|
+
import X from '@lucide/svelte/icons/x';
|
|
4
|
+
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
|
5
|
+
import type { DocsmithAnnouncement } from '../../core/index.js';
|
|
6
|
+
|
|
7
|
+
const { announcement }: { announcement: DocsmithAnnouncement } = $props();
|
|
8
|
+
|
|
9
|
+
const dismissible = $derived(announcement.dismissible ?? true);
|
|
10
|
+
const storageKey = $derived('docsmith-announcement:' + (announcement.id ?? announcement.text));
|
|
11
|
+
|
|
12
|
+
// Rendered visible on the server; the dismissal is read after mount so the
|
|
13
|
+
// first client render matches the server and there's no hydration mismatch.
|
|
14
|
+
// The blocking <head> script below is what actually prevents the flash on a
|
|
15
|
+
// reload — this effect just removes the (already-hidden) element from the DOM.
|
|
16
|
+
let dismissed = $state(false);
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (!dismissible) return;
|
|
19
|
+
try {
|
|
20
|
+
dismissed = localStorage.getItem(storageKey) === '1';
|
|
21
|
+
} catch {
|
|
22
|
+
// storage unavailable; treat as not dismissed
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function dismiss() {
|
|
27
|
+
dismissed = true;
|
|
28
|
+
if (BROWSER) {
|
|
29
|
+
try {
|
|
30
|
+
localStorage.setItem(storageKey, '1');
|
|
31
|
+
} catch {
|
|
32
|
+
// storage unavailable; dismissal holds for this session only
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A blocking snippet run in <head> before first paint: if this bar was
|
|
38
|
+
// already dismissed, hide it via an injected style so it is never painted
|
|
39
|
+
// and can't shift the layout when hydration removes it. Targets a stable
|
|
40
|
+
// class (not the scoped one) so it also works in dev, where component CSS is
|
|
41
|
+
// injected after paint. localStorage is read here because it isn't available
|
|
42
|
+
// during SSR.
|
|
43
|
+
const preventFlashScript = $derived(
|
|
44
|
+
`(function(){try{if(localStorage.getItem(${JSON.stringify(storageKey)})==="1"){` +
|
|
45
|
+
`var s=document.createElement("style");` +
|
|
46
|
+
`s.textContent=".docsmith-announcement{display:none!important}";` +
|
|
47
|
+
`document.head.appendChild(s)}}catch(e){}})()`
|
|
48
|
+
);
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<svelte:head>
|
|
52
|
+
{#if dismissible}
|
|
53
|
+
<!-- The `<\/script>` escape keeps the Svelte parser from ending the tag early. -->
|
|
54
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags, no-useless-escape -->
|
|
55
|
+
{@html `<script>${preventFlashScript}<\/script>`}
|
|
56
|
+
{/if}
|
|
57
|
+
</svelte:head>
|
|
58
|
+
|
|
59
|
+
{#if !dismissed}
|
|
60
|
+
<div class="announcement docsmith-announcement" role="region" aria-label="Announcement">
|
|
61
|
+
{#if announcement.href}
|
|
62
|
+
<a
|
|
63
|
+
class="announcement-body announcement-link"
|
|
64
|
+
href={announcement.href}
|
|
65
|
+
target={announcement.external ? '_blank' : undefined}
|
|
66
|
+
rel={announcement.external ? 'noopener noreferrer' : undefined}
|
|
67
|
+
>
|
|
68
|
+
{#if announcement.tag}<span class="announcement-tag">{announcement.tag}</span>{/if}
|
|
69
|
+
<span class="announcement-text">{announcement.text}</span>
|
|
70
|
+
<ArrowRight class="announcement-arrow size-3.5" aria-hidden="true" />
|
|
71
|
+
</a>
|
|
72
|
+
{:else}
|
|
73
|
+
<div class="announcement-body">
|
|
74
|
+
{#if announcement.tag}<span class="announcement-tag">{announcement.tag}</span>{/if}
|
|
75
|
+
<span class="announcement-text">{announcement.text}</span>
|
|
76
|
+
</div>
|
|
77
|
+
{/if}
|
|
78
|
+
|
|
79
|
+
{#if dismissible}
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
class="announcement-dismiss"
|
|
83
|
+
onclick={dismiss}
|
|
84
|
+
aria-label="Dismiss announcement"
|
|
85
|
+
>
|
|
86
|
+
<X class="size-3.5" aria-hidden="true" />
|
|
87
|
+
</button>
|
|
88
|
+
{/if}
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
/* A thin bar above the header. The warm accent is spent on a solid tag, not
|
|
94
|
+
a full-width wash, so it reads as a deliberate mark; text stays on
|
|
95
|
+
--foreground for AA contrast. */
|
|
96
|
+
.announcement {
|
|
97
|
+
position: relative;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
min-height: 2.5rem;
|
|
102
|
+
padding: 0.4rem 2.75rem;
|
|
103
|
+
background: color-mix(in oklch, var(--primary) 6%, var(--background));
|
|
104
|
+
border-bottom: 1px solid color-mix(in oklch, var(--primary) 20%, var(--border));
|
|
105
|
+
color: var(--foreground);
|
|
106
|
+
font-size: 0.8125rem;
|
|
107
|
+
line-height: 1.4;
|
|
108
|
+
text-align: center;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.announcement-body {
|
|
112
|
+
display: inline-flex;
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
gap: 0.3rem 0.55rem;
|
|
117
|
+
min-width: 0;
|
|
118
|
+
color: var(--foreground);
|
|
119
|
+
text-decoration: none;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Solid brand pill — the sanctioned primary-foreground-on-primary pairing, so
|
|
123
|
+
it stays legible in both themes and matches the props-table "required" pill. */
|
|
124
|
+
.announcement-tag {
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
background: var(--primary);
|
|
127
|
+
color: var(--primary-foreground);
|
|
128
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
129
|
+
font-size: 0.625rem;
|
|
130
|
+
font-weight: 700;
|
|
131
|
+
letter-spacing: 0.04em;
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
padding: 0.15rem 0.5rem;
|
|
134
|
+
border-radius: 9999px;
|
|
135
|
+
line-height: 1.5;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.announcement-text {
|
|
139
|
+
color: var(--foreground);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.announcement-link :global(.announcement-arrow) {
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
color: var(--primary);
|
|
145
|
+
transition: transform 0.2s ease-out;
|
|
146
|
+
}
|
|
147
|
+
.announcement-link:hover :global(.announcement-arrow) {
|
|
148
|
+
transform: translateX(2px);
|
|
149
|
+
}
|
|
150
|
+
@media (prefers-reduced-motion: reduce) {
|
|
151
|
+
.announcement-link :global(.announcement-arrow) {
|
|
152
|
+
transition: none;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Dismiss sits at the trailing edge, inside the bar's padding. */
|
|
157
|
+
.announcement-dismiss {
|
|
158
|
+
position: absolute;
|
|
159
|
+
top: 50%;
|
|
160
|
+
right: 0.5rem;
|
|
161
|
+
transform: translateY(-50%);
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
padding: 0.35rem;
|
|
166
|
+
border: 0;
|
|
167
|
+
background: transparent;
|
|
168
|
+
border-radius: 0.375rem;
|
|
169
|
+
color: var(--muted-foreground);
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
transition:
|
|
172
|
+
color 0.15s ease-out,
|
|
173
|
+
background-color 0.15s ease-out;
|
|
174
|
+
}
|
|
175
|
+
.announcement-dismiss:hover {
|
|
176
|
+
color: var(--foreground);
|
|
177
|
+
background: color-mix(in oklch, var(--foreground) 8%, transparent);
|
|
178
|
+
}
|
|
179
|
+
.announcement-dismiss:focus-visible {
|
|
180
|
+
outline: 2px solid var(--primary);
|
|
181
|
+
outline-offset: 1px;
|
|
182
|
+
}
|
|
183
|
+
@media (prefers-reduced-motion: reduce) {
|
|
184
|
+
.announcement-dismiss {
|
|
185
|
+
transition: none;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DocsmithAnnouncement } from '../../core/index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
announcement: DocsmithAnnouncement;
|
|
4
|
+
};
|
|
5
|
+
declare const AnnouncementBar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type AnnouncementBar = ReturnType<typeof AnnouncementBar>;
|
|
7
|
+
export default AnnouncementBar;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The current selection for a sync key, or `undefined` if none is stored. */
|
|
2
|
+
export declare function syncedValue(key: string): string | undefined;
|
|
3
|
+
/** Record a selection for a sync key and persist it. Client-only. */
|
|
4
|
+
export declare function setSyncedValue(key: string, value: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Load a key's persisted selection into the reactive store, once, on the
|
|
7
|
+
* client. Called from an effect (after hydration) so the first client render
|
|
8
|
+
* still matches the server's default and there is no hydration mismatch.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hydrateSyncedValue(key: string): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BROWSER } from 'esm-env';
|
|
2
|
+
// Cross-instance tab sync. Every <Tabs syncKey="x"> reads and writes the same
|
|
3
|
+
// entry here, so choosing "pnpm" in one code block selects it in every block
|
|
4
|
+
// with that key, and the choice survives reloads and navigation.
|
|
5
|
+
//
|
|
6
|
+
// The store is a module singleton. It is only ever written on the client (all
|
|
7
|
+
// writers below are BROWSER-guarded), so on the server it stays empty and each
|
|
8
|
+
// request falls back to the default tab — no cross-request state leakage.
|
|
9
|
+
const PREFIX = 'docsmith-tabs:';
|
|
10
|
+
const selections = $state({});
|
|
11
|
+
let listening = false;
|
|
12
|
+
function listenForOtherWindows() {
|
|
13
|
+
if (listening || !BROWSER)
|
|
14
|
+
return;
|
|
15
|
+
listening = true;
|
|
16
|
+
// Mirror choices made in other tabs/windows of the same site.
|
|
17
|
+
window.addEventListener('storage', (event) => {
|
|
18
|
+
if (event.key?.startsWith(PREFIX) && event.newValue !== null) {
|
|
19
|
+
selections[event.key.slice(PREFIX.length)] = event.newValue;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** The current selection for a sync key, or `undefined` if none is stored. */
|
|
24
|
+
export function syncedValue(key) {
|
|
25
|
+
return selections[key];
|
|
26
|
+
}
|
|
27
|
+
/** Record a selection for a sync key and persist it. Client-only. */
|
|
28
|
+
export function setSyncedValue(key, value) {
|
|
29
|
+
if (!BROWSER || selections[key] === value)
|
|
30
|
+
return;
|
|
31
|
+
selections[key] = value;
|
|
32
|
+
try {
|
|
33
|
+
localStorage.setItem(PREFIX + key, value);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Storage can be unavailable (private mode, quota); sync stays in-memory
|
|
37
|
+
// for this session rather than throwing.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Load a key's persisted selection into the reactive store, once, on the
|
|
42
|
+
* client. Called from an effect (after hydration) so the first client render
|
|
43
|
+
* still matches the server's default and there is no hydration mismatch.
|
|
44
|
+
*/
|
|
45
|
+
export function hydrateSyncedValue(key) {
|
|
46
|
+
if (!BROWSER)
|
|
47
|
+
return;
|
|
48
|
+
listenForOtherWindows();
|
|
49
|
+
if (key in selections)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
const stored = localStorage.getItem(PREFIX + key);
|
|
53
|
+
if (stored !== null)
|
|
54
|
+
selections[key] = stored;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// ignore unreadable storage
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -22,29 +22,65 @@
|
|
|
22
22
|
import { setContext, type Snippet } from 'svelte';
|
|
23
23
|
import * as TabsPrimitive from '../shadcn/tabs/index.js';
|
|
24
24
|
import TabsPhaseScope from './tabs-phase.svelte';
|
|
25
|
+
import { syncedValue, setSyncedValue, hydrateSyncedValue } from './tabs-sync.svelte.js';
|
|
25
26
|
|
|
26
27
|
const {
|
|
27
28
|
value,
|
|
29
|
+
syncKey,
|
|
28
30
|
children
|
|
29
31
|
}: {
|
|
30
32
|
/** Value/label of the tab selected by default. Defaults to the first tab. */
|
|
31
33
|
value?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Sync group. Every `<Tabs>` with the same `syncKey` shares its selection
|
|
36
|
+
* (e.g. `"pkg"` for npm/pnpm/yarn blocks), and the choice is remembered
|
|
37
|
+
* across reloads and navigation.
|
|
38
|
+
*/
|
|
39
|
+
syncKey?: string;
|
|
32
40
|
children: Snippet;
|
|
33
41
|
} = $props();
|
|
34
42
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
43
|
+
// Each <TabItem> registers its value during the collect pass, which renders
|
|
44
|
+
// (below) before <Tabs.Root> reads them — so the right panel is already
|
|
45
|
+
// selected in the server-rendered HTML. The first registered value is the
|
|
46
|
+
// default; the full list validates a restored sync choice against this block.
|
|
47
|
+
let values = $state<string[]>([]);
|
|
39
48
|
setContext<TabsContext>(TABS_CTX, {
|
|
40
|
-
register: (v) =>
|
|
49
|
+
register: (v) => {
|
|
50
|
+
if (!values.includes(v)) values.push(v);
|
|
51
|
+
}
|
|
41
52
|
});
|
|
53
|
+
const firstValue = $derived<string | undefined>(values[0]);
|
|
54
|
+
|
|
55
|
+
// Restore a persisted selection after mount (client-only), so it does not
|
|
56
|
+
// diverge from the server's default render until hydration is done. `ready`
|
|
57
|
+
// gates persistence until then, so any value event the tab primitive fires
|
|
58
|
+
// while mounting can't overwrite the reader's stored choice before we read it.
|
|
59
|
+
let ready = $state(false);
|
|
60
|
+
$effect(() => {
|
|
61
|
+
if (syncKey) hydrateSyncedValue(syncKey);
|
|
62
|
+
ready = true;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// A restored sync choice (valid for this block) wins, then the explicit
|
|
66
|
+
// `value`, then the first tab. Reactive, so a sibling group's change or a
|
|
67
|
+
// storage restore switches this instance too.
|
|
68
|
+
const selected = $derived.by(() => {
|
|
69
|
+
const fallback = value ?? firstValue ?? '';
|
|
70
|
+
if (!syncKey) return fallback;
|
|
71
|
+
const stored = syncedValue(syncKey);
|
|
72
|
+
return stored && values.includes(stored) ? stored : fallback;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function handleValueChange(next: string) {
|
|
76
|
+
if (ready && syncKey && next) setSyncedValue(syncKey, next);
|
|
77
|
+
}
|
|
42
78
|
</script>
|
|
43
79
|
|
|
44
80
|
<!-- collect: registers each TabItem's value; renders nothing. -->
|
|
45
81
|
<TabsPhaseScope phase="collect">{@render children()}</TabsPhaseScope>
|
|
46
82
|
|
|
47
|
-
<TabsPrimitive.Root value={
|
|
83
|
+
<TabsPrimitive.Root value={selected} onValueChange={handleValueChange} class="my-6">
|
|
48
84
|
<TabsPrimitive.List class="not-prose mb-3">
|
|
49
85
|
<TabsPhaseScope phase="list">{@render children()}</TabsPhaseScope>
|
|
50
86
|
</TabsPrimitive.List>
|
|
@@ -11,6 +11,12 @@ import { type Snippet } from 'svelte';
|
|
|
11
11
|
type $$ComponentProps = {
|
|
12
12
|
/** Value/label of the tab selected by default. Defaults to the first tab. */
|
|
13
13
|
value?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Sync group. Every `<Tabs>` with the same `syncKey` shares its selection
|
|
16
|
+
* (e.g. `"pkg"` for npm/pnpm/yarn blocks), and the choice is remembered
|
|
17
|
+
* across reloads and navigation.
|
|
18
|
+
*/
|
|
19
|
+
syncKey?: string;
|
|
14
20
|
children: Snippet;
|
|
15
21
|
};
|
|
16
22
|
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { afterNavigate } from '$app/navigation';
|
|
4
4
|
import {
|
|
5
5
|
navFromContent,
|
|
6
|
+
flattenNav,
|
|
7
|
+
navTrail,
|
|
6
8
|
type DocsContentItem,
|
|
7
9
|
type DocsmithConfig,
|
|
8
10
|
type SearchDoc
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
import Search from '../chrome/search.svelte';
|
|
14
16
|
import BackgroundPattern from '../chrome/background-pattern.svelte';
|
|
15
17
|
import ThemeProvider from '../chrome/theme-provider.svelte';
|
|
18
|
+
import AnnouncementBar from '../chrome/announcement-bar.svelte';
|
|
16
19
|
import DocsHeader from './docs-header.svelte';
|
|
17
20
|
import DocsFooter from './docs-footer.svelte';
|
|
18
21
|
import DocsMobileHeader from './docs-mobile-header.svelte';
|
|
@@ -128,8 +131,8 @@
|
|
|
128
131
|
readingTime && currentEntry?.readingTime ? `${currentEntry.readingTime} min read` : undefined
|
|
129
132
|
);
|
|
130
133
|
|
|
131
|
-
// Ordered flat page list drives
|
|
132
|
-
const flatNav = $derived(nav
|
|
134
|
+
// Ordered flat page list (leaves in sidebar reading order) drives prev/next.
|
|
135
|
+
const flatNav = $derived(flattenNav(nav));
|
|
133
136
|
const pageIndex = $derived(flatNav.findIndex((item) => item.url === pathname));
|
|
134
137
|
const prev = $derived(pageIndex > 0 ? flatNav[pageIndex - 1] : undefined);
|
|
135
138
|
const next = $derived(
|
|
@@ -137,13 +140,10 @@
|
|
|
137
140
|
);
|
|
138
141
|
const currentTitle = $derived(pageIndex >= 0 ? flatNav[pageIndex].title : config.title);
|
|
139
142
|
|
|
140
|
-
// Breadcrumb trail: the current page's sidebar group, then the page
|
|
141
|
-
|
|
142
|
-
nav.find((group) => group.items.some((item) => item.url === pathname))
|
|
143
|
-
);
|
|
143
|
+
// Breadcrumb trail: the current page's sidebar group path, then the page
|
|
144
|
+
// itself (so a nested page reads e.g. Guides > Advanced > Middleware).
|
|
144
145
|
const breadcrumbs = $derived.by(() => {
|
|
145
|
-
const crumbs: Crumb[] = [];
|
|
146
|
-
if (currentGroup) crumbs.push({ title: currentGroup.title });
|
|
146
|
+
const crumbs: Crumb[] = (navTrail(nav, pathname) ?? []).map((title) => ({ title }));
|
|
147
147
|
if (pageIndex >= 0) crumbs.push({ title: currentTitle });
|
|
148
148
|
return crumbs;
|
|
149
149
|
});
|
|
@@ -189,6 +189,12 @@
|
|
|
189
189
|
<BackgroundPattern />
|
|
190
190
|
{/if}
|
|
191
191
|
|
|
192
|
+
<!-- Announcement bar (if configured) sits above the sticky header, so it
|
|
193
|
+
scrolls away while the header stays pinned. -->
|
|
194
|
+
{#if config.announcement}
|
|
195
|
+
<AnnouncementBar announcement={config.announcement} />
|
|
196
|
+
{/if}
|
|
197
|
+
|
|
192
198
|
<!-- One header system everywhere: DocsHeader on desktop, DocsMobileHeader
|
|
193
199
|
below lg. The `page` layout just omits the sidebar nav and in-page TOC. -->
|
|
194
200
|
<DocsHeader {config} {logo} {actions} />
|
|
@@ -1,35 +1,69 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { page } from '$app/state';
|
|
3
|
+
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
3
4
|
import { cn } from '../../utils/cn.js';
|
|
4
5
|
import { normalizePath } from '../../utils/normalize-path.js';
|
|
5
|
-
import type
|
|
6
|
+
import { isNavGroup, type NavGroup, type NavNode } from '../../core/index.js';
|
|
6
7
|
|
|
7
8
|
const { nav, class: className = '' }: { nav: NavGroup[]; class?: string } = $props();
|
|
9
|
+
|
|
10
|
+
const current = $derived(normalizePath(page.url.pathname));
|
|
11
|
+
|
|
12
|
+
// Whether a subtree contains the current page — drives which nested groups
|
|
13
|
+
// start open. Recomputes on navigation, so navigating into a collapsed branch
|
|
14
|
+
// (e.g. via prev/next) opens it, while manual toggles on other branches stick.
|
|
15
|
+
function containsCurrent(node: NavNode): boolean {
|
|
16
|
+
return isNavGroup(node) ? node.items.some(containsCurrent) : node.url === current;
|
|
17
|
+
}
|
|
8
18
|
</script>
|
|
9
19
|
|
|
20
|
+
{#snippet tree(items: NavNode[])}
|
|
21
|
+
<ul class="space-y-1">
|
|
22
|
+
{#each items as node (isNavGroup(node) ? 'g:' + node.title : node.url)}
|
|
23
|
+
{#if isNavGroup(node)}
|
|
24
|
+
<li>
|
|
25
|
+
<details class="group/nav" open={containsCurrent(node)}>
|
|
26
|
+
<summary
|
|
27
|
+
class="text-muted-foreground hover:text-foreground flex cursor-pointer list-none items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium transition-colors select-none [&::-webkit-details-marker]:hidden"
|
|
28
|
+
>
|
|
29
|
+
<span class="truncate">{node.title}</span>
|
|
30
|
+
<ChevronRight
|
|
31
|
+
class="ml-auto size-3.5 shrink-0 opacity-70 transition-transform duration-200 ease-out group-open/nav:rotate-90 motion-reduce:transition-none"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
/>
|
|
34
|
+
</summary>
|
|
35
|
+
<div class="border-border/60 mt-1 ml-2 border-l pl-3">
|
|
36
|
+
{@render tree(node.items)}
|
|
37
|
+
</div>
|
|
38
|
+
</details>
|
|
39
|
+
</li>
|
|
40
|
+
{:else}
|
|
41
|
+
<li>
|
|
42
|
+
<a
|
|
43
|
+
href={node.url}
|
|
44
|
+
aria-current={current === node.url ? 'page' : undefined}
|
|
45
|
+
class={cn(
|
|
46
|
+
'hover:text-primary hover:bg-primary/20 block rounded-md px-2 py-1.5 text-sm transition-colors',
|
|
47
|
+
current === node.url
|
|
48
|
+
? 'text-primary bg-primary/20 font-medium'
|
|
49
|
+
: 'text-muted-foreground'
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{node.title}
|
|
53
|
+
</a>
|
|
54
|
+
</li>
|
|
55
|
+
{/if}
|
|
56
|
+
{/each}
|
|
57
|
+
</ul>
|
|
58
|
+
{/snippet}
|
|
59
|
+
|
|
10
60
|
<aside class={cn('sticky top-24 hidden h-screen w-56 shrink-0 bg-transparent lg:block', className)}>
|
|
11
61
|
<div class="max-h-[calc(100vh-10rem)] overflow-y-auto px-4">
|
|
12
62
|
<nav aria-label="Documentation" class="space-y-6">
|
|
13
63
|
{#each nav as group (group.title)}
|
|
14
64
|
<div class="space-y-3">
|
|
15
65
|
<h4 class="text-foreground px-2 text-sm font-semibold">{group.title}</h4>
|
|
16
|
-
|
|
17
|
-
{#each group.items as item (item.url)}
|
|
18
|
-
<li>
|
|
19
|
-
<a
|
|
20
|
-
href={item.url}
|
|
21
|
-
class={cn(
|
|
22
|
-
'hover:text-primary hover:bg-primary/20 block rounded-md px-2 py-1.5 text-sm transition-colors',
|
|
23
|
-
normalizePath(page.url.pathname) === item.url
|
|
24
|
-
? 'text-primary bg-primary/20 font-medium'
|
|
25
|
-
: 'text-muted-foreground'
|
|
26
|
-
)}
|
|
27
|
-
>
|
|
28
|
-
{item.title}
|
|
29
|
-
</a>
|
|
30
|
-
</li>
|
|
31
|
-
{/each}
|
|
32
|
-
</ul>
|
|
66
|
+
{@render tree(group.items)}
|
|
33
67
|
</div>
|
|
34
68
|
{/each}
|
|
35
69
|
</nav>
|
package/dist/core/config.d.ts
CHANGED
|
@@ -14,6 +14,25 @@ export type DocsmithFooterColumn = {
|
|
|
14
14
|
title: string;
|
|
15
15
|
links: DocsmithLink[];
|
|
16
16
|
};
|
|
17
|
+
/** A thin announcement bar shown at the top of every page. */
|
|
18
|
+
export type DocsmithAnnouncement = {
|
|
19
|
+
/** The message text. */
|
|
20
|
+
text: string;
|
|
21
|
+
/** Optional leading pill label, e.g. `'New'`, `'Beta'`, or a version. */
|
|
22
|
+
tag?: string;
|
|
23
|
+
/** Optional link the bar points to. */
|
|
24
|
+
href?: string;
|
|
25
|
+
/** Open the link in a new tab with `rel="noopener"`. */
|
|
26
|
+
external?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Dismissal key. A dismissed bar stays dismissed until this value changes, so
|
|
29
|
+
* bump it (or edit `text`) when you post a new announcement. Defaults to the
|
|
30
|
+
* text itself.
|
|
31
|
+
*/
|
|
32
|
+
id?: string;
|
|
33
|
+
/** Let readers dismiss the bar. Defaults to `true`. */
|
|
34
|
+
dismissible?: boolean;
|
|
35
|
+
};
|
|
17
36
|
export type DocsmithConfig = {
|
|
18
37
|
/** Site title, shown in the header/sidebar and in the `<title>` suffix. */
|
|
19
38
|
title: string;
|
|
@@ -42,6 +61,8 @@ export type DocsmithConfig = {
|
|
|
42
61
|
version?: string;
|
|
43
62
|
/** Optional logo image src; falls back to the built-in book mark. */
|
|
44
63
|
logo?: string;
|
|
64
|
+
/** Optional announcement bar shown at the top of every page. */
|
|
65
|
+
announcement?: DocsmithAnnouncement;
|
|
45
66
|
/** Top-level header navigation links. */
|
|
46
67
|
nav?: DocsmithLink[];
|
|
47
68
|
/** Footer content, driven by data. */
|
package/dist/core/config.js
CHANGED
|
@@ -23,6 +23,25 @@ export function defineConfig(config) {
|
|
|
23
23
|
throw new Error(`[svelte-docsmith] config.${key} must be a string when set.`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
if (config.announcement !== undefined) {
|
|
27
|
+
const a = config.announcement;
|
|
28
|
+
if (typeof a !== 'object' || a === null) {
|
|
29
|
+
throw new Error('[svelte-docsmith] config.announcement must be an object ({ text, href?, id?, external?, dismissible? }).');
|
|
30
|
+
}
|
|
31
|
+
if (typeof a.text !== 'string' || a.text.trim() === '') {
|
|
32
|
+
throw new Error('[svelte-docsmith] config.announcement.text is required and must be a string.');
|
|
33
|
+
}
|
|
34
|
+
for (const key of ['tag', 'href', 'id']) {
|
|
35
|
+
if (a[key] !== undefined && typeof a[key] !== 'string') {
|
|
36
|
+
throw new Error(`[svelte-docsmith] config.announcement.${key} must be a string when set.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const key of ['external', 'dismissible']) {
|
|
40
|
+
if (a[key] !== undefined && typeof a[key] !== 'boolean') {
|
|
41
|
+
throw new Error(`[svelte-docsmith] config.announcement.${key} must be a boolean when set.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
if (config.nav !== undefined) {
|
|
27
46
|
if (!Array.isArray(config.nav)) {
|
|
28
47
|
throw new Error('[svelte-docsmith] config.nav must be an array of { label, href } links.');
|
package/dist/core/content.d.ts
CHANGED
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
export type DocsContentItem = {
|
|
14
14
|
title: string;
|
|
15
15
|
path: string;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Sidebar group. A string places the page in a single top-level group; an
|
|
18
|
+
* array names a nested group path (e.g. `['Guides', 'Advanced']`), building
|
|
19
|
+
* collapsible subsections. Omitted pages fall under "Docs".
|
|
20
|
+
*/
|
|
21
|
+
section?: string | string[];
|
|
17
22
|
order?: number;
|
|
18
23
|
description?: string;
|
|
19
24
|
/** Source file path relative to the app cwd, for the "Edit this page" link. */
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFooterColumn } from './config.js';
|
|
1
|
+
export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFooterColumn, type DocsmithAnnouncement } from './config.js';
|
|
2
2
|
export type { DocsContentItem, SearchDoc, LlmsDoc } from './content.js';
|
|
3
|
-
export { navFromContent, type NavItem, type NavGroup } from './nav.js';
|
|
3
|
+
export { navFromContent, flattenNav, navTrail, isNavGroup, type NavItem, type NavGroup, type NavNode } from './nav.js';
|
package/dist/core/index.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// several of these (e.g. the shell) import from here; the public surface is
|
|
4
4
|
// re-exported through `src/lib/index.ts`.
|
|
5
5
|
export { defineConfig } from './config.js';
|
|
6
|
-
export { navFromContent } from './nav.js';
|
|
6
|
+
export { navFromContent, flattenNav, navTrail, isNavGroup } from './nav.js';
|
package/dist/core/nav.d.ts
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import type { DocsContentItem } from './content.js';
|
|
2
|
-
/** A single sidebar link. */
|
|
2
|
+
/** A single sidebar link (a leaf). */
|
|
3
3
|
export type NavItem = {
|
|
4
4
|
title: string;
|
|
5
5
|
url: string;
|
|
6
6
|
};
|
|
7
|
-
/** A titled group of sidebar
|
|
7
|
+
/** A titled group of sidebar entries, which may themselves be nested groups. */
|
|
8
8
|
export type NavGroup = {
|
|
9
9
|
title: string;
|
|
10
|
-
items:
|
|
10
|
+
items: NavNode[];
|
|
11
11
|
};
|
|
12
|
+
/** A sidebar entry: either a link or a nested group. */
|
|
13
|
+
export type NavNode = NavItem | NavGroup;
|
|
14
|
+
/** Narrow a {@link NavNode} to a {@link NavGroup} (an entry with children). */
|
|
15
|
+
export declare function isNavGroup(node: NavNode): node is NavGroup;
|
|
12
16
|
/**
|
|
13
|
-
* Derive sidebar nav from a content collection
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* Derive sidebar nav from a content collection. Each page's `section` (a string
|
|
18
|
+
* or a nested path) places it in the tree; within every level, entries order by
|
|
19
|
+
* `order` (then title), and a group inherits the smallest `order` of its
|
|
20
|
+
* descendants so groups sort by their earliest child. Top-level entries are
|
|
21
|
+
* always groups; pages without a section fall under "Docs".
|
|
16
22
|
*/
|
|
17
23
|
export declare function navFromContent(content: DocsContentItem[]): NavGroup[];
|
|
24
|
+
/**
|
|
25
|
+
* Flatten the nav tree to its leaf links in sidebar reading order (depth-first),
|
|
26
|
+
* for the prev/next pager.
|
|
27
|
+
*/
|
|
28
|
+
export declare function flattenNav(nodes: NavNode[]): NavItem[];
|
|
29
|
+
/**
|
|
30
|
+
* The trail of group titles leading to the page at `url` (top group first,
|
|
31
|
+
* excluding the page itself), or `undefined` if the page is not in the tree.
|
|
32
|
+
* Drives the breadcrumb trail.
|
|
33
|
+
*/
|
|
34
|
+
export declare function navTrail(nodes: NavNode[], url: string): string[] | undefined;
|
package/dist/core/nav.js
CHANGED
|
@@ -1,22 +1,101 @@
|
|
|
1
|
+
/** Narrow a {@link NavNode} to a {@link NavGroup} (an entry with children). */
|
|
2
|
+
export function isNavGroup(node) {
|
|
3
|
+
return 'items' in node;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a page's `section` to a group path. A string is a single level; an
|
|
7
|
+
* array is a nested path; blank or missing sections fall under "Docs". Empty
|
|
8
|
+
* strings inside an array are dropped so a stray `['Guides', '']` stays sane.
|
|
9
|
+
*/
|
|
10
|
+
function sectionPath(section) {
|
|
11
|
+
const segs = Array.isArray(section) ? section : section ? [section] : [];
|
|
12
|
+
const clean = segs.filter((s) => typeof s === 'string' && s.trim() !== '');
|
|
13
|
+
return clean.length ? clean : ['Docs'];
|
|
14
|
+
}
|
|
15
|
+
function makeBuilder(title) {
|
|
16
|
+
return { title, order: Infinity, groups: new Map(), leaves: [] };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Emit a group's children (nested groups and leaf links) as one list, sorted by
|
|
20
|
+
* `order` and then title. The title tiebreak keeps the output deterministic when
|
|
21
|
+
* pages share an order (e.g. all default to 0), instead of depending on the
|
|
22
|
+
* filesystem scan order.
|
|
23
|
+
*/
|
|
24
|
+
function emitItems(node) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
for (const leaf of node.leaves) {
|
|
27
|
+
entries.push({
|
|
28
|
+
order: leaf.order,
|
|
29
|
+
title: leaf.title,
|
|
30
|
+
node: { title: leaf.title, url: leaf.url }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
for (const group of node.groups.values()) {
|
|
34
|
+
entries.push({
|
|
35
|
+
order: group.order,
|
|
36
|
+
title: group.title,
|
|
37
|
+
node: { title: group.title, items: emitItems(group) }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
entries.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
41
|
+
return entries.map((e) => e.node);
|
|
42
|
+
}
|
|
1
43
|
/**
|
|
2
|
-
* Derive sidebar nav from a content collection
|
|
3
|
-
*
|
|
4
|
-
*
|
|
44
|
+
* Derive sidebar nav from a content collection. Each page's `section` (a string
|
|
45
|
+
* or a nested path) places it in the tree; within every level, entries order by
|
|
46
|
+
* `order` (then title), and a group inherits the smallest `order` of its
|
|
47
|
+
* descendants so groups sort by their earliest child. Top-level entries are
|
|
48
|
+
* always groups; pages without a section fall under "Docs".
|
|
5
49
|
*/
|
|
6
50
|
export function navFromContent(content) {
|
|
7
|
-
const
|
|
51
|
+
const root = makeBuilder('');
|
|
8
52
|
for (const item of content) {
|
|
9
|
-
const section = item.section ?? 'Docs';
|
|
10
53
|
const order = item.order ?? 0;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
54
|
+
let node = root;
|
|
55
|
+
for (const seg of sectionPath(item.section)) {
|
|
56
|
+
let child = node.groups.get(seg);
|
|
57
|
+
if (!child) {
|
|
58
|
+
child = makeBuilder(seg);
|
|
59
|
+
node.groups.set(seg, child);
|
|
60
|
+
}
|
|
61
|
+
child.order = Math.min(child.order, order);
|
|
62
|
+
node = child;
|
|
63
|
+
}
|
|
64
|
+
node.leaves.push({ title: item.title, url: item.path, order });
|
|
65
|
+
}
|
|
66
|
+
// The root only ever holds groups (every page has at least one path segment),
|
|
67
|
+
// so its emitted items are all NavGroups.
|
|
68
|
+
return emitItems(root);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Flatten the nav tree to its leaf links in sidebar reading order (depth-first),
|
|
72
|
+
* for the prev/next pager.
|
|
73
|
+
*/
|
|
74
|
+
export function flattenNav(nodes) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (isNavGroup(node))
|
|
78
|
+
out.push(...flattenNav(node.items));
|
|
79
|
+
else
|
|
80
|
+
out.push(node);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The trail of group titles leading to the page at `url` (top group first,
|
|
86
|
+
* excluding the page itself), or `undefined` if the page is not in the tree.
|
|
87
|
+
* Drives the breadcrumb trail.
|
|
88
|
+
*/
|
|
89
|
+
export function navTrail(nodes, url) {
|
|
90
|
+
for (const node of nodes) {
|
|
91
|
+
if (isNavGroup(node)) {
|
|
92
|
+
const inner = navTrail(node.items, url);
|
|
93
|
+
if (inner)
|
|
94
|
+
return [node.title, ...inner];
|
|
95
|
+
}
|
|
96
|
+
else if (node.url === url) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
15
99
|
}
|
|
16
|
-
return
|
|
17
|
-
.sort((a, b) => a[1].minOrder - b[1].minOrder)
|
|
18
|
-
.map(([title, group]) => ({
|
|
19
|
-
title,
|
|
20
|
-
items: group.items.sort((a, b) => a.order - b.order).map(({ title, url }) => ({ title, url }))
|
|
21
|
-
}));
|
|
100
|
+
return undefined;
|
|
22
101
|
}
|
package/dist/theme.css
CHANGED
|
@@ -107,38 +107,38 @@
|
|
|
107
107
|
|
|
108
108
|
@layer base {
|
|
109
109
|
:root {
|
|
110
|
-
--background: oklch(0
|
|
111
|
-
--foreground: oklch(0.
|
|
110
|
+
--background: oklch(1 0 0);
|
|
111
|
+
--foreground: oklch(0.2101 0.0318 264.6645);
|
|
112
112
|
--card: oklch(1 0 0);
|
|
113
|
-
--card-foreground: oklch(0.
|
|
113
|
+
--card-foreground: oklch(0.2101 0.0318 264.6645);
|
|
114
114
|
--popover: oklch(1 0 0);
|
|
115
|
-
--popover-foreground: oklch(0.
|
|
116
|
-
--primary: oklch(0.
|
|
115
|
+
--popover-foreground: oklch(0.2101 0.0318 264.6645);
|
|
116
|
+
--primary: oklch(0.6716 0.1368 48.513);
|
|
117
117
|
--primary-foreground: oklch(1 0 0);
|
|
118
|
-
--secondary: oklch(0.
|
|
119
|
-
--secondary-foreground: oklch(0
|
|
120
|
-
--muted: oklch(0.
|
|
118
|
+
--secondary: oklch(0.536 0.0398 196.028);
|
|
119
|
+
--secondary-foreground: oklch(1 0 0);
|
|
120
|
+
--muted: oklch(0.967 0.0029 264.5419);
|
|
121
121
|
--muted-foreground: oklch(0.551 0.0234 264.3637);
|
|
122
|
-
--accent: oklch(0.
|
|
123
|
-
--accent-foreground: oklch(0.
|
|
122
|
+
--accent: oklch(0.9491 0 0);
|
|
123
|
+
--accent-foreground: oklch(0.2101 0.0318 264.6645);
|
|
124
124
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
|
125
|
-
--destructive-foreground: oklch(
|
|
126
|
-
--border: oklch(0.
|
|
127
|
-
--input: oklch(0.
|
|
128
|
-
--ring: oklch(0.
|
|
129
|
-
--chart-1: oklch(0.
|
|
130
|
-
--chart-2: oklch(0.
|
|
131
|
-
--chart-3: oklch(0.
|
|
132
|
-
--chart-4: oklch(0.
|
|
133
|
-
--chart-5: oklch(0.
|
|
134
|
-
--sidebar: oklch(0.
|
|
135
|
-
--sidebar-foreground: oklch(0.
|
|
136
|
-
--sidebar-primary: oklch(0.
|
|
125
|
+
--destructive-foreground: oklch(0.9851 0 0);
|
|
126
|
+
--border: oklch(0.9276 0.0058 264.5313);
|
|
127
|
+
--input: oklch(0.9276 0.0058 264.5313);
|
|
128
|
+
--ring: oklch(0.6716 0.1368 48.513);
|
|
129
|
+
--chart-1: oklch(0.594 0.0443 196.0233);
|
|
130
|
+
--chart-2: oklch(0.7214 0.1337 49.9802);
|
|
131
|
+
--chart-3: oklch(0.8721 0.0864 68.5474);
|
|
132
|
+
--chart-4: oklch(0.6268 0 0);
|
|
133
|
+
--chart-5: oklch(0.683 0 0);
|
|
134
|
+
--sidebar: oklch(0.967 0.0029 264.5419);
|
|
135
|
+
--sidebar-foreground: oklch(0.2101 0.0318 264.6645);
|
|
136
|
+
--sidebar-primary: oklch(0.6716 0.1368 48.513);
|
|
137
137
|
--sidebar-primary-foreground: oklch(1 0 0);
|
|
138
|
-
--sidebar-accent: oklch(0
|
|
139
|
-
--sidebar-accent-foreground: oklch(0.
|
|
138
|
+
--sidebar-accent: oklch(1 0 0);
|
|
139
|
+
--sidebar-accent-foreground: oklch(0.2101 0.0318 264.6645);
|
|
140
140
|
--sidebar-border: oklch(0.9276 0.0058 264.5313);
|
|
141
|
-
--sidebar-ring: oklch(0.
|
|
141
|
+
--sidebar-ring: oklch(0.6716 0.1368 48.513);
|
|
142
142
|
--font-sans: Inter, sans-serif;
|
|
143
143
|
--font-serif: Source Serif 4, serif;
|
|
144
144
|
--font-mono: JetBrains Mono, monospace;
|
|
@@ -165,38 +165,38 @@
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
.dark {
|
|
168
|
-
--background: oklch(0.
|
|
169
|
-
--foreground: oklch(0.
|
|
170
|
-
--card: oklch(0.
|
|
171
|
-
--card-foreground: oklch(0.
|
|
172
|
-
--popover: oklch(0.
|
|
173
|
-
--popover-foreground: oklch(0.
|
|
174
|
-
--primary: oklch(0.
|
|
175
|
-
--primary-foreground: oklch(
|
|
176
|
-
--secondary: oklch(0.
|
|
177
|
-
--secondary-foreground: oklch(0.
|
|
178
|
-
--muted: oklch(0.
|
|
179
|
-
--muted-foreground: oklch(0.
|
|
180
|
-
--accent: oklch(0.
|
|
181
|
-
--accent-foreground: oklch(0.
|
|
182
|
-
--destructive: oklch(0.
|
|
183
|
-
--destructive-foreground: oklch(
|
|
184
|
-
--border: oklch(0.
|
|
185
|
-
--input: oklch(0.
|
|
186
|
-
--ring: oklch(0.
|
|
187
|
-
--chart-1: oklch(0.
|
|
188
|
-
--chart-2: oklch(0.
|
|
189
|
-
--chart-3: oklch(0.
|
|
190
|
-
--chart-4: oklch(0.
|
|
191
|
-
--chart-5: oklch(0.
|
|
192
|
-
--sidebar: oklch(0.
|
|
193
|
-
--sidebar-foreground: oklch(0.
|
|
194
|
-
--sidebar-primary: oklch(0.
|
|
195
|
-
--sidebar-primary-foreground: oklch(
|
|
196
|
-
--sidebar-accent: oklch(0.
|
|
197
|
-
--sidebar-accent-foreground: oklch(0.
|
|
198
|
-
--sidebar-border: oklch(0.
|
|
199
|
-
--sidebar-ring: oklch(0.
|
|
168
|
+
--background: oklch(0.1797 0.0043 308.1928);
|
|
169
|
+
--foreground: oklch(0.8109 0 0);
|
|
170
|
+
--card: oklch(0.1822 0 0);
|
|
171
|
+
--card-foreground: oklch(0.8109 0 0);
|
|
172
|
+
--popover: oklch(0.1797 0.0043 308.1928);
|
|
173
|
+
--popover-foreground: oklch(0.8109 0 0);
|
|
174
|
+
--primary: oklch(0.7214 0.1337 49.9802);
|
|
175
|
+
--primary-foreground: oklch(0.1797 0.0043 308.1928);
|
|
176
|
+
--secondary: oklch(0.594 0.0443 196.0233);
|
|
177
|
+
--secondary-foreground: oklch(0.1797 0.0043 308.1928);
|
|
178
|
+
--muted: oklch(0.252 0 0);
|
|
179
|
+
--muted-foreground: oklch(0.6268 0 0);
|
|
180
|
+
--accent: oklch(0.3211 0 0);
|
|
181
|
+
--accent-foreground: oklch(0.8109 0 0);
|
|
182
|
+
--destructive: oklch(0.7106 0.1661 22.2162);
|
|
183
|
+
--destructive-foreground: oklch(0.1797 0.0043 308.1928);
|
|
184
|
+
--border: oklch(0.252 0 0);
|
|
185
|
+
--input: oklch(0.252 0 0);
|
|
186
|
+
--ring: oklch(0.7214 0.1337 49.9802);
|
|
187
|
+
--chart-1: oklch(0.594 0.0443 196.0233);
|
|
188
|
+
--chart-2: oklch(0.7214 0.1337 49.9802);
|
|
189
|
+
--chart-3: oklch(0.8721 0.0864 68.5474);
|
|
190
|
+
--chart-4: oklch(0.6268 0 0);
|
|
191
|
+
--chart-5: oklch(0.683 0 0);
|
|
192
|
+
--sidebar: oklch(0.1822 0 0);
|
|
193
|
+
--sidebar-foreground: oklch(0.8109 0 0);
|
|
194
|
+
--sidebar-primary: oklch(0.7214 0.1337 49.9802);
|
|
195
|
+
--sidebar-primary-foreground: oklch(0.1797 0.0043 308.1928);
|
|
196
|
+
--sidebar-accent: oklch(0.3211 0 0);
|
|
197
|
+
--sidebar-accent-foreground: oklch(0.8109 0 0);
|
|
198
|
+
--sidebar-border: oklch(0.252 0 0);
|
|
199
|
+
--sidebar-ring: oklch(0.7214 0.1337 49.9802);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
* {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* svelte-docsmith theme — Darkmatter (based on the tweakcn
|
|
2
|
+
* svelte-docsmith theme — Darkmatter. The built-in default (based on the tweakcn
|
|
3
|
+
* preset): theme.css already ships these tokens, so you only import this file to
|
|
4
|
+
* return to Darkmatter after choosing another preset.
|
|
3
5
|
*
|
|
4
|
-
* A near-monochrome shell with a warm orange primary and a muted teal
|
|
5
|
-
*
|
|
6
|
+
* A near-monochrome shell with a warm orange primary and a muted teal secondary;
|
|
7
|
+
* light mode is crisp white, dark mode is deep near-black.
|
|
6
8
|
*
|
|
7
9
|
* @import 'svelte-docsmith/theme.css';
|
|
8
10
|
* @import 'svelte-docsmith/themes/darkmatter.css';
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
--border: oklch(0.252 0 0);
|
|
64
66
|
--input: oklch(0.252 0 0);
|
|
65
67
|
--ring: oklch(0.7214 0.1337 49.9802);
|
|
66
|
-
--destructive: oklch(0.
|
|
68
|
+
--destructive: oklch(0.7106 0.1661 22.2162);
|
|
67
69
|
--destructive-foreground: oklch(0.1797 0.0043 308.1928);
|
|
68
70
|
--chart-1: oklch(0.594 0.0443 196.0233);
|
|
69
71
|
--chart-2: oklch(0.7214 0.1337 49.9802);
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* svelte-docsmith theme — Tangerine
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* so you can re-apply the default explicitly or use it as a starting point:
|
|
2
|
+
* svelte-docsmith theme — Tangerine. A warm terracotta primary on calm,
|
|
3
|
+
* faintly-cool neutrals. Import it after theme.css to swap the default Darkmatter
|
|
4
|
+
* tokens for this palette:
|
|
6
5
|
*
|
|
7
6
|
* @import 'svelte-docsmith/theme.css';
|
|
8
7
|
* @import 'svelte-docsmith/themes/tangerine.css';
|
|
9
|
-
*
|
|
10
|
-
* Warm terracotta primary on calm, faintly-cool neutrals.
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
10
|
:root {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-docsmith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A framework for building beautiful documentation sites with Svelte.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "George Daskalakis",
|
|
@@ -104,9 +104,17 @@
|
|
|
104
104
|
},
|
|
105
105
|
"keywords": [
|
|
106
106
|
"svelte",
|
|
107
|
+
"sveltekit",
|
|
108
|
+
"svelte5",
|
|
109
|
+
"docs",
|
|
110
|
+
"documentation",
|
|
111
|
+
"documentation-generator",
|
|
112
|
+
"docs-site",
|
|
113
|
+
"markdown",
|
|
114
|
+
"mdsvex",
|
|
115
|
+
"shiki",
|
|
107
116
|
"ui",
|
|
108
|
-
"
|
|
109
|
-
"docs"
|
|
117
|
+
"components"
|
|
110
118
|
],
|
|
111
119
|
"scripts": {
|
|
112
120
|
"dev": "vite dev",
|