specra 0.2.67 → 0.2.68
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/category.schema.json +58 -0
- package/dist/badges.d.ts +38 -0
- package/dist/badges.js +109 -0
- package/dist/category.d.ts +2 -0
- package/dist/changelog-context.d.ts +6 -0
- package/dist/changelog-context.js +15 -0
- package/dist/changelog.d.ts +59 -0
- package/dist/changelog.js +204 -0
- package/dist/components/docs/Changelog.svelte +71 -0
- package/dist/components/docs/Changelog.svelte.d.ts +14 -0
- package/dist/components/docs/Sidebar.svelte +3 -0
- package/dist/components/docs/Sidebar.svelte.d.ts +3 -0
- package/dist/components/docs/SidebarBadge.svelte +15 -0
- package/dist/components/docs/SidebarBadge.svelte.d.ts +7 -0
- package/dist/components/docs/SidebarMenuItems.svelte +50 -12
- package/dist/components/docs/SidebarMenuItems.svelte.d.ts +3 -0
- package/dist/components/docs/Update.svelte +75 -0
- package/dist/components/docs/Update.svelte.d.ts +19 -0
- package/dist/components/docs/index.d.ts +3 -0
- package/dist/components/docs/index.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mdx-components.d.ts +2 -2
- package/dist/mdx-components.js +4 -2
- package/dist/mdx.d.ts +5 -0
- package/dist/mdx.js +54 -16
- package/dist/rehype-changelog-ids.d.ts +20 -0
- package/dist/rehype-changelog-ids.js +43 -0
- package/dist/sidebar-utils.d.ts +3 -0
- package/dist/sidebar-utils.js +5 -1
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { ChevronRight, ChevronDown, Lock } from 'lucide-svelte';
|
|
5
5
|
import type { SpecraConfig } from '../../config.types.js';
|
|
6
6
|
import Icon from './Icon.svelte';
|
|
7
|
+
import SidebarBadge from './SidebarBadge.svelte';
|
|
8
|
+
import { resolveBadges, type BadgeInput } from '../../badges.js';
|
|
7
9
|
import { sortSidebarItems, sortSidebarGroups } from '../../sidebar-utils.js';
|
|
8
10
|
import { renderInlineCode } from '../../inline.js';
|
|
9
11
|
|
|
@@ -21,11 +23,13 @@
|
|
|
21
23
|
categoryCollapsed?: boolean;
|
|
22
24
|
categoryIcon?: string;
|
|
23
25
|
categoryTabGroup?: string;
|
|
26
|
+
categoryBadge?: BadgeInput;
|
|
24
27
|
meta?: {
|
|
25
28
|
icon?: string;
|
|
26
29
|
tab_group?: string;
|
|
27
30
|
sidebar_position?: number;
|
|
28
31
|
order?: number;
|
|
32
|
+
badge?: BadgeInput;
|
|
29
33
|
[key: string]: any;
|
|
30
34
|
};
|
|
31
35
|
}
|
|
@@ -34,6 +38,7 @@
|
|
|
34
38
|
label: string;
|
|
35
39
|
path: string;
|
|
36
40
|
icon?: string;
|
|
41
|
+
badge?: BadgeInput;
|
|
37
42
|
items: DocItem[];
|
|
38
43
|
position: number;
|
|
39
44
|
collapsible: boolean;
|
|
@@ -150,6 +155,7 @@
|
|
|
150
155
|
if (isIndexFile) {
|
|
151
156
|
rootGroups[groupName].position = doc.sidebar_position ?? 999;
|
|
152
157
|
rootGroups[groupName].icon = doc.categoryIcon;
|
|
158
|
+
rootGroups[groupName].badge = doc.categoryBadge;
|
|
153
159
|
}
|
|
154
160
|
rootGroups[groupName].items.push(doc);
|
|
155
161
|
return;
|
|
@@ -168,14 +174,19 @@
|
|
|
168
174
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
169
175
|
.join(' ');
|
|
170
176
|
|
|
177
|
+
const isOwnCategory = i === folderParts.length - 1;
|
|
178
|
+
|
|
171
179
|
if (!currentLevel[folder]) {
|
|
172
180
|
currentLevel[folder] = {
|
|
173
181
|
label:
|
|
174
|
-
doc.categoryLabel &&
|
|
182
|
+
doc.categoryLabel && isOwnCategory
|
|
175
183
|
? doc.categoryLabel
|
|
176
184
|
: folderLabel,
|
|
177
185
|
path: currentPath,
|
|
178
186
|
icon: doc.categoryIcon,
|
|
187
|
+
// A doc's `_category_.json` describes its *own* folder, so the
|
|
188
|
+
// badge must not leak onto the ancestor folders we walk through.
|
|
189
|
+
badge: isOwnCategory ? doc.categoryBadge : undefined,
|
|
179
190
|
items: [],
|
|
180
191
|
position: doc.categoryPosition ?? 999,
|
|
181
192
|
collapsible: doc.categoryCollapsible ?? true,
|
|
@@ -184,7 +195,10 @@
|
|
|
184
195
|
};
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
if (
|
|
198
|
+
if (isOwnCategory) {
|
|
199
|
+
if (doc.categoryBadge) {
|
|
200
|
+
currentLevel[folder].badge = doc.categoryBadge;
|
|
201
|
+
}
|
|
188
202
|
if (isIndexFile) {
|
|
189
203
|
currentLevel[folder].position =
|
|
190
204
|
doc.categoryPosition ?? doc.sidebar_position ?? 999;
|
|
@@ -287,6 +301,7 @@
|
|
|
287
301
|
{@const marginLeft = depth > 0 ? 'ml-4' : ''}
|
|
288
302
|
{@const groupHref = getGroupHref(group)}
|
|
289
303
|
{@const mergedItems = getMergedItems(group)}
|
|
304
|
+
{@const groupBadges = resolveBadges(group.badge)}
|
|
290
305
|
|
|
291
306
|
<div class="space-y-1 {marginLeft}">
|
|
292
307
|
<div class="flex items-center group">
|
|
@@ -296,14 +311,21 @@
|
|
|
296
311
|
e.preventDefault();
|
|
297
312
|
toggleSection(groupKey);
|
|
298
313
|
}}
|
|
299
|
-
class="flex items-center gap-2 flex-1 px-3 py-2 text-sm font-semibold rounded-l-xl transition-all {isGroupActive
|
|
314
|
+
class="flex items-center gap-2 flex-1 min-w-0 px-3 py-2 text-sm font-semibold rounded-l-xl transition-all {isGroupActive
|
|
300
315
|
? 'bg-primary/10 text-primary'
|
|
301
316
|
: 'text-foreground hover:bg-accent/50'}"
|
|
302
317
|
>
|
|
303
318
|
{#if group.icon}
|
|
304
319
|
<Icon icon={group.icon} size={16} className="shrink-0" />
|
|
305
320
|
{/if}
|
|
306
|
-
{@html renderInlineCode(group.label)}
|
|
321
|
+
<span class="truncate">{@html renderInlineCode(group.label)}</span>
|
|
322
|
+
{#if groupBadges.length > 0}
|
|
323
|
+
<span class="ml-auto flex items-center gap-1 shrink-0">
|
|
324
|
+
{#each groupBadges as badge (badge.text)}
|
|
325
|
+
<SidebarBadge {badge} />
|
|
326
|
+
{/each}
|
|
327
|
+
</span>
|
|
328
|
+
{/if}
|
|
307
329
|
</a>
|
|
308
330
|
|
|
309
331
|
{#if hasContent && group.collapsible && config.navigation?.collapsibleSidebar}
|
|
@@ -333,19 +355,27 @@
|
|
|
333
355
|
{:else}
|
|
334
356
|
{@const href = `${docsBase}/${item.doc.slug}`}
|
|
335
357
|
{@const isActive = pathname === href}
|
|
358
|
+
{@const badges = resolveBadges(item.doc.meta?.badge)}
|
|
336
359
|
<a
|
|
337
360
|
{href}
|
|
338
361
|
onclick={onLinkClick}
|
|
339
|
-
class="flex items-center gap-2 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
362
|
+
class="flex items-center gap-2 min-w-0 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
340
363
|
? 'bg-primary/10 text-primary font-medium'
|
|
341
364
|
: 'text-foreground hover:text-foreground hover:bg-accent/50'}"
|
|
342
365
|
>
|
|
343
366
|
{#if item.doc.meta?.icon}
|
|
344
367
|
<Icon icon={item.doc.meta.icon} size={16} className="shrink-0" />
|
|
345
368
|
{/if}
|
|
346
|
-
{@html renderInlineCode(item.doc.title)}
|
|
347
|
-
{#if item.doc.meta?.isProtected}
|
|
348
|
-
<
|
|
369
|
+
<span class="truncate">{@html renderInlineCode(item.doc.title)}</span>
|
|
370
|
+
{#if badges.length > 0 || item.doc.meta?.isProtected}
|
|
371
|
+
<span class="ml-auto flex items-center gap-1 shrink-0">
|
|
372
|
+
{#each badges as badge (badge.text)}
|
|
373
|
+
<SidebarBadge {badge} />
|
|
374
|
+
{/each}
|
|
375
|
+
{#if item.doc.meta?.isProtected}
|
|
376
|
+
<Lock size={14} class="text-muted-foreground" />
|
|
377
|
+
{/if}
|
|
378
|
+
</span>
|
|
349
379
|
{/if}
|
|
350
380
|
</a>
|
|
351
381
|
{/if}
|
|
@@ -360,19 +390,27 @@
|
|
|
360
390
|
{#each sortedStandalone as doc (doc.slug)}
|
|
361
391
|
{@const href = `${docsBase}/${doc.slug}`}
|
|
362
392
|
{@const isActive = pathname === href}
|
|
393
|
+
{@const badges = resolveBadges(doc.meta?.badge)}
|
|
363
394
|
<a
|
|
364
395
|
{href}
|
|
365
396
|
onclick={onLinkClick}
|
|
366
|
-
class="flex items-center gap-2 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
397
|
+
class="flex items-center gap-2 min-w-0 px-3 py-2 text-sm rounded-xl transition-all {isActive
|
|
367
398
|
? 'bg-primary/10 text-primary font-medium'
|
|
368
399
|
: 'text-foreground hover:text-foreground hover:bg-accent/50'}"
|
|
369
400
|
>
|
|
370
401
|
{#if doc.meta?.icon}
|
|
371
402
|
<Icon icon={doc.meta.icon} size={16} className="shrink-0" />
|
|
372
403
|
{/if}
|
|
373
|
-
{@html renderInlineCode(doc.title)}
|
|
374
|
-
{#if doc.meta?.isProtected}
|
|
375
|
-
<
|
|
404
|
+
<span class="truncate">{@html renderInlineCode(doc.title)}</span>
|
|
405
|
+
{#if badges.length > 0 || doc.meta?.isProtected}
|
|
406
|
+
<span class="ml-auto flex items-center gap-1 shrink-0">
|
|
407
|
+
{#each badges as badge (badge.text)}
|
|
408
|
+
<SidebarBadge {badge} />
|
|
409
|
+
{/each}
|
|
410
|
+
{#if doc.meta?.isProtected}
|
|
411
|
+
<Lock size={14} class="text-muted-foreground" />
|
|
412
|
+
{/if}
|
|
413
|
+
</span>
|
|
376
414
|
{/if}
|
|
377
415
|
</a>
|
|
378
416
|
{/each}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SpecraConfig } from '../../config.types.js';
|
|
2
|
+
import { type BadgeInput } from '../../badges.js';
|
|
2
3
|
interface DocItem {
|
|
3
4
|
title: string;
|
|
4
5
|
slug: string;
|
|
@@ -13,11 +14,13 @@ interface DocItem {
|
|
|
13
14
|
categoryCollapsed?: boolean;
|
|
14
15
|
categoryIcon?: string;
|
|
15
16
|
categoryTabGroup?: string;
|
|
17
|
+
categoryBadge?: BadgeInput;
|
|
16
18
|
meta?: {
|
|
17
19
|
icon?: string;
|
|
18
20
|
tab_group?: string;
|
|
19
21
|
sidebar_position?: number;
|
|
20
22
|
order?: number;
|
|
23
|
+
badge?: BadgeInput;
|
|
21
24
|
[key: string]: any;
|
|
22
25
|
};
|
|
23
26
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { getChangelogContext } from '../../changelog-context.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Date or release name. Anchors the entry and names it in the ToC. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Slug of `label`, injected server-side by rehype-changelog-ids. */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** Secondary line, typically a version. */
|
|
11
|
+
description?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
/** Overrides the RSS entry. Consumed server-side; unused when rendering. */
|
|
14
|
+
rss?: { title?: string; description?: string };
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { label, id, description, tags = [], children }: Props = $props();
|
|
19
|
+
|
|
20
|
+
// Absent outside a <Changelog>, in which case nothing filters this update.
|
|
21
|
+
const changelog = getChangelogContext();
|
|
22
|
+
|
|
23
|
+
// Reads the parent's selected-tag state, so toggling a filter re-renders.
|
|
24
|
+
let visible = $derived(changelog ? changelog.matches(tags) : true);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if visible}
|
|
28
|
+
<div
|
|
29
|
+
{id}
|
|
30
|
+
class="specra-update grid gap-x-8 gap-y-3 pb-12 scroll-mt-24 last:pb-0 md:grid-cols-[10rem_minmax(0,1fr)]"
|
|
31
|
+
>
|
|
32
|
+
<div class="md:sticky md:top-24 md:self-start md:text-right">
|
|
33
|
+
{#if label}
|
|
34
|
+
{#if id}
|
|
35
|
+
<a
|
|
36
|
+
href="#{id}"
|
|
37
|
+
class="text-sm font-semibold text-foreground transition-colors hover:text-primary"
|
|
38
|
+
>
|
|
39
|
+
{label}
|
|
40
|
+
</a>
|
|
41
|
+
{:else}
|
|
42
|
+
<span class="text-sm font-semibold text-foreground">{label}</span>
|
|
43
|
+
{/if}
|
|
44
|
+
{/if}
|
|
45
|
+
|
|
46
|
+
{#if description}
|
|
47
|
+
<div class="mt-1 text-xs text-muted-foreground">{description}</div>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
{#if tags.length > 0}
|
|
51
|
+
<div class="mt-2 flex flex-wrap gap-1 md:justify-end">
|
|
52
|
+
{#each tags as tag (tag)}
|
|
53
|
+
<span
|
|
54
|
+
class="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium leading-none text-muted-foreground"
|
|
55
|
+
>
|
|
56
|
+
{tag}
|
|
57
|
+
</span>
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="relative border-l border-border pl-8">
|
|
64
|
+
<span
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
class="absolute -left-[4.5px] top-2 h-2 w-2 rounded-full bg-border ring-4 ring-background"
|
|
67
|
+
></span>
|
|
68
|
+
<div class="prose prose-sm dark:prose-invert max-w-none [&>*:last-child]:mb-0">
|
|
69
|
+
{#if children}
|
|
70
|
+
{@render children()}
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Date or release name. Anchors the entry and names it in the ToC. */
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Slug of `label`, injected server-side by rehype-changelog-ids. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Secondary line, typically a version. */
|
|
8
|
+
description?: string;
|
|
9
|
+
tags?: string[];
|
|
10
|
+
/** Overrides the RSS entry. Consumed server-side; unused when rendering. */
|
|
11
|
+
rss?: {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
};
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
declare const Update: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Update = ReturnType<typeof Update>;
|
|
19
|
+
export default Update;
|
|
@@ -37,7 +37,10 @@ export { default as MobileSidebarWrapper } from './MobileSidebarWrapper.svelte';
|
|
|
37
37
|
export { default as NotFoundContent } from './NotFoundContent.svelte';
|
|
38
38
|
export { default as SearchHighlight } from './SearchHighlight.svelte';
|
|
39
39
|
export { default as SearchModal } from './SearchModal.svelte';
|
|
40
|
+
export { default as Changelog } from './Changelog.svelte';
|
|
41
|
+
export { default as Update } from './Update.svelte';
|
|
40
42
|
export { default as Sidebar } from './Sidebar.svelte';
|
|
43
|
+
export { default as SidebarBadge } from './SidebarBadge.svelte';
|
|
41
44
|
export { default as SidebarMenuItems } from './SidebarMenuItems.svelte';
|
|
42
45
|
export { default as SidebarSkeleton } from './SidebarSkeleton.svelte';
|
|
43
46
|
export { default as SiteBanner } from './SiteBanner.svelte';
|
|
@@ -38,7 +38,10 @@ export { default as MobileSidebarWrapper } from './MobileSidebarWrapper.svelte';
|
|
|
38
38
|
export { default as NotFoundContent } from './NotFoundContent.svelte';
|
|
39
39
|
export { default as SearchHighlight } from './SearchHighlight.svelte';
|
|
40
40
|
export { default as SearchModal } from './SearchModal.svelte';
|
|
41
|
+
export { default as Changelog } from './Changelog.svelte';
|
|
42
|
+
export { default as Update } from './Update.svelte';
|
|
41
43
|
export { default as Sidebar } from './Sidebar.svelte';
|
|
44
|
+
export { default as SidebarBadge } from './SidebarBadge.svelte';
|
|
42
45
|
export { default as SidebarMenuItems } from './SidebarMenuItems.svelte';
|
|
43
46
|
export { default as SidebarSkeleton } from './SidebarSkeleton.svelte';
|
|
44
47
|
export { default as SiteBanner } from './SiteBanner.svelte';
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export * from './utils.js';
|
|
|
14
14
|
export * from './links.js';
|
|
15
15
|
export * from './inline.js';
|
|
16
16
|
export * from './sidebar-utils.js';
|
|
17
|
+
export * from './badges.js';
|
|
18
|
+
export * from './changelog.js';
|
|
19
|
+
export * from './changelog-context.js';
|
|
17
20
|
export * from './category.js';
|
|
18
21
|
export * from './redirects.js';
|
|
19
22
|
export * from './dev-utils.js';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ export * from './utils.js';
|
|
|
18
18
|
export * from './links.js';
|
|
19
19
|
export * from './inline.js';
|
|
20
20
|
export * from './sidebar-utils.js';
|
|
21
|
+
export * from './badges.js';
|
|
22
|
+
export * from './changelog.js';
|
|
23
|
+
export * from './changelog-context.js';
|
|
21
24
|
export * from './category.js';
|
|
22
25
|
export * from './redirects.js';
|
|
23
26
|
export * from './dev-utils.js';
|
package/dist/mdx-components.d.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
import type { Component } from 'svelte';
|
|
17
|
-
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference } from './components/docs';
|
|
18
|
-
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, };
|
|
17
|
+
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update } from './components/docs';
|
|
18
|
+
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update, };
|
|
19
19
|
/**
|
|
20
20
|
* Component map for passing to layout components that render MDX content.
|
|
21
21
|
*/
|
package/dist/mdx-components.js
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* <Callout type="info">This is a callout</Callout>
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
-
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, } from './components/docs';
|
|
16
|
+
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update, } from './components/docs';
|
|
17
17
|
// Re-export all MDX-usable components
|
|
18
|
-
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, };
|
|
18
|
+
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, Changelog, Update, };
|
|
19
19
|
/**
|
|
20
20
|
* Component map for passing to layout components that render MDX content.
|
|
21
21
|
*/
|
|
@@ -50,4 +50,6 @@ export const mdxComponents = {
|
|
|
50
50
|
ApiResponse,
|
|
51
51
|
ApiPlayground,
|
|
52
52
|
ApiReference,
|
|
53
|
+
Changelog,
|
|
54
|
+
Update,
|
|
53
55
|
};
|
package/dist/mdx.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BadgeInput } from "./badges";
|
|
1
2
|
import type { I18nConfig } from "./config.types";
|
|
2
3
|
/**
|
|
3
4
|
* Structured node type for MDX content rendering.
|
|
@@ -12,6 +13,7 @@ export interface MdxNode {
|
|
|
12
13
|
props?: Record<string, any>;
|
|
13
14
|
children?: MdxNode[];
|
|
14
15
|
}
|
|
16
|
+
export declare function processMarkdownToMdxNodes(markdown: string): Promise<MdxNode[]>;
|
|
15
17
|
export interface DocMeta {
|
|
16
18
|
title: string;
|
|
17
19
|
description?: string;
|
|
@@ -34,6 +36,8 @@ export interface DocMeta {
|
|
|
34
36
|
word_count?: number;
|
|
35
37
|
icon?: string;
|
|
36
38
|
tab_group?: string;
|
|
39
|
+
badge?: BadgeInput;
|
|
40
|
+
rss?: boolean;
|
|
37
41
|
locale?: string;
|
|
38
42
|
protected?: boolean;
|
|
39
43
|
isProtected?: boolean;
|
|
@@ -51,6 +55,7 @@ export interface Doc {
|
|
|
51
55
|
categoryCollapsed?: boolean;
|
|
52
56
|
categoryIcon?: string;
|
|
53
57
|
categoryTabGroup?: string;
|
|
58
|
+
categoryBadge?: BadgeInput;
|
|
54
59
|
locale?: string;
|
|
55
60
|
}
|
|
56
61
|
export interface TocItem {
|
package/dist/mdx.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "path";
|
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
5
|
import { rehypeBasePath } from "./rehype-base-path.js";
|
|
6
|
+
import { rehypeChangelogIds } from "./rehype-changelog-ids.js";
|
|
7
|
+
import { annotateChangelogNodes } from "./changelog.js";
|
|
6
8
|
import { remarkCodeMeta } from "./remark-code-meta.js";
|
|
7
9
|
import { unified } from "unified";
|
|
8
10
|
import remarkParse from "remark-parse";
|
|
@@ -10,7 +12,7 @@ import remarkGfm from "remark-gfm";
|
|
|
10
12
|
import remarkMath from "remark-math";
|
|
11
13
|
import remarkRehype from "remark-rehype";
|
|
12
14
|
import rehypeSlug from "rehype-slug";
|
|
13
|
-
import
|
|
15
|
+
import GithubSlugger from "github-slugger";
|
|
14
16
|
import rehypeRaw from "rehype-raw";
|
|
15
17
|
import rehypeKatex from "rehype-katex";
|
|
16
18
|
import rehypeStringify from "rehype-stringify";
|
|
@@ -67,6 +69,8 @@ const COMPONENT_TAG_MAP = {
|
|
|
67
69
|
apiresponse: 'ApiResponse',
|
|
68
70
|
apiplayground: 'ApiPlayground',
|
|
69
71
|
apireference: 'ApiReference',
|
|
72
|
+
changelog: 'Changelog',
|
|
73
|
+
update: 'Update',
|
|
70
74
|
};
|
|
71
75
|
/**
|
|
72
76
|
* Map of lowercased attribute names to their correct camelCase form.
|
|
@@ -1268,7 +1272,7 @@ function ensureComponentBlockIntegrity(markdown) {
|
|
|
1268
1272
|
}
|
|
1269
1273
|
return markdown;
|
|
1270
1274
|
}
|
|
1271
|
-
async function processMarkdownToMdxNodes(markdown) {
|
|
1275
|
+
export async function processMarkdownToMdxNodes(markdown) {
|
|
1272
1276
|
// Mask pipes inside inline code spans so GFM tables containing
|
|
1273
1277
|
// `{{ x | filter }}`-style code don't get their rows broken.
|
|
1274
1278
|
const masked = maskInlineCodePipes(markdown);
|
|
@@ -1289,6 +1293,9 @@ async function processMarkdownToMdxNodes(markdown) {
|
|
|
1289
1293
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
1290
1294
|
.use(rehypeRaw)
|
|
1291
1295
|
.use(rehypeSlug)
|
|
1296
|
+
// After rehypeSlug so the intent is obvious: replay the heading sequence
|
|
1297
|
+
// through a second slugger, then anchor each <Update> in that namespace.
|
|
1298
|
+
.use(rehypeChangelogIds)
|
|
1292
1299
|
.use(rehypeKatex);
|
|
1293
1300
|
if (basePath) {
|
|
1294
1301
|
processor.use(rehypeBasePath, { basePath });
|
|
@@ -1299,6 +1306,10 @@ async function processMarkdownToMdxNodes(markdown) {
|
|
|
1299
1306
|
const children = hast.children || [];
|
|
1300
1307
|
const nodes = await hastChildrenToMdxNodes(children);
|
|
1301
1308
|
restorePipeMarkersInNodes(nodes);
|
|
1309
|
+
// Union each <Changelog>'s child tags into `allTags`. Done here, on the
|
|
1310
|
+
// server, because children render after their parent during SSR — a filter
|
|
1311
|
+
// bar that waited for its updates to self-register would serialize empty.
|
|
1312
|
+
annotateChangelogNodes(nodes);
|
|
1302
1313
|
return nodes;
|
|
1303
1314
|
}
|
|
1304
1315
|
/**
|
|
@@ -1566,6 +1577,7 @@ export function getAllDocs(version = "v1.0.0", locale, product) {
|
|
|
1566
1577
|
doc.categoryCollapsed = categoryConfig.collapsed;
|
|
1567
1578
|
doc.categoryIcon = categoryConfig.icon;
|
|
1568
1579
|
doc.categoryTabGroup = categoryConfig.tab_group;
|
|
1580
|
+
doc.categoryBadge = categoryConfig.badge;
|
|
1569
1581
|
}
|
|
1570
1582
|
}
|
|
1571
1583
|
return doc;
|
|
@@ -1710,26 +1722,52 @@ export function getAdjacentDocs(currentSlug, allDocs) {
|
|
|
1710
1722
|
};
|
|
1711
1723
|
}
|
|
1712
1724
|
export function extractTableOfContents(content) {
|
|
1713
|
-
|
|
1725
|
+
// One regex over both kinds of anchor so they come back in document order.
|
|
1726
|
+
// group 1/2 — an ATX heading, at any level (see the slugger note below)
|
|
1727
|
+
// group 3 — the `label` of an <Update>, which anchors a changelog entry
|
|
1728
|
+
// `[^>]*?` (not `.`) spans newlines, because authors wrap long <Update> tags.
|
|
1729
|
+
const anchorRegex = /^(#{1,6})[ \t]+(.+)$|<update\b[^>]*?\blabel\s*=\s*["']([^"']+)["']/gim;
|
|
1714
1730
|
const toc = [];
|
|
1731
|
+
// Mirror rehype-slug exactly: it builds ONE GithubSlugger per document and
|
|
1732
|
+
// slugs every h1–h6 in order, so a repeated heading renders as `setup` and
|
|
1733
|
+
// then `setup-1`. github-slugger's bare `slug()` export is stateless and
|
|
1734
|
+
// cannot know that, so it emitted `setup` twice and the second ToC link
|
|
1735
|
+
// silently scrolled to the first heading.
|
|
1736
|
+
//
|
|
1737
|
+
// The slugger must see headings the ToC never displays (h1, h4–h6) because
|
|
1738
|
+
// they still consume names from the same namespace — `# Setup` followed by
|
|
1739
|
+
// `## Setup` renders the h2 as `setup-1`.
|
|
1740
|
+
//
|
|
1741
|
+
// <Update> labels share this slugger too, mirroring rehype-changelog-ids.ts,
|
|
1742
|
+
// so a label can never collide with a heading of the same text.
|
|
1743
|
+
const slugger = new GithubSlugger();
|
|
1715
1744
|
// Headings inside fenced code blocks are code, not headings. They get no
|
|
1716
|
-
// `id` in the rendered page, so a ToC entry for them can only ever 404
|
|
1745
|
+
// `id` in the rendered page, so a ToC entry for them can only ever 404 —
|
|
1746
|
+
// and rehype-slug never sees them either, so skipping them keeps the two
|
|
1747
|
+
// sluggers in lockstep. The same reasoning covers a fenced <Update> example.
|
|
1717
1748
|
for (const { text: segment, isCode } of splitByCodeFences(content)) {
|
|
1718
1749
|
if (isCode)
|
|
1719
1750
|
continue;
|
|
1720
1751
|
let match;
|
|
1721
|
-
|
|
1722
|
-
while ((match =
|
|
1723
|
-
const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
const
|
|
1752
|
+
anchorRegex.lastIndex = 0;
|
|
1753
|
+
while ((match = anchorRegex.exec(segment)) !== null) {
|
|
1754
|
+
const [, hashes, headingText, updateLabel] = match;
|
|
1755
|
+
if (updateLabel !== undefined) {
|
|
1756
|
+
const label = updateLabel.trim();
|
|
1757
|
+
if (!label)
|
|
1758
|
+
continue;
|
|
1759
|
+
// Updates sit at the ToC's top level, alongside h2s.
|
|
1760
|
+
toc.push({ id: slugger.slug(label), title: label, level: 2 });
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const level = hashes.length;
|
|
1764
|
+
const text = headingText.trim();
|
|
1765
|
+
// Backticks are stripped first because rehype-slug slugs the RENDERED
|
|
1766
|
+
// heading text, where `code` markup is already gone. Always slug, even
|
|
1767
|
+
// for levels we drop, to keep the occurrence counters aligned.
|
|
1768
|
+
const id = slugger.slug(text.replace(/`/g, ""));
|
|
1769
|
+
if (level < 2 || level > 3)
|
|
1770
|
+
continue;
|
|
1733
1771
|
toc.push({ id, title: text, level });
|
|
1734
1772
|
}
|
|
1735
1773
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehype plugin that gives every `<Update>` a stable anchor id.
|
|
3
|
+
*
|
|
4
|
+
* The id is slugged from the update's `label`, using a slugger that has ALSO
|
|
5
|
+
* consumed every heading on the page. rehype-slug builds one GithubSlugger per
|
|
6
|
+
* document and feeds it every h1–h6 in order; by replaying that same sequence
|
|
7
|
+
* here we land in the same namespace. Two consequences:
|
|
8
|
+
*
|
|
9
|
+
* - An `<Update label="Changelog">` on a page that also has `## Changelog`
|
|
10
|
+
* becomes `changelog-1` instead of silently duplicating the heading's id.
|
|
11
|
+
* - Two updates sharing a label get `v1` and `v1-1`, never a collision.
|
|
12
|
+
*
|
|
13
|
+
* `extractTableOfContents` performs the same walk over the raw markdown, so the
|
|
14
|
+
* anchors it links to are byte-identical to the ones rendered here.
|
|
15
|
+
*
|
|
16
|
+
* Manually walks the tree to avoid ESM/CJS issues with unist-util-visit, matching
|
|
17
|
+
* rehype-base-path.ts.
|
|
18
|
+
*/
|
|
19
|
+
import type { Root } from 'hast';
|
|
20
|
+
export declare function rehypeChangelogIds(): (tree: Root) => void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import GithubSlugger from 'github-slugger';
|
|
2
|
+
const HEADING_TAG = /^h[1-6]$/;
|
|
3
|
+
/** Concatenate the text nodes beneath a node, as hast-util-to-string would. */
|
|
4
|
+
function textContent(node) {
|
|
5
|
+
if (node.type === 'text')
|
|
6
|
+
return String(node.value ?? '');
|
|
7
|
+
if (Array.isArray(node.children))
|
|
8
|
+
return node.children.map(textContent).join('');
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
/** Preorder walk over element nodes, i.e. document order. */
|
|
12
|
+
function walkElements(nodes, fn) {
|
|
13
|
+
for (const node of nodes) {
|
|
14
|
+
if (node.type !== 'element')
|
|
15
|
+
continue;
|
|
16
|
+
fn(node);
|
|
17
|
+
if (node.children) {
|
|
18
|
+
walkElements(node.children, fn);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function rehypeChangelogIds() {
|
|
23
|
+
return (tree) => {
|
|
24
|
+
const slugger = new GithubSlugger();
|
|
25
|
+
walkElements(tree.children, (node) => {
|
|
26
|
+
// Consume the name rehype-slug already consumed for this heading, keeping
|
|
27
|
+
// the two occurrence counters aligned. The return value is discarded —
|
|
28
|
+
// rehype-slug owns the heading's actual id.
|
|
29
|
+
if (HEADING_TAG.test(node.tagName)) {
|
|
30
|
+
slugger.slug(textContent(node));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (node.tagName !== 'update')
|
|
34
|
+
return;
|
|
35
|
+
const label = node.properties?.label;
|
|
36
|
+
// A label-less update has no identity to anchor. It still renders; it just
|
|
37
|
+
// gets no anchor, no ToC entry and no feed item.
|
|
38
|
+
if (typeof label !== 'string' || !label.trim())
|
|
39
|
+
return;
|
|
40
|
+
node.properties = { ...node.properties, id: slugger.slug(label.trim()) };
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/sidebar-utils.d.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* This module provides consistent sidebar logic across the application
|
|
4
4
|
* to ensure ordering is handled the same way everywhere.
|
|
5
5
|
*/
|
|
6
|
+
import type { BadgeInput } from "./badges.js";
|
|
6
7
|
export interface SidebarGroup {
|
|
7
8
|
label: string;
|
|
8
9
|
path: string;
|
|
9
10
|
icon?: string;
|
|
11
|
+
badge?: BadgeInput;
|
|
10
12
|
items: any[];
|
|
11
13
|
position: number;
|
|
12
14
|
collapsible: boolean;
|
|
@@ -50,6 +52,7 @@ export declare function buildSidebarStructure<T extends {
|
|
|
50
52
|
categoryLabel?: string;
|
|
51
53
|
categoryPosition?: number;
|
|
52
54
|
categoryIcon?: string;
|
|
55
|
+
categoryBadge?: BadgeInput;
|
|
53
56
|
categoryCollapsible?: boolean;
|
|
54
57
|
categoryCollapsed?: boolean;
|
|
55
58
|
meta: any;
|
package/dist/sidebar-utils.js
CHANGED
|
@@ -49,11 +49,14 @@ export function buildSidebarStructure(docs) {
|
|
|
49
49
|
docs.forEach((doc) => {
|
|
50
50
|
const pathParts = doc.filePath.split("/");
|
|
51
51
|
const folderPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
|
|
52
|
-
|
|
52
|
+
// A `_category_.json` may set a badge without setting a label, so keying
|
|
53
|
+
// this purely off `categoryLabel` would silently drop the badge.
|
|
54
|
+
if (folderPath && (doc.categoryLabel || doc.categoryBadge)) {
|
|
53
55
|
categoryMetadata.set(folderPath, {
|
|
54
56
|
label: doc.categoryLabel,
|
|
55
57
|
position: doc.categoryPosition,
|
|
56
58
|
icon: doc.categoryIcon,
|
|
59
|
+
badge: doc.categoryBadge,
|
|
57
60
|
collapsible: doc.categoryCollapsible,
|
|
58
61
|
collapsed: doc.categoryCollapsed
|
|
59
62
|
});
|
|
@@ -100,6 +103,7 @@ export function buildSidebarStructure(docs) {
|
|
|
100
103
|
label: metadata?.label ?? folderLabel,
|
|
101
104
|
path: currentPath,
|
|
102
105
|
icon: metadata?.icon,
|
|
106
|
+
badge: metadata?.badge,
|
|
103
107
|
items: [],
|
|
104
108
|
position: metadata?.position ?? 999,
|
|
105
109
|
collapsible: metadata?.collapsible ?? true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specra",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.68",
|
|
4
4
|
"description": "A modern documentation library for SvelteKit with built-in versioning, API reference generation, full-text search, and MDX support",
|
|
5
5
|
"svelte": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|