specra 0.2.2 → 0.2.4
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/config/specra.config.schema.json +9 -14
- package/dist/components/docs/Callout.svelte +5 -4
- package/dist/components/docs/CodeBlock.svelte +2 -2
- package/dist/components/docs/DocLayout.svelte +1 -1
- package/dist/components/docs/Header.svelte +73 -3
- package/dist/components/docs/Header.svelte.d.ts +11 -0
- package/dist/components/docs/MdxLayout.svelte +1 -1
- package/dist/components/docs/MobileDocLayout.svelte +3 -15
- package/dist/components/docs/Sidebar.svelte +4 -11
- package/dist/components/docs/SidebarMenuItems.svelte +1 -3
- package/dist/components/docs/Step.svelte +2 -2
- package/dist/components/docs/TabGroups.svelte +2 -2
- package/dist/components/docs/TableOfContents.svelte +3 -10
- package/dist/components/docs/Timeline.svelte +15 -0
- package/dist/components/docs/Timeline.svelte.d.ts +7 -0
- package/dist/components/docs/TimelineItem.svelte +50 -0
- package/dist/components/docs/TimelineItem.svelte.d.ts +10 -0
- package/dist/components/docs/VersionBanner.svelte +60 -0
- package/dist/components/docs/VersionBanner.svelte.d.ts +10 -0
- package/dist/components/docs/VersionSwitcher.svelte +40 -13
- package/dist/components/docs/VersionSwitcher.svelte.d.ts +7 -0
- package/dist/components/docs/componentTextProps.js +2 -0
- package/dist/components/docs/index.d.ts +3 -0
- package/dist/components/docs/index.js +3 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.server.d.ts +28 -1
- package/dist/config.server.js +66 -0
- package/dist/config.types.d.ts +25 -0
- package/dist/mdx-components.js +4 -2
- package/dist/mdx.js +325 -42
- package/dist/styles/globals.css +71 -0
- package/package.json +1 -1
|
@@ -45,31 +45,26 @@
|
|
|
45
45
|
},
|
|
46
46
|
"BannerConfig": {
|
|
47
47
|
"additionalProperties": false,
|
|
48
|
-
"description": "Site-wide banner configuration",
|
|
48
|
+
"description": "Banner configuration for version-level or site-level banners. Site-wide banner configuration",
|
|
49
49
|
"properties": {
|
|
50
|
-
"
|
|
51
|
-
"description": "
|
|
52
|
-
"type": "boolean"
|
|
53
|
-
},
|
|
54
|
-
"enabled": {
|
|
55
|
-
"description": "Whether the banner is enabled",
|
|
56
|
-
"type": "boolean"
|
|
57
|
-
},
|
|
58
|
-
"message": {
|
|
59
|
-
"description": "Banner message",
|
|
50
|
+
"text": {
|
|
51
|
+
"description": "Banner message text. Supports markdown links like [text](/url).",
|
|
60
52
|
"type": "string"
|
|
61
53
|
},
|
|
62
54
|
"type": {
|
|
63
|
-
"description": "Banner type",
|
|
55
|
+
"description": "Banner style: info, warning, error, success Banner type",
|
|
64
56
|
"enum": [
|
|
65
57
|
"info",
|
|
66
58
|
"warning",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
59
|
+
"error",
|
|
60
|
+
"success"
|
|
69
61
|
],
|
|
70
62
|
"type": "string"
|
|
71
63
|
}
|
|
72
64
|
},
|
|
65
|
+
"required": [
|
|
66
|
+
"text"
|
|
67
|
+
],
|
|
73
68
|
"type": "object"
|
|
74
69
|
},
|
|
75
70
|
"DeploymentConfig": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import {
|
|
3
3
|
Info,
|
|
4
|
+
PenLine,
|
|
4
5
|
AlertTriangle,
|
|
5
6
|
CheckCircle2,
|
|
6
7
|
XCircle,
|
|
@@ -33,10 +34,10 @@
|
|
|
33
34
|
defaultTitle: 'Info',
|
|
34
35
|
},
|
|
35
36
|
note: {
|
|
36
|
-
icon:
|
|
37
|
-
className: 'bg-
|
|
38
|
-
iconClassName: 'text-
|
|
39
|
-
titleClassName: 'text-
|
|
37
|
+
icon: PenLine,
|
|
38
|
+
className: 'bg-slate-500/10 border-slate-500/30 text-slate-900 dark:bg-slate-400/5 dark:border-slate-400/20 dark:text-slate-300',
|
|
39
|
+
iconClassName: 'text-slate-600 dark:text-slate-400',
|
|
40
|
+
titleClassName: 'text-slate-700 dark:text-slate-300',
|
|
40
41
|
defaultTitle: 'Note',
|
|
41
42
|
},
|
|
42
43
|
warning: {
|
|
@@ -114,8 +114,8 @@
|
|
|
114
114
|
</div>
|
|
115
115
|
|
|
116
116
|
<!-- Code content -->
|
|
117
|
-
<div class="bg-
|
|
118
|
-
<pre class="p-2 text-[13px] font-mono leading-relaxed text-gray-800 dark:text-gray-200"><code class="table w-full">{#each lines as line, i}{@const isDeletion = line.startsWith('-')}{@const isAddition = line.startsWith('+')}{@const isDiff = isDeletion || isAddition}{@const diffBgClass = isDeletion ? 'bg-red-500/5 dark:bg-red-500/10' : isAddition ? 'bg-green-500/5 dark:bg-green-500/10' : ''}{@const diffMarkerClass = isDeletion ? 'text-red-600 dark:text-red-400' : isAddition ? 'text-green-600 dark:text-green-400' : ''}{@const tokens = tokenizeLine(line)}<div class="table-row {diffBgClass}"><span class="table-cell pr-4 text-right select-none text-muted-foreground/40 w-8 align-top">{i + 1}</span><span class="table-cell align-top">{#if tokens.length === 0} {:else}{#each tokens as token, j}{#if j === 0 && isDiff && token.value.length > 0 && (token.value[0] === '+' || token.value[0] === '-')}<span class="{diffMarkerClass} font-bold">{token.value[0]}</span>{#if token.value.length > 1}<span class="token-{token.type}">{token.value.slice(1)}</span>{/if}{:else}<span class="token-{token.type}">{token.value}</span>{/if}{/each}{/if}</span></div>{/each}</code></pre>
|
|
117
|
+
<div class="bg-muted/30 dark:bg-muted/20 rounded-b-xl overflow-x-auto border border-border/50">
|
|
118
|
+
<pre class="p-2 text-[13px] font-mono leading-relaxed text-gray-800 dark:text-gray-200"><code class="table w-full">{#each lines as line, i}{@const isDeletion = language === 'diff' && line.startsWith('-') && !line.startsWith('--')}{@const isAddition = language === 'diff' && line.startsWith('+') && !line.startsWith('++')}{@const isDiff = isDeletion || isAddition}{@const diffBgClass = isDeletion ? 'bg-red-500/5 dark:bg-red-500/10' : isAddition ? 'bg-green-500/5 dark:bg-green-500/10' : ''}{@const diffMarkerClass = isDeletion ? 'text-red-600 dark:text-red-400' : isAddition ? 'text-green-600 dark:text-green-400' : ''}{@const tokens = tokenizeLine(line)}<div class="table-row {diffBgClass}"><span class="table-cell pr-4 text-right select-none text-muted-foreground/40 w-8 align-top">{i + 1}</span><span class="table-cell align-top">{#if tokens.length === 0} {:else}{#each tokens as token, j}{#if j === 0 && isDiff && token.value.length > 0 && (token.value[0] === '+' || token.value[0] === '-')}<span class="{diffMarkerClass} font-bold">{token.value[0]}</span>{#if token.value.length > 1}<span class="token-{token.type}">{token.value.slice(1)}</span>{/if}{:else}<span class="token-{token.type}">{token.value}</span>{/if}{/each}{/if}</span></div>{/each}</code></pre>
|
|
119
119
|
</div>
|
|
120
120
|
</div>
|
|
121
121
|
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
<DocMetadata {meta} {config} />
|
|
68
68
|
|
|
69
|
-
<div class="prose prose-slate dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3 prose-p:text-base prose-p:leading-7 prose-p:text-muted-foreground prose-p:mb-4 prose-a:font-normal prose-a:transition-all prose-code:text-primary prose-code:bg-muted/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-[13px] prose-code:font-mono prose-code:border prose-code:border-border/50 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-transparent prose-pre:p-0 prose-ul:list-disc prose-ul:
|
|
69
|
+
<div class="prose prose-slate dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3 prose-p:text-base prose-p:leading-7 prose-p:text-muted-foreground prose-p:mb-4 prose-a:font-normal prose-a:transition-all prose-code:text-primary prose-code:bg-muted/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-[13px] prose-code:font-mono prose-code:border prose-code:border-border/50 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-transparent prose-pre:p-0 prose-ul:list-disc prose-ul:space-y-2 prose-ul:mb-4 prose-ol:list-decimal prose-ol:space-y-2 prose-ol:mb-4 prose-li:leading-7 prose-li:text-muted-foreground [&_ul_ul]:ps-4 [&_ul_ul]:mt-2 [&_ul_ul]:mb-0 [&_ol_ol]:ps-4 [&_ol_ol]:mt-2 [&_ol_ol]:mb-0 [&_ol_ul]:ps-4 [&_ol_ul]:mt-2 [&_ol_ul]:mb-0 [&_ul_ol]:ps-4 [&_ul_ol]:mt-2 [&_ul_ol]:mb-0 prose-strong:text-foreground prose-strong:font-semibold">
|
|
70
70
|
{@render children()}
|
|
71
71
|
</div>
|
|
72
72
|
|
|
@@ -3,23 +3,63 @@
|
|
|
3
3
|
import { getConfigContext } from '../../stores/config.js';
|
|
4
4
|
import { sidebarStore } from '../../stores/sidebar.js';
|
|
5
5
|
import VersionSwitcher from './VersionSwitcher.svelte';
|
|
6
|
+
import VersionBanner from './VersionBanner.svelte';
|
|
6
7
|
import ThemeToggle from './ThemeToggle.svelte';
|
|
7
8
|
import SearchModal from './SearchModal.svelte';
|
|
8
9
|
import Logo from './Logo.svelte';
|
|
10
|
+
import Icon from './Icon.svelte';
|
|
9
11
|
import type { SpecraConfig } from '../../config.types.js';
|
|
12
|
+
import type { BannerConfig } from '../../config.types.js';
|
|
13
|
+
import type { Snippet } from 'svelte';
|
|
14
|
+
import { browser } from '$app/environment';
|
|
15
|
+
|
|
16
|
+
interface VersionMeta {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
badge?: string;
|
|
20
|
+
hidden?: boolean;
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
interface Props {
|
|
12
24
|
currentVersion: string;
|
|
13
25
|
versions: string[];
|
|
26
|
+
versionsMeta?: VersionMeta[];
|
|
27
|
+
versionBanner?: BannerConfig;
|
|
14
28
|
config?: SpecraConfig;
|
|
29
|
+
subheader?: Snippet;
|
|
15
30
|
}
|
|
16
31
|
|
|
17
|
-
let { currentVersion, versions, config: configProp }: Props = $props();
|
|
32
|
+
let { currentVersion, versions, versionsMeta, versionBanner, config: configProp, subheader }: Props = $props();
|
|
18
33
|
|
|
19
34
|
const configStore = getConfigContext();
|
|
20
35
|
let config = $derived(configProp || $configStore);
|
|
21
36
|
let searchOpen = $state(false);
|
|
22
37
|
let isFlush = $derived(config?.navigation?.sidebarStyle === 'flush');
|
|
38
|
+
let headerEl = $state<HTMLElement | null>(null);
|
|
39
|
+
|
|
40
|
+
// Set a CSS variable on :root with the header's actual height so sidebars
|
|
41
|
+
// can use it for their sticky top offset. Uses ResizeObserver to stay
|
|
42
|
+
// in sync when banner is dismissed or tabs change.
|
|
43
|
+
$effect(() => {
|
|
44
|
+
if (!browser || !headerEl) return;
|
|
45
|
+
|
|
46
|
+
function updateHeight() {
|
|
47
|
+
if (headerEl) {
|
|
48
|
+
const height = headerEl.offsetHeight;
|
|
49
|
+
document.documentElement.style.setProperty('--header-height', `${height}px`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Set immediately to avoid flash of wrong position
|
|
54
|
+
updateHeight();
|
|
55
|
+
// Also set on next frame in case layout hasn't settled
|
|
56
|
+
requestAnimationFrame(updateHeight);
|
|
57
|
+
|
|
58
|
+
const observer = new ResizeObserver(updateHeight);
|
|
59
|
+
observer.observe(headerEl);
|
|
60
|
+
|
|
61
|
+
return () => observer.disconnect();
|
|
62
|
+
});
|
|
23
63
|
|
|
24
64
|
$effect(() => {
|
|
25
65
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
@@ -33,7 +73,7 @@
|
|
|
33
73
|
});
|
|
34
74
|
</script>
|
|
35
75
|
|
|
36
|
-
<header class="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
|
76
|
+
<header bind:this={headerEl} class="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
|
37
77
|
<div class="{isFlush ? '' : 'container mx-auto'} flex h-16 items-center justify-between px-4 md:px-6">
|
|
38
78
|
<div class="flex items-center gap-1">
|
|
39
79
|
<button
|
|
@@ -76,7 +116,7 @@
|
|
|
76
116
|
{/if}
|
|
77
117
|
|
|
78
118
|
{#if config.features?.versioning}
|
|
79
|
-
<VersionSwitcher {currentVersion} {versions} />
|
|
119
|
+
<VersionSwitcher {currentVersion} {versions} {versionsMeta} />
|
|
80
120
|
{/if}
|
|
81
121
|
|
|
82
122
|
<!-- Social Links -->
|
|
@@ -113,6 +153,28 @@
|
|
|
113
153
|
<MessageCircle class="h-4 w-4" />
|
|
114
154
|
</a>
|
|
115
155
|
{/if}
|
|
156
|
+
{#if config.social?.custom}
|
|
157
|
+
{#each config.social.custom as link}
|
|
158
|
+
<span class="relative hidden md:inline-flex group/tip">
|
|
159
|
+
<a
|
|
160
|
+
href={link.url}
|
|
161
|
+
target="_blank"
|
|
162
|
+
rel="noopener noreferrer"
|
|
163
|
+
class="flex items-center justify-center h-9 w-9 rounded-md hover:bg-muted transition-colors"
|
|
164
|
+
aria-label={link.label}
|
|
165
|
+
>
|
|
166
|
+
{#if link.icon}
|
|
167
|
+
<Icon icon={link.icon} size={16} />
|
|
168
|
+
{:else}
|
|
169
|
+
<span class="text-xs font-medium">{link.label.slice(0, 2)}</span>
|
|
170
|
+
{/if}
|
|
171
|
+
</a>
|
|
172
|
+
<span class="absolute top-full left-1/2 -translate-x-1/2 mt-1 px-2 py-1 text-xs text-popover-foreground bg-popover border border-border rounded-md shadow-sm whitespace-nowrap pointer-events-none opacity-0 group-hover/tip:opacity-100 transition-opacity z-50">
|
|
173
|
+
{link.label}
|
|
174
|
+
</span>
|
|
175
|
+
</span>
|
|
176
|
+
{/each}
|
|
177
|
+
{/if}
|
|
116
178
|
|
|
117
179
|
<ThemeToggle />
|
|
118
180
|
</div>
|
|
@@ -120,4 +182,12 @@
|
|
|
120
182
|
|
|
121
183
|
<!-- Search Modal -->
|
|
122
184
|
<SearchModal isOpen={searchOpen} onClose={() => (searchOpen = false)} {config} />
|
|
185
|
+
|
|
186
|
+
{#if versionBanner}
|
|
187
|
+
<VersionBanner banner={versionBanner} />
|
|
188
|
+
{/if}
|
|
189
|
+
|
|
190
|
+
{#if subheader}
|
|
191
|
+
{@render subheader()}
|
|
192
|
+
{/if}
|
|
123
193
|
</header>
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { SpecraConfig } from '../../config.types.js';
|
|
2
|
+
import type { BannerConfig } from '../../config.types.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
interface VersionMeta {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
badge?: string;
|
|
8
|
+
hidden?: boolean;
|
|
9
|
+
}
|
|
2
10
|
interface Props {
|
|
3
11
|
currentVersion: string;
|
|
4
12
|
versions: string[];
|
|
13
|
+
versionsMeta?: VersionMeta[];
|
|
14
|
+
versionBanner?: BannerConfig;
|
|
5
15
|
config?: SpecraConfig;
|
|
16
|
+
subheader?: Snippet;
|
|
6
17
|
}
|
|
7
18
|
declare const Header: import("svelte").Component<Props, {}, "">;
|
|
8
19
|
type Header = ReturnType<typeof Header>;
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
let { children }: { children?: import('svelte').Snippet } = $props()
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
|
-
<div class="prose prose-slate dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3 prose-p:text-base prose-p:leading-7 prose-p:text-muted-foreground prose-p:mb-4 prose-a:font-normal prose-a:transition-all prose-code:text-primary prose-code:bg-muted/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-[13px] prose-code:font-mono prose-code:border prose-code:border-border/50 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-transparent prose-pre:p-0 prose-ul:list-disc prose-ul:
|
|
14
|
+
<div class="prose prose-slate dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3 prose-p:text-base prose-p:leading-7 prose-p:text-muted-foreground prose-p:mb-4 prose-a:font-normal prose-a:transition-all prose-code:text-primary prose-code:bg-muted/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-[13px] prose-code:font-mono prose-code:border prose-code:border-border/50 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-transparent prose-pre:p-0 prose-ul:list-disc prose-ul:space-y-2 prose-ul:mb-4 prose-ol:list-decimal prose-ol:space-y-2 prose-ol:mb-4 prose-li:leading-7 prose-li:text-muted-foreground [&_ul_ul]:ps-4 [&_ul_ul]:mt-2 [&_ul_ul]:mb-0 [&_ol_ol]:ps-4 [&_ol_ol]:mt-2 [&_ol_ol]:mb-0 [&_ol_ul]:ps-4 [&_ol_ul]:mt-2 [&_ol_ul]:mb-0 [&_ul_ol]:ps-4 [&_ul_ol]:mt-2 [&_ul_ol]:mb-0 prose-strong:text-foreground prose-strong:font-semibold">
|
|
15
15
|
{@render children?.()}
|
|
16
16
|
</div>
|
|
@@ -56,24 +56,12 @@
|
|
|
56
56
|
</script>
|
|
57
57
|
|
|
58
58
|
<div class="min-h-screen bg-background">
|
|
59
|
-
<!-- Header -->
|
|
59
|
+
<!-- Header (includes banner and tab groups via subheader snippet) -->
|
|
60
60
|
{@render header()}
|
|
61
61
|
|
|
62
62
|
<!-- Site-wide Banner -->
|
|
63
63
|
<SiteBanner {config} />
|
|
64
64
|
|
|
65
|
-
<!-- Tab Groups - shown only if configured -->
|
|
66
|
-
{#if config.navigation?.tabGroups && config.navigation.tabGroups.length > 0}
|
|
67
|
-
<TabGroups
|
|
68
|
-
tabGroups={config.navigation.tabGroups}
|
|
69
|
-
activeTabId={activeTabGroup}
|
|
70
|
-
onTabChange={handleTabChange}
|
|
71
|
-
flush={isFlush}
|
|
72
|
-
{docs}
|
|
73
|
-
{version}
|
|
74
|
-
/>
|
|
75
|
-
{/if}
|
|
76
|
-
|
|
77
65
|
<!-- Mobile Sidebar Overlay -->
|
|
78
66
|
{#if sidebarOpen}
|
|
79
67
|
<div
|
|
@@ -179,7 +167,7 @@
|
|
|
179
167
|
</main>
|
|
180
168
|
</div>
|
|
181
169
|
{:else}
|
|
182
|
-
<main class="container mx-auto px-2 md:px-6
|
|
170
|
+
<main class="container mx-auto px-2 md:px-6">
|
|
183
171
|
<div class="flex">
|
|
184
172
|
<!-- Desktop Sidebar - inside container -->
|
|
185
173
|
<div class="hidden lg:block">
|
|
@@ -191,7 +179,7 @@
|
|
|
191
179
|
/>
|
|
192
180
|
</div>
|
|
193
181
|
|
|
194
|
-
<div class="flex-1 min-w-0">
|
|
182
|
+
<div class="flex-1 min-w-0 py-4">
|
|
195
183
|
<div class="flex flex-col gap-2 px-2 md:px-8">
|
|
196
184
|
<!-- Content -->
|
|
197
185
|
{@render children()}
|
|
@@ -33,24 +33,17 @@
|
|
|
33
33
|
|
|
34
34
|
let { docs, version, onLinkClick, config, activeTabGroup }: Props = $props();
|
|
35
35
|
|
|
36
|
-
let hasTabGroups = $derived(
|
|
37
|
-
config.navigation?.tabGroups && config.navigation.tabGroups.length > 0
|
|
38
|
-
);
|
|
39
36
|
let isFlush = $derived(config.navigation?.sidebarStyle === 'flush');
|
|
40
|
-
let stickyTop = $derived(hasTabGroups ? 'top-[7.5rem]' : 'top-24');
|
|
41
|
-
let maxHeight = $derived(
|
|
42
|
-
hasTabGroups ? 'max-h-[calc(100vh-10rem)]' : 'max-h-[calc(100vh-7rem)]'
|
|
43
|
-
);
|
|
44
37
|
let containerClass = $derived(
|
|
45
38
|
isFlush
|
|
46
|
-
?
|
|
47
|
-
:
|
|
39
|
+
? `overflow-y-auto p-4 border-r border-border`
|
|
40
|
+
: `overflow-y-auto bg-muted/30 dark:bg-muted/10 rounded-2xl p-4 border border-border/50`
|
|
48
41
|
);
|
|
49
42
|
</script>
|
|
50
43
|
|
|
51
44
|
{#if config.navigation?.showSidebar}
|
|
52
|
-
<aside class="w-64 shrink-0 sticky
|
|
53
|
-
<div class={containerClass}>
|
|
45
|
+
<aside class="w-64 shrink-0 sticky self-start pt-4" style="top: var(--header-height, 4rem);">
|
|
46
|
+
<div class={containerClass} style="max-height: calc(100vh - var(--header-height, 4rem) - 2rem);">
|
|
54
47
|
<div class="flex items-center justify-between mb-4 px-2">
|
|
55
48
|
<h2 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Documentation</h2>
|
|
56
49
|
{#if config.features?.showVersionBadge && version}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { page } from '$app/stores';
|
|
3
|
-
import { ChevronRight, ChevronDown,
|
|
3
|
+
import { ChevronRight, ChevronDown, Lock } from 'lucide-svelte';
|
|
4
4
|
import type { SpecraConfig } from '../../config.types.js';
|
|
5
5
|
import Icon from './Icon.svelte';
|
|
6
6
|
import { sortSidebarItems, sortSidebarGroups } from '../../sidebar-utils.js';
|
|
@@ -260,8 +260,6 @@
|
|
|
260
260
|
>
|
|
261
261
|
{#if group.icon}
|
|
262
262
|
<Icon icon={group.icon} size={16} className="shrink-0" />
|
|
263
|
-
{:else}
|
|
264
|
-
<FolderOpen size={16} class="shrink-0" />
|
|
265
263
|
{/if}
|
|
266
264
|
{group.label}
|
|
267
265
|
</a>
|
|
@@ -111,8 +111,8 @@
|
|
|
111
111
|
{/if}
|
|
112
112
|
</div>
|
|
113
113
|
{:else}
|
|
114
|
-
<!-- Full version
|
|
115
|
-
<div class="
|
|
114
|
+
<!-- Full version -->
|
|
115
|
+
<div class="border-b border-border bg-muted/40 backdrop-blur supports-[backdrop-filter]:bg-muted/30">
|
|
116
116
|
<div class="{flush ? '' : 'container mx-auto'} px-4 md:px-6">
|
|
117
117
|
<!-- Mobile Dropdown -->
|
|
118
118
|
<div class="md:hidden relative">
|
|
@@ -16,14 +16,7 @@
|
|
|
16
16
|
const maxDepth = $derived(config.navigation?.tocMaxDepth ?? 3);
|
|
17
17
|
const filteredItems = $derived(items.filter((item) => item.level <= maxDepth));
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
const hasTabGroups = $derived(
|
|
21
|
-
config.navigation?.tabGroups != null && config.navigation.tabGroups.length > 0
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
// Adjust top position based on whether tabs are present
|
|
25
|
-
const stickyTop = $derived(hasTabGroups ? 'top-[7.5rem]' : 'top-24');
|
|
26
|
-
const maxHeight = $derived(hasTabGroups ? 'max-h-[calc(100vh-10rem)]' : 'max-h-[calc(100vh-7rem)]');
|
|
19
|
+
// Removed hardcoded top/max-height — now uses --header-height CSS variable set by Header
|
|
27
20
|
|
|
28
21
|
$effect(() => {
|
|
29
22
|
if (!browser || !showToc || filteredItems.length === 0) return;
|
|
@@ -79,8 +72,8 @@
|
|
|
79
72
|
</script>
|
|
80
73
|
|
|
81
74
|
{#if showToc && filteredItems.length > 0}
|
|
82
|
-
<aside class="w-64 hidden xl:block shrink-0 sticky
|
|
83
|
-
<div class="
|
|
75
|
+
<aside class="w-64 hidden xl:block shrink-0 sticky self-start pt-4" style="top: var(--header-height, 4rem);">
|
|
76
|
+
<div class="overflow-y-auto bg-muted/30 dark:bg-muted/10 rounded-2xl p-4 border border-border/50" style="max-height: calc(100vh - var(--header-height, 4rem) - 2rem);">
|
|
84
77
|
<h3 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-2">On this page</h3>
|
|
85
78
|
<nav class="space-y-1">
|
|
86
79
|
{#each filteredItems as item, index}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let { children }: Props = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div class="my-6 ml-4" style="counter-reset: timeline-step;">
|
|
12
|
+
{#if children}
|
|
13
|
+
{@render children()}
|
|
14
|
+
{/if}
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
title: string;
|
|
6
|
+
date?: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { title, date, icon, children }: Props = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class="timeline-item relative pl-8 pb-8 border-l-2 border-border last:border-l-0 last:pb-0">
|
|
15
|
+
<div class="mb-2">
|
|
16
|
+
<h3 class="text-lg font-semibold text-foreground">{title}</h3>
|
|
17
|
+
{#if date}
|
|
18
|
+
<span class="text-sm text-muted-foreground">{date}</span>
|
|
19
|
+
{/if}
|
|
20
|
+
</div>
|
|
21
|
+
<div class="prose prose-sm dark:prose-invert max-w-none [&>*:last-child]:mb-0">
|
|
22
|
+
{#if children}
|
|
23
|
+
{@render children()}
|
|
24
|
+
{/if}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.timeline-item {
|
|
30
|
+
counter-increment: timeline-step;
|
|
31
|
+
}
|
|
32
|
+
.timeline-item::before {
|
|
33
|
+
content: counter(timeline-step);
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: 0;
|
|
36
|
+
left: -1px;
|
|
37
|
+
transform: translateX(-50%);
|
|
38
|
+
width: 2rem;
|
|
39
|
+
height: 2rem;
|
|
40
|
+
border-radius: 9999px;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
font-size: 0.875rem;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
z-index: 10;
|
|
47
|
+
background: var(--primary);
|
|
48
|
+
color: var(--primary-foreground);
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
date?: string;
|
|
5
|
+
icon?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const TimelineItem: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type TimelineItem = ReturnType<typeof TimelineItem>;
|
|
10
|
+
export default TimelineItem;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Info, AlertTriangle, XCircle, CheckCircle2, X } from 'lucide-svelte';
|
|
3
|
+
|
|
4
|
+
interface BannerConfig {
|
|
5
|
+
text: string;
|
|
6
|
+
type?: 'info' | 'warning' | 'error' | 'success';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
banner: BannerConfig;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { banner }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let dismissed = $state(false);
|
|
16
|
+
|
|
17
|
+
const styles = {
|
|
18
|
+
info: {
|
|
19
|
+
bg: 'bg-blue-500/10 border-blue-500/30',
|
|
20
|
+
text: 'text-blue-900 dark:text-blue-300',
|
|
21
|
+
icon: Info,
|
|
22
|
+
},
|
|
23
|
+
warning: {
|
|
24
|
+
bg: 'bg-yellow-500/10 border-yellow-500/30',
|
|
25
|
+
text: 'text-yellow-900 dark:text-yellow-300',
|
|
26
|
+
icon: AlertTriangle,
|
|
27
|
+
},
|
|
28
|
+
error: {
|
|
29
|
+
bg: 'bg-red-500/10 border-red-500/30',
|
|
30
|
+
text: 'text-red-900 dark:text-red-300',
|
|
31
|
+
icon: XCircle,
|
|
32
|
+
},
|
|
33
|
+
success: {
|
|
34
|
+
bg: 'bg-green-500/10 border-green-500/30',
|
|
35
|
+
text: 'text-green-900 dark:text-green-300',
|
|
36
|
+
icon: CheckCircle2,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let style = $derived(styles[banner.type || 'info']);
|
|
41
|
+
let IconComponent = $derived(style.icon);
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
{#if !dismissed}
|
|
45
|
+
<div class="border-b {style.bg} {style.text}">
|
|
46
|
+
<div class="max-w-7xl mx-auto px-4 py-2.5 flex items-center justify-between gap-3">
|
|
47
|
+
<div class="flex items-center gap-2 text-sm">
|
|
48
|
+
<IconComponent class="h-4 w-4 shrink-0" />
|
|
49
|
+
<span>{banner.text}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<button
|
|
52
|
+
onclick={() => dismissed = true}
|
|
53
|
+
class="shrink-0 p-0.5 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
|
54
|
+
aria-label="Dismiss banner"
|
|
55
|
+
>
|
|
56
|
+
<X class="h-3.5 w-3.5" />
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface BannerConfig {
|
|
2
|
+
text: string;
|
|
3
|
+
type?: 'info' | 'warning' | 'error' | 'success';
|
|
4
|
+
}
|
|
5
|
+
interface Props {
|
|
6
|
+
banner: BannerConfig;
|
|
7
|
+
}
|
|
8
|
+
declare const VersionBanner: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type VersionBanner = ReturnType<typeof VersionBanner>;
|
|
10
|
+
export default VersionBanner;
|
|
@@ -4,16 +4,38 @@
|
|
|
4
4
|
import { page } from '$app/stores';
|
|
5
5
|
import { browser } from '$app/environment';
|
|
6
6
|
|
|
7
|
+
interface VersionMeta {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
badge?: string;
|
|
11
|
+
hidden?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
interface Props {
|
|
8
15
|
currentVersion: string;
|
|
9
16
|
versions: string[];
|
|
17
|
+
versionsMeta?: VersionMeta[];
|
|
10
18
|
}
|
|
11
19
|
|
|
12
|
-
let { currentVersion, versions }: Props = $props();
|
|
20
|
+
let { currentVersion, versions, versionsMeta }: Props = $props();
|
|
13
21
|
|
|
14
22
|
let isOpen = $state(false);
|
|
15
23
|
let dropdownEl = $state<HTMLDivElement | null>(null);
|
|
16
24
|
|
|
25
|
+
// Build version list: use metadata if available, filter hidden versions
|
|
26
|
+
let visibleVersions = $derived.by(() => {
|
|
27
|
+
if (versionsMeta && versionsMeta.length > 0) {
|
|
28
|
+
return versionsMeta.filter(v => !v.hidden);
|
|
29
|
+
}
|
|
30
|
+
return versions.map(id => ({ id, label: id }));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Get current version display label
|
|
34
|
+
let currentLabel = $derived.by(() => {
|
|
35
|
+
const meta = versionsMeta?.find(v => v.id === currentVersion);
|
|
36
|
+
return meta?.label || currentVersion;
|
|
37
|
+
});
|
|
38
|
+
|
|
17
39
|
$effect(() => {
|
|
18
40
|
if (!browser || !isOpen) return;
|
|
19
41
|
|
|
@@ -38,8 +60,8 @@
|
|
|
38
60
|
};
|
|
39
61
|
});
|
|
40
62
|
|
|
41
|
-
function switchVersion(
|
|
42
|
-
if (
|
|
63
|
+
function switchVersion(versionId: string) {
|
|
64
|
+
if (versionId === currentVersion) {
|
|
43
65
|
isOpen = false;
|
|
44
66
|
return;
|
|
45
67
|
}
|
|
@@ -47,7 +69,7 @@
|
|
|
47
69
|
const currentPath = $page.url.pathname;
|
|
48
70
|
const newPath = currentPath.replace(
|
|
49
71
|
new RegExp(`/${currentVersion}(/|$)`),
|
|
50
|
-
`/${
|
|
72
|
+
`/${versionId}$1`
|
|
51
73
|
);
|
|
52
74
|
|
|
53
75
|
isOpen = false;
|
|
@@ -55,7 +77,7 @@
|
|
|
55
77
|
}
|
|
56
78
|
</script>
|
|
57
79
|
|
|
58
|
-
{#if
|
|
80
|
+
{#if visibleVersions.length > 1}
|
|
59
81
|
<div class="relative" bind:this={dropdownEl}>
|
|
60
82
|
<button
|
|
61
83
|
onclick={() => (isOpen = !isOpen)}
|
|
@@ -64,27 +86,32 @@
|
|
|
64
86
|
aria-haspopup="listbox"
|
|
65
87
|
aria-label="Switch version"
|
|
66
88
|
>
|
|
67
|
-
<span>{
|
|
89
|
+
<span>{currentLabel}</span>
|
|
68
90
|
<ChevronDown class="h-3.5 w-3.5 text-muted-foreground transition-transform {isOpen ? 'rotate-180' : ''}" />
|
|
69
91
|
</button>
|
|
70
92
|
|
|
71
93
|
{#if isOpen}
|
|
72
94
|
<div
|
|
73
|
-
class="absolute top-full right-0 mt-1 w-
|
|
95
|
+
class="absolute top-full right-0 mt-1 w-48 py-1 bg-popover border border-border rounded-md shadow-lg z-50"
|
|
74
96
|
role="listbox"
|
|
75
97
|
aria-label="Available versions"
|
|
76
98
|
>
|
|
77
|
-
{#each
|
|
99
|
+
{#each visibleVersions as version}
|
|
78
100
|
<button
|
|
79
101
|
role="option"
|
|
80
|
-
aria-selected={version === currentVersion}
|
|
81
|
-
onclick={() => switchVersion(version)}
|
|
82
|
-
class="w-full flex items-center justify-between px-3 py-1.5 text-sm transition-colors {version === currentVersion
|
|
102
|
+
aria-selected={version.id === currentVersion}
|
|
103
|
+
onclick={() => switchVersion(version.id)}
|
|
104
|
+
class="w-full flex items-center justify-between px-3 py-1.5 text-sm transition-colors {version.id === currentVersion
|
|
83
105
|
? 'text-primary bg-accent/50 font-medium'
|
|
84
106
|
: 'text-foreground hover:bg-accent'}"
|
|
85
107
|
>
|
|
86
|
-
<span>
|
|
87
|
-
|
|
108
|
+
<span class="flex items-center gap-2">
|
|
109
|
+
{version.label}
|
|
110
|
+
{#if version.badge}
|
|
111
|
+
<span class="px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-primary/10 text-primary leading-none">{version.badge}</span>
|
|
112
|
+
{/if}
|
|
113
|
+
</span>
|
|
114
|
+
{#if version.id === currentVersion}
|
|
88
115
|
<Check class="h-3.5 w-3.5 text-primary" />
|
|
89
116
|
{/if}
|
|
90
117
|
</button>
|