svelte-docsmith 0.7.0 → 0.9.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/dist/buildtime/changelog.d.ts +20 -0
- package/dist/buildtime/changelog.js +74 -0
- package/dist/buildtime/fence-meta.d.ts +20 -0
- package/dist/buildtime/fence-meta.js +48 -0
- package/dist/buildtime/markdown-layout.svelte.d.ts +3 -0
- package/dist/buildtime/preprocess.d.ts +13 -0
- package/dist/buildtime/preprocess.js +77 -17
- package/dist/buildtime/twoslash.d.ts +26 -0
- package/dist/buildtime/twoslash.js +40 -0
- package/dist/buildtime/vite/collect.js +31 -3
- package/dist/buildtime/vite/git.d.ts +7 -0
- package/dist/buildtime/vite/git.js +32 -0
- package/dist/buildtime/vite/index.d.ts +13 -0
- package/dist/buildtime/vite/index.js +16 -0
- package/dist/buildtime/vite/releases.d.ts +12 -0
- package/dist/buildtime/vite/releases.js +57 -0
- package/dist/components/changelog/changelog-entry.svelte +90 -0
- package/dist/components/changelog/changelog-entry.svelte.d.ts +7 -0
- package/dist/components/changelog/changelog.svelte +56 -0
- package/dist/components/changelog/changelog.svelte.d.ts +14 -0
- package/dist/components/chrome/announcement-bar.svelte +188 -0
- package/dist/components/chrome/announcement-bar.svelte.d.ts +7 -0
- package/dist/components/docs/mermaid.svelte +213 -0
- package/dist/components/docs/mermaid.svelte.d.ts +6 -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/landing/action.svelte +50 -0
- package/dist/components/landing/action.svelte.d.ts +13 -0
- package/dist/components/landing/cta.svelte +52 -0
- package/dist/components/landing/cta.svelte.d.ts +13 -0
- package/dist/components/landing/feature-grid.svelte +51 -0
- package/dist/components/landing/feature-grid.svelte.d.ts +19 -0
- package/dist/components/landing/feature.svelte +33 -0
- package/dist/components/landing/feature.svelte.d.ts +11 -0
- package/dist/components/landing/hero.svelte +70 -0
- package/dist/components/landing/hero.svelte.d.ts +19 -0
- package/dist/components/layouts/docs-header.svelte +36 -3
- 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/components/markdown/pre.svelte +238 -8
- package/dist/components/markdown/pre.svelte.d.ts +6 -0
- package/dist/core/changelog.d.ts +31 -0
- package/dist/core/changelog.js +7 -0
- 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/fallbacks/changelog.d.ts +10 -0
- package/dist/fallbacks/changelog.js +3 -0
- package/dist/generate/feed.d.ts +35 -0
- package/dist/generate/feed.js +80 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/theme.css +32 -0
- package/package.json +21 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
3
|
+
|
|
4
|
+
const { release }: { release: ChangelogRelease } = $props();
|
|
5
|
+
|
|
6
|
+
const formatted = $derived(
|
|
7
|
+
release.date
|
|
8
|
+
? new Date(release.date).toLocaleDateString('en-US', {
|
|
9
|
+
year: 'numeric',
|
|
10
|
+
month: 'long',
|
|
11
|
+
day: 'numeric'
|
|
12
|
+
})
|
|
13
|
+
: undefined
|
|
14
|
+
);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- The version is the anchor the feed links to, so it carries the id. -->
|
|
18
|
+
<section id={release.version} class="scroll-mt-24">
|
|
19
|
+
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
20
|
+
<h2 class="text-2xl font-semibold tracking-tight">
|
|
21
|
+
{#if release.path}
|
|
22
|
+
<a href={release.path} class="hover:text-primary transition-colors">{release.version}</a>
|
|
23
|
+
{:else}
|
|
24
|
+
{release.version}
|
|
25
|
+
{/if}
|
|
26
|
+
</h2>
|
|
27
|
+
{#if formatted}
|
|
28
|
+
<time datetime={release.date} class="text-muted-foreground text-sm">{formatted}</time>
|
|
29
|
+
{/if}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{#each release.groups as group (group.kind)}
|
|
33
|
+
<h3 class="text-muted-foreground mt-6 text-xs font-semibold tracking-wide uppercase">
|
|
34
|
+
{group.kind}
|
|
35
|
+
</h3>
|
|
36
|
+
<ul class="mt-3 space-y-4">
|
|
37
|
+
{#each group.items as item (item)}
|
|
38
|
+
<li class="border-border/70 border-l-2 pl-4">
|
|
39
|
+
<!-- Rendered at build time from your own repository's changelog. -->
|
|
40
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
41
|
+
<div class="changelog-prose">{@html item}</div>
|
|
42
|
+
</li>
|
|
43
|
+
{/each}
|
|
44
|
+
</ul>
|
|
45
|
+
{/each}
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
/* The entries arrive as HTML fragments rather than flowing through the
|
|
50
|
+
markdown layout's prose styles, so they need their own small set. */
|
|
51
|
+
.changelog-prose :global(p) {
|
|
52
|
+
margin: 0 0 0.75rem;
|
|
53
|
+
line-height: 1.65;
|
|
54
|
+
}
|
|
55
|
+
.changelog-prose :global(p:last-child) {
|
|
56
|
+
margin-bottom: 0;
|
|
57
|
+
}
|
|
58
|
+
.changelog-prose :global(code) {
|
|
59
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
60
|
+
font-size: 0.85em;
|
|
61
|
+
padding: 0.05em 0.3em;
|
|
62
|
+
border-radius: 0.3rem;
|
|
63
|
+
background: color-mix(in oklch, var(--muted) 60%, transparent);
|
|
64
|
+
}
|
|
65
|
+
.changelog-prose :global(a) {
|
|
66
|
+
color: var(--primary);
|
|
67
|
+
text-decoration: underline;
|
|
68
|
+
text-underline-offset: 2px;
|
|
69
|
+
}
|
|
70
|
+
.changelog-prose :global(ul) {
|
|
71
|
+
margin: 0.5rem 0 0.75rem;
|
|
72
|
+
padding-left: 1.1rem;
|
|
73
|
+
list-style: disc;
|
|
74
|
+
}
|
|
75
|
+
.changelog-prose :global(li) {
|
|
76
|
+
margin: 0.25rem 0;
|
|
77
|
+
}
|
|
78
|
+
.changelog-prose :global(pre) {
|
|
79
|
+
margin: 0.75rem 0;
|
|
80
|
+
padding: 0.75rem 1rem;
|
|
81
|
+
border-radius: var(--radius, 0.5rem);
|
|
82
|
+
background: var(--muted);
|
|
83
|
+
overflow-x: auto;
|
|
84
|
+
font-size: 0.85em;
|
|
85
|
+
}
|
|
86
|
+
.changelog-prose :global(pre code) {
|
|
87
|
+
background: none;
|
|
88
|
+
padding: 0;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
release: ChangelogRelease;
|
|
4
|
+
};
|
|
5
|
+
declare const ChangelogEntry: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type ChangelogEntry = ReturnType<typeof ChangelogEntry>;
|
|
7
|
+
export default ChangelogEntry;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
3
|
+
import ChangelogEntry from './changelog-entry.svelte';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
releases,
|
|
7
|
+
title = 'Changelog',
|
|
8
|
+
description,
|
|
9
|
+
feed = '/changelog.xml'
|
|
10
|
+
}: {
|
|
11
|
+
/** Releases from the generated `svelte-docsmith/changelog` index. */
|
|
12
|
+
releases: ChangelogRelease[];
|
|
13
|
+
/** Page heading. */
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Line below the heading. */
|
|
16
|
+
description?: string;
|
|
17
|
+
/** Path to the Atom feed, or `false` to hide the subscribe link. */
|
|
18
|
+
feed?: string | false;
|
|
19
|
+
} = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="mx-auto max-w-3xl px-4 py-16 md:px-6 lg:py-20">
|
|
23
|
+
<header class="mb-12">
|
|
24
|
+
<h1 class="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">{title}</h1>
|
|
25
|
+
{#if description}
|
|
26
|
+
<p class="text-muted-foreground mt-4 text-lg leading-relaxed text-pretty">{description}</p>
|
|
27
|
+
{/if}
|
|
28
|
+
{#if feed}
|
|
29
|
+
<a
|
|
30
|
+
href={feed}
|
|
31
|
+
class="text-muted-foreground hover:text-foreground mt-4 inline-flex items-center gap-2 text-sm transition-colors"
|
|
32
|
+
>
|
|
33
|
+
<svg viewBox="0 0 24 24" fill="currentColor" class="size-3.5" aria-hidden="true">
|
|
34
|
+
<path
|
|
35
|
+
d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16M6 19a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"
|
|
36
|
+
fill="none"
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
stroke-width="2"
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
Subscribe via Atom
|
|
43
|
+
</a>
|
|
44
|
+
{/if}
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
{#if releases.length === 0}
|
|
48
|
+
<p class="text-muted-foreground">No releases yet.</p>
|
|
49
|
+
{:else}
|
|
50
|
+
<div class="space-y-14">
|
|
51
|
+
{#each releases as release (release.version)}
|
|
52
|
+
<ChangelogEntry {release} />
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
55
|
+
{/if}
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Releases from the generated `svelte-docsmith/changelog` index. */
|
|
4
|
+
releases: ChangelogRelease[];
|
|
5
|
+
/** Page heading. */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Line below the heading. */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Path to the Atom feed, or `false` to hide the subscribe link. */
|
|
10
|
+
feed?: string | false;
|
|
11
|
+
};
|
|
12
|
+
declare const Changelog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type Changelog = ReturnType<typeof Changelog>;
|
|
14
|
+
export default Changelog;
|
|
@@ -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,213 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
type Mermaid = (typeof import('mermaid'))['default'];
|
|
3
|
+
|
|
4
|
+
// One shared import; the bundler dedupes the chunk, this dedupes the promise.
|
|
5
|
+
let loaded: Promise<Mermaid> | undefined;
|
|
6
|
+
const loadMermaid = () => (loaded ??= import('mermaid').then((m) => m.default));
|
|
7
|
+
|
|
8
|
+
// `mermaid.initialize()` writes global config and `render()` is async, so two
|
|
9
|
+
// diagrams rendering concurrently can apply each other's theme — most visibly
|
|
10
|
+
// when one falls back to a built-in theme and reconfigures for everyone. Every
|
|
11
|
+
// initialize/render pair runs through this queue so they never interleave.
|
|
12
|
+
let chain: Promise<unknown> = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
function serialize<T>(job: () => Promise<T>): Promise<T> {
|
|
15
|
+
const result = chain.then(job, job);
|
|
16
|
+
// Swallow rejections on the chain itself so one bad diagram doesn't stall
|
|
17
|
+
// the queue for the rest of the page.
|
|
18
|
+
chain = result.catch(() => {});
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import { onMount } from 'svelte';
|
|
25
|
+
|
|
26
|
+
// Rendered from a ```mermaid code fence: the preprocessor emits a lazy
|
|
27
|
+
// `import('svelte-docsmith/mermaid')` so `mermaid` (an optional peer dep) is
|
|
28
|
+
// only pulled into pages that actually contain a diagram. Rendering is
|
|
29
|
+
// client-side — mermaid needs a DOM — with a `<pre>` source fallback.
|
|
30
|
+
const { code }: { code: string } = $props();
|
|
31
|
+
|
|
32
|
+
// Graph ids must be unique among elements currently in the document: mermaid
|
|
33
|
+
// clears whatever already carries the id it's given, so a shared id silently
|
|
34
|
+
// wipes another diagram that's already on the page. `$props.id()` is unique per
|
|
35
|
+
// instance and stays unique even if two copies of this module end up loaded.
|
|
36
|
+
const uid = $props.id();
|
|
37
|
+
// Bumped per render, because the theme toggle re-renders while this instance's
|
|
38
|
+
// previous SVG is still mounted — reusing the id would delete it mid-swap.
|
|
39
|
+
let renders = 0;
|
|
40
|
+
|
|
41
|
+
let svg = $state('');
|
|
42
|
+
let failed = $state(false);
|
|
43
|
+
// Whether the last render targeted dark mode, so we only re-render when the
|
|
44
|
+
// site theme actually flips (not on every unrelated <html> class change).
|
|
45
|
+
let lastDark: boolean | undefined;
|
|
46
|
+
|
|
47
|
+
// Reused canvas for normalizing color tokens (see resolveColor); `null` once
|
|
48
|
+
// we've determined this browser gives us no 2d context to work with.
|
|
49
|
+
let colorCanvas: CanvasRenderingContext2D | null | undefined;
|
|
50
|
+
|
|
51
|
+
// Assigning an unparseable colour to `fillStyle` is silently ignored rather
|
|
52
|
+
// than throwing, so we set this first and check whether it survived. Any
|
|
53
|
+
// colour works as long as it's an unlikely design token, since an unchanged
|
|
54
|
+
// `fillStyle` is what tells us the assignment was rejected.
|
|
55
|
+
const SENTINEL = '#010203';
|
|
56
|
+
|
|
57
|
+
// Resolve a CSS color to a concrete `#rrggbb` string mermaid's color math
|
|
58
|
+
// (khroma) can parse. Our design tokens are `oklch(...)`, which khroma can't
|
|
59
|
+
// read. Reading `fillStyle` back is not enough to convert them: a browser
|
|
60
|
+
// serializes a wide-gamut colour unchanged, so Chrome hands back the same
|
|
61
|
+
// `oklch(...)` string it was given. Painting a pixel and reading its bytes
|
|
62
|
+
// forces the conversion to sRGB. Returns null when the value can't be
|
|
63
|
+
// resolved, which also covers an engine that doesn't understand oklch at all.
|
|
64
|
+
function resolveColor(value: string): string | null {
|
|
65
|
+
colorCanvas ??= document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
66
|
+
const ctx = colorCanvas;
|
|
67
|
+
const input = value.trim();
|
|
68
|
+
if (!ctx || !input) return null;
|
|
69
|
+
ctx.fillStyle = SENTINEL;
|
|
70
|
+
ctx.fillStyle = input;
|
|
71
|
+
if (ctx.fillStyle === SENTINEL && input.toLowerCase() !== SENTINEL) return null;
|
|
72
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
73
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
74
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
75
|
+
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Map the site's design tokens onto mermaid's documented theme variables, so a
|
|
79
|
+
// diagram reads as part of the site (its palette, borders, text) instead of
|
|
80
|
+
// mermaid's stock look. Read fresh each render so it follows the light/dark
|
|
81
|
+
// toggle. Returns null when the tokens can't be resolved — the caller then
|
|
82
|
+
// falls back to a built-in theme rather than shipping broken colors.
|
|
83
|
+
function themeVariables(dark: boolean): Record<string, string | boolean> | null {
|
|
84
|
+
const root = getComputedStyle(document.documentElement);
|
|
85
|
+
const background = resolveColor(root.getPropertyValue('--background'));
|
|
86
|
+
const foreground = resolveColor(root.getPropertyValue('--foreground'));
|
|
87
|
+
const muted = resolveColor(root.getPropertyValue('--muted'));
|
|
88
|
+
const mutedForeground = resolveColor(root.getPropertyValue('--muted-foreground'));
|
|
89
|
+
const border = resolveColor(root.getPropertyValue('--border'));
|
|
90
|
+
const accent = resolveColor(root.getPropertyValue('--accent'));
|
|
91
|
+
const card = resolveColor(root.getPropertyValue('--card'));
|
|
92
|
+
if (!background || !foreground || !muted || !mutedForeground || !border || !accent || !card) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Only mermaid's documented base variables — it derives node/cluster/edge
|
|
96
|
+
// colors from these, which keeps us off its more fragile internals.
|
|
97
|
+
return {
|
|
98
|
+
darkMode: dark,
|
|
99
|
+
background,
|
|
100
|
+
primaryColor: muted, // node fill
|
|
101
|
+
primaryTextColor: foreground,
|
|
102
|
+
primaryBorderColor: border,
|
|
103
|
+
lineColor: mutedForeground, // edges/arrows
|
|
104
|
+
secondaryColor: accent,
|
|
105
|
+
tertiaryColor: card
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function render() {
|
|
110
|
+
let mermaid: Mermaid;
|
|
111
|
+
try {
|
|
112
|
+
mermaid = await loadMermaid();
|
|
113
|
+
} catch {
|
|
114
|
+
// `mermaid` isn't installed — fall back to the source.
|
|
115
|
+
failed = true;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const dark = document.documentElement.classList.contains('dark');
|
|
119
|
+
lastDark = dark;
|
|
120
|
+
const vars = themeVariables(dark);
|
|
121
|
+
// Try the token-themed `base` render first; if mermaid's color math rejects
|
|
122
|
+
// anything, retry with a built-in theme so a valid diagram never silently
|
|
123
|
+
// disappears. Only a genuine syntax error reaches the `<pre>` fallback.
|
|
124
|
+
const attempts = [
|
|
125
|
+
vars && { theme: 'base' as const, themeVariables: vars },
|
|
126
|
+
{ theme: dark ? ('dark' as const) : ('default' as const) }
|
|
127
|
+
];
|
|
128
|
+
for (const attempt of attempts) {
|
|
129
|
+
if (!attempt) continue;
|
|
130
|
+
try {
|
|
131
|
+
// Configure and render as one unit so a concurrent diagram can't
|
|
132
|
+
// re-initialize mermaid between the two calls.
|
|
133
|
+
const result = await serialize(() => {
|
|
134
|
+
mermaid.initialize({
|
|
135
|
+
startOnLoad: false,
|
|
136
|
+
securityLevel: 'strict',
|
|
137
|
+
fontFamily: 'inherit',
|
|
138
|
+
...attempt
|
|
139
|
+
});
|
|
140
|
+
return mermaid.render(`${uid}-${(renders += 1)}`, code);
|
|
141
|
+
});
|
|
142
|
+
svg = result.svg;
|
|
143
|
+
failed = false;
|
|
144
|
+
return;
|
|
145
|
+
} catch {
|
|
146
|
+
// Try the next (safer) config.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Bad diagram syntax — fall back to source.
|
|
150
|
+
failed = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
onMount(() => {
|
|
154
|
+
render();
|
|
155
|
+
// Re-render when the theme toggles so the diagram follows light/dark.
|
|
156
|
+
const observer = new MutationObserver(() => {
|
|
157
|
+
if (document.documentElement.classList.contains('dark') !== lastDark) render();
|
|
158
|
+
});
|
|
159
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
160
|
+
return () => observer.disconnect();
|
|
161
|
+
});
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
{#if failed}
|
|
165
|
+
<pre class="docsmith-mermaid-fallback not-prose"><code>{code}</code></pre>
|
|
166
|
+
{:else if svg}
|
|
167
|
+
<!-- mermaid sanitizes its own output (securityLevel: strict) -->
|
|
168
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
169
|
+
<figure class="docsmith-mermaid not-prose">{@html svg}</figure>
|
|
170
|
+
{:else}
|
|
171
|
+
<!-- Shares the global .docsmith-mermaid-skeleton with the server-rendered
|
|
172
|
+
placeholder, so swapping to the diagram doesn't change the height. -->
|
|
173
|
+
<div class="docsmith-mermaid-skeleton not-prose" role="status" aria-label="Loading diagram"></div>
|
|
174
|
+
{/if}
|
|
175
|
+
|
|
176
|
+
<style>
|
|
177
|
+
/* The loading skeleton is global (theme.css), shared with the server-rendered
|
|
178
|
+
placeholder. Here we only style the resolved diagram and the fallback. */
|
|
179
|
+
.docsmith-mermaid {
|
|
180
|
+
margin: 1.5rem 0;
|
|
181
|
+
display: flex;
|
|
182
|
+
justify-content: center;
|
|
183
|
+
animation: docsmith-mermaid-in 0.25s ease-out;
|
|
184
|
+
}
|
|
185
|
+
.docsmith-mermaid :global(svg) {
|
|
186
|
+
max-width: 100%;
|
|
187
|
+
height: auto;
|
|
188
|
+
}
|
|
189
|
+
@keyframes docsmith-mermaid-in {
|
|
190
|
+
from {
|
|
191
|
+
opacity: 0;
|
|
192
|
+
}
|
|
193
|
+
to {
|
|
194
|
+
opacity: 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
@media (prefers-reduced-motion: reduce) {
|
|
198
|
+
.docsmith-mermaid {
|
|
199
|
+
animation: none;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.docsmith-mermaid-fallback {
|
|
204
|
+
margin: 1.5rem 0;
|
|
205
|
+
padding: 1rem;
|
|
206
|
+
border-radius: var(--radius, 0.75rem);
|
|
207
|
+
border: 1px solid var(--border);
|
|
208
|
+
background: var(--muted);
|
|
209
|
+
overflow-x: auto;
|
|
210
|
+
font-size: 0.85rem;
|
|
211
|
+
line-height: 1.5;
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
@@ -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
|
+
}
|