specra 0.2.6 → 0.2.7
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 +23 -0
- package/dist/category.d.ts +1 -1
- package/dist/category.js +4 -2
- package/dist/components/docs/Breadcrumb.svelte +11 -4
- package/dist/components/docs/Breadcrumb.svelte.d.ts +1 -0
- package/dist/components/docs/CategoryIndex.svelte +7 -2
- package/dist/components/docs/CategoryIndex.svelte.d.ts +1 -0
- package/dist/components/docs/DocLayout.svelte +9 -8
- package/dist/components/docs/DocLayout.svelte.d.ts +1 -0
- package/dist/components/docs/DocNavigation.svelte +10 -3
- package/dist/components/docs/DocNavigation.svelte.d.ts +1 -0
- package/dist/components/docs/Header.svelte +17 -1
- package/dist/components/docs/Header.svelte.d.ts +10 -0
- package/dist/components/docs/MobileDocLayout.svelte +5 -1
- package/dist/components/docs/MobileDocLayout.svelte.d.ts +1 -0
- package/dist/components/docs/MobileSidebar.svelte +3 -1
- package/dist/components/docs/MobileSidebar.svelte.d.ts +1 -0
- package/dist/components/docs/MobileSidebarWrapper.svelte +3 -1
- package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +1 -0
- package/dist/components/docs/ProductSwitcher.svelte +175 -0
- package/dist/components/docs/ProductSwitcher.svelte.d.ts +28 -0
- package/dist/components/docs/Sidebar.svelte +3 -1
- package/dist/components/docs/Sidebar.svelte.d.ts +1 -0
- package/dist/components/docs/SidebarMenuItems.svelte +16 -8
- package/dist/components/docs/SidebarMenuItems.svelte.d.ts +1 -0
- package/dist/components/docs/TabGroups.svelte +9 -2
- package/dist/components/docs/TabGroups.svelte.d.ts +1 -0
- package/dist/components/docs/VersionSwitcher.svelte +1 -1
- package/dist/components/docs/index.d.ts +1 -0
- package/dist/components/docs/index.js +1 -0
- package/dist/components/ui/Button.svelte.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.schema.json +18 -0
- package/dist/config.server.d.ts +35 -7
- package/dist/config.server.js +185 -23
- package/dist/config.types.d.ts +46 -0
- package/dist/mdx-cache.d.ts +3 -6
- package/dist/mdx-cache.js +36 -8
- package/dist/mdx.d.ts +8 -3
- package/dist/mdx.js +40 -9
- package/dist/redirects.d.ts +2 -1
- package/dist/redirects.js +37 -11
- package/package.json +1 -1
|
@@ -67,6 +67,25 @@
|
|
|
67
67
|
],
|
|
68
68
|
"type": "object"
|
|
69
69
|
},
|
|
70
|
+
"DefaultProductConfig": {
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"description": "Default product configuration in specra.config.json under site.defaultProduct. Used when the default product needs a custom label/icon instead of inheriting from site title.",
|
|
73
|
+
"properties": {
|
|
74
|
+
"activeVersion": {
|
|
75
|
+
"description": "Override active version for the default product",
|
|
76
|
+
"type": "string"
|
|
77
|
+
},
|
|
78
|
+
"icon": {
|
|
79
|
+
"description": "Icon for the default product",
|
|
80
|
+
"type": "string"
|
|
81
|
+
},
|
|
82
|
+
"label": {
|
|
83
|
+
"description": "Display name for the default product in the switcher",
|
|
84
|
+
"type": "string"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"type": "object"
|
|
88
|
+
},
|
|
70
89
|
"DeploymentConfig": {
|
|
71
90
|
"additionalProperties": false,
|
|
72
91
|
"description": "Deployment configuration for different hosting scenarios",
|
|
@@ -410,6 +429,10 @@
|
|
|
410
429
|
"description": "Base URL path for the documentation (e.g., '/docs')",
|
|
411
430
|
"type": "string"
|
|
412
431
|
},
|
|
432
|
+
"defaultProduct": {
|
|
433
|
+
"$ref": "#/definitions/DefaultProductConfig",
|
|
434
|
+
"description": "Configuration for the default product in multi-product mode"
|
|
435
|
+
},
|
|
413
436
|
"description": {
|
|
414
437
|
"description": "Short description of the documentation",
|
|
415
438
|
"type": "string"
|
package/dist/category.d.ts
CHANGED
|
@@ -18,4 +18,4 @@ export declare function getCategoryConfig(folderPath: string): CategoryConfig |
|
|
|
18
18
|
/**
|
|
19
19
|
* Get all category configs for a version
|
|
20
20
|
*/
|
|
21
|
-
export declare function getAllCategoryConfigs(version: string): Map<string, CategoryConfig>;
|
|
21
|
+
export declare function getAllCategoryConfigs(version: string, product?: string): Map<string, CategoryConfig>;
|
package/dist/category.js
CHANGED
|
@@ -22,9 +22,11 @@ export function getCategoryConfig(folderPath) {
|
|
|
22
22
|
/**
|
|
23
23
|
* Get all category configs for a version
|
|
24
24
|
*/
|
|
25
|
-
export function getAllCategoryConfigs(version) {
|
|
25
|
+
export function getAllCategoryConfigs(version, product) {
|
|
26
26
|
const configs = new Map();
|
|
27
|
-
const versionDir =
|
|
27
|
+
const versionDir = (product && product !== "_default_")
|
|
28
|
+
? path.join(DOCS_DIR, product, version)
|
|
29
|
+
: path.join(DOCS_DIR, version);
|
|
28
30
|
if (!fs.existsSync(versionDir)) {
|
|
29
31
|
return configs;
|
|
30
32
|
}
|
|
@@ -6,9 +6,16 @@
|
|
|
6
6
|
version: string;
|
|
7
7
|
slug: string;
|
|
8
8
|
title: string;
|
|
9
|
+
product?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
let { version, slug, title }: Props = $props();
|
|
12
|
+
let { version, slug, title, product }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let docsBase = $derived(
|
|
15
|
+
product && product !== '_default_'
|
|
16
|
+
? `/docs/${product}/${version}`
|
|
17
|
+
: `/docs/${version}`
|
|
18
|
+
);
|
|
12
19
|
|
|
13
20
|
const configStore = getConfigContext();
|
|
14
21
|
let config = $derived($configStore);
|
|
@@ -24,7 +31,7 @@
|
|
|
24
31
|
const potentialLocale = parts[0];
|
|
25
32
|
const isLc = locales.includes(potentialLocale);
|
|
26
33
|
|
|
27
|
-
const homeHref = isLc ?
|
|
34
|
+
const homeHref = isLc ? `${docsBase}/${potentialLocale}` : docsBase;
|
|
28
35
|
|
|
29
36
|
const crumbs: Array<{ label: string; href: string }> = [
|
|
30
37
|
{ label: 'Docs', href: homeHref }
|
|
@@ -45,14 +52,14 @@
|
|
|
45
52
|
label: part
|
|
46
53
|
.replace(/-/g, ' ')
|
|
47
54
|
.replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
48
|
-
href:
|
|
55
|
+
href: `${docsBase}/${currentPath}`
|
|
49
56
|
});
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
// Add current page
|
|
53
60
|
crumbs.push({
|
|
54
61
|
label: title,
|
|
55
|
-
href:
|
|
62
|
+
href: `${docsBase}/${slug}`
|
|
56
63
|
});
|
|
57
64
|
|
|
58
65
|
return crumbs;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
interface Props {
|
|
23
23
|
categoryPath: string;
|
|
24
24
|
version: string;
|
|
25
|
+
product?: string;
|
|
25
26
|
allDocs: DocItem[];
|
|
26
27
|
title?: string;
|
|
27
28
|
description?: string;
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
config: SpecraConfig;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
let { categoryPath, version, allDocs, title, description, content, config }: Props = $props();
|
|
33
|
+
let { categoryPath, version, product, allDocs, title, description, content, config }: Props = $props();
|
|
33
34
|
|
|
34
35
|
// Filter docs that belong to this category (direct children)
|
|
35
36
|
const childDocs = $derived(() => {
|
|
@@ -51,7 +52,11 @@
|
|
|
51
52
|
});
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
const baseUrl = $derived(
|
|
55
|
+
const baseUrl = $derived(
|
|
56
|
+
product && product !== '_default_'
|
|
57
|
+
? `/docs/${product}`
|
|
58
|
+
: (config.site?.baseUrl?.replace(/\/$/, '') || '')
|
|
59
|
+
);
|
|
55
60
|
</script>
|
|
56
61
|
|
|
57
62
|
<div class="space-y-8">
|
|
@@ -32,20 +32,21 @@
|
|
|
32
32
|
nextDoc?: NavDoc;
|
|
33
33
|
version: string;
|
|
34
34
|
slug: string;
|
|
35
|
+
product?: string;
|
|
35
36
|
config: SpecraConfig;
|
|
36
37
|
children: Snippet;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
let { meta, previousDoc, nextDoc, version, slug, config, children }: Props = $props();
|
|
40
|
+
let { meta, previousDoc, nextDoc, version, slug, product, config, children }: Props = $props();
|
|
40
41
|
|
|
41
42
|
let isDevelopment = $derived(dev);
|
|
42
43
|
|
|
43
|
-
// Build edit URL if configured
|
|
44
|
-
let editUrl = $derived(
|
|
45
|
-
config.features?.editUrl
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
44
|
+
// Build edit URL if configured — include product prefix for non-default products
|
|
45
|
+
let editUrl = $derived.by(() => {
|
|
46
|
+
if (!config.features?.editUrl || typeof config.features.editUrl !== 'string') return null;
|
|
47
|
+
const productPrefix = product && product !== '_default_' ? `${product}/` : '';
|
|
48
|
+
return `${config.features.editUrl}/${productPrefix}${version}/${slug}.mdx`;
|
|
49
|
+
});
|
|
49
50
|
</script>
|
|
50
51
|
|
|
51
52
|
<article class="flex-1 min-w-0">
|
|
@@ -103,5 +104,5 @@
|
|
|
103
104
|
</div>
|
|
104
105
|
{/if}
|
|
105
106
|
|
|
106
|
-
<DocNavigation {previousDoc} {nextDoc} {version} />
|
|
107
|
+
<DocNavigation {previousDoc} {nextDoc} {version} {product} />
|
|
107
108
|
</article>
|
|
@@ -10,16 +10,23 @@
|
|
|
10
10
|
previousDoc?: NavDoc;
|
|
11
11
|
nextDoc?: NavDoc;
|
|
12
12
|
version: string;
|
|
13
|
+
product?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
let { previousDoc, nextDoc, version }: Props = $props();
|
|
16
|
+
let { previousDoc, nextDoc, version, product }: Props = $props();
|
|
17
|
+
|
|
18
|
+
let docsBase = $derived(
|
|
19
|
+
product && product !== '_default_'
|
|
20
|
+
? `/docs/${product}/${version}`
|
|
21
|
+
: `/docs/${version}`
|
|
22
|
+
);
|
|
16
23
|
</script>
|
|
17
24
|
|
|
18
25
|
{#if previousDoc || nextDoc}
|
|
19
26
|
<div class="mt-12 pt-8 border-t border-border grid grid-cols-2 gap-4">
|
|
20
27
|
{#if previousDoc}
|
|
21
28
|
<a
|
|
22
|
-
href="
|
|
29
|
+
href="{docsBase}/{previousDoc.slug}"
|
|
23
30
|
class="group flex flex-col gap-2 p-4 rounded-xl border border-border hover:border-primary/50 hover:bg-muted/50 transition-all"
|
|
24
31
|
style="text-decoration: none !important;"
|
|
25
32
|
>
|
|
@@ -37,7 +44,7 @@
|
|
|
37
44
|
|
|
38
45
|
{#if nextDoc}
|
|
39
46
|
<a
|
|
40
|
-
href="
|
|
47
|
+
href="{docsBase}/{nextDoc.slug}"
|
|
41
48
|
class="group flex flex-col gap-2 p-4 rounded-xl border border-border hover:border-primary/50 hover:bg-muted/50 transition-all text-right"
|
|
42
49
|
style="text-decoration: none !important;"
|
|
43
50
|
>
|
|
@@ -3,6 +3,7 @@
|
|
|
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 ProductSwitcher from './ProductSwitcher.svelte';
|
|
6
7
|
import VersionBanner from './VersionBanner.svelte';
|
|
7
8
|
import ThemeToggle from './ThemeToggle.svelte';
|
|
8
9
|
import SearchModal from './SearchModal.svelte';
|
|
@@ -20,16 +21,27 @@
|
|
|
20
21
|
hidden?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
interface ProductItem {
|
|
25
|
+
slug: string;
|
|
26
|
+
label: string;
|
|
27
|
+
icon?: string;
|
|
28
|
+
badge?: string;
|
|
29
|
+
activeVersion?: string;
|
|
30
|
+
isDefault: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
interface Props {
|
|
24
34
|
currentVersion: string;
|
|
25
35
|
versions: string[];
|
|
26
36
|
versionsMeta?: VersionMeta[];
|
|
27
37
|
versionBanner?: BannerConfig;
|
|
28
38
|
config?: SpecraConfig;
|
|
39
|
+
products?: ProductItem[];
|
|
40
|
+
currentProduct?: string;
|
|
29
41
|
subheader?: Snippet;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
let { currentVersion, versions, versionsMeta, versionBanner, config: configProp, subheader }: Props = $props();
|
|
44
|
+
let { currentVersion, versions = [], versionsMeta, versionBanner, config: configProp, products, currentProduct, subheader }: Props = $props();
|
|
33
45
|
|
|
34
46
|
const configStore = getConfigContext();
|
|
35
47
|
let config = $derived(configProp || $configStore);
|
|
@@ -115,6 +127,10 @@
|
|
|
115
127
|
</button>
|
|
116
128
|
{/if}
|
|
117
129
|
|
|
130
|
+
{#if products && products.length > 1}
|
|
131
|
+
<ProductSwitcher {products} {currentProduct} />
|
|
132
|
+
{/if}
|
|
133
|
+
|
|
118
134
|
{#if config.features?.versioning}
|
|
119
135
|
<VersionSwitcher {currentVersion} {versions} {versionsMeta} />
|
|
120
136
|
{/if}
|
|
@@ -7,12 +7,22 @@ interface VersionMeta {
|
|
|
7
7
|
badge?: string;
|
|
8
8
|
hidden?: boolean;
|
|
9
9
|
}
|
|
10
|
+
interface ProductItem {
|
|
11
|
+
slug: string;
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: string;
|
|
14
|
+
badge?: string;
|
|
15
|
+
activeVersion?: string;
|
|
16
|
+
isDefault: boolean;
|
|
17
|
+
}
|
|
10
18
|
interface Props {
|
|
11
19
|
currentVersion: string;
|
|
12
20
|
versions: string[];
|
|
13
21
|
versionsMeta?: VersionMeta[];
|
|
14
22
|
versionBanner?: BannerConfig;
|
|
15
23
|
config?: SpecraConfig;
|
|
24
|
+
products?: ProductItem[];
|
|
25
|
+
currentProduct?: string;
|
|
16
26
|
subheader?: Snippet;
|
|
17
27
|
}
|
|
18
28
|
declare const Header: import("svelte").Component<Props, {}, "">;
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
interface Props {
|
|
34
34
|
docs: DocItem[];
|
|
35
35
|
version: string;
|
|
36
|
+
product?: string;
|
|
36
37
|
config: SpecraConfig;
|
|
37
38
|
activeTabGroup?: string;
|
|
38
39
|
onTabChange?: (tabId: string) => void;
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
children: Snippet;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
let { docs, version, config, activeTabGroup, onTabChange, header, toc, children }: Props = $props();
|
|
45
|
+
let { docs = [], version, product, config, activeTabGroup, onTabChange, header, toc, children }: Props = $props();
|
|
45
46
|
|
|
46
47
|
let sidebarOpen = $derived($sidebarStore);
|
|
47
48
|
let isFlush = $derived(config.navigation?.sidebarStyle === 'flush');
|
|
@@ -126,6 +127,7 @@
|
|
|
126
127
|
<SidebarMenuItems
|
|
127
128
|
{docs}
|
|
128
129
|
{version}
|
|
130
|
+
{product}
|
|
129
131
|
{config}
|
|
130
132
|
onLinkClick={closeSidebar}
|
|
131
133
|
{activeTabGroup}
|
|
@@ -142,6 +144,7 @@
|
|
|
142
144
|
<Sidebar
|
|
143
145
|
{docs}
|
|
144
146
|
{version}
|
|
147
|
+
{product}
|
|
145
148
|
{config}
|
|
146
149
|
{activeTabGroup}
|
|
147
150
|
/>
|
|
@@ -174,6 +177,7 @@
|
|
|
174
177
|
<Sidebar
|
|
175
178
|
{docs}
|
|
176
179
|
{version}
|
|
180
|
+
{product}
|
|
177
181
|
{config}
|
|
178
182
|
{activeTabGroup}
|
|
179
183
|
/>
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
interface Props {
|
|
30
30
|
docs: DocItem[];
|
|
31
31
|
version: string;
|
|
32
|
+
product?: string;
|
|
32
33
|
config: SpecraConfig;
|
|
33
34
|
activeTabGroup?: string;
|
|
34
35
|
onTabChange?: (tabId: string) => void;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
let { docs, version, config, activeTabGroup, onTabChange }: Props = $props();
|
|
38
|
+
let { docs = [], version, product, config, activeTabGroup, onTabChange }: Props = $props();
|
|
38
39
|
|
|
39
40
|
let isOpen = $derived($sidebarStore);
|
|
40
41
|
|
|
@@ -113,6 +114,7 @@
|
|
|
113
114
|
<SidebarMenuItems
|
|
114
115
|
{docs}
|
|
115
116
|
{version}
|
|
117
|
+
{product}
|
|
116
118
|
{config}
|
|
117
119
|
onLinkClick={handleClose}
|
|
118
120
|
{activeTabGroup}
|
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
header: Snippet;
|
|
32
32
|
docs: DocItem[];
|
|
33
33
|
version: string;
|
|
34
|
+
product?: string;
|
|
34
35
|
config: SpecraConfig;
|
|
35
36
|
activeTabGroup?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
let { header, docs, version, config, activeTabGroup }: Props = $props();
|
|
39
|
+
let { header, docs, version, product, config, activeTabGroup }: Props = $props();
|
|
39
40
|
|
|
40
41
|
let sidebarOpen = $derived($sidebarStore);
|
|
41
42
|
|
|
@@ -113,6 +114,7 @@
|
|
|
113
114
|
<SidebarMenuItems
|
|
114
115
|
{docs}
|
|
115
116
|
{version}
|
|
117
|
+
{product}
|
|
116
118
|
{config}
|
|
117
119
|
onLinkClick={closeSidebar}
|
|
118
120
|
{activeTabGroup}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronDown, Check } from 'lucide-svelte';
|
|
3
|
+
import { goto } from '$app/navigation';
|
|
4
|
+
import { browser } from '$app/environment';
|
|
5
|
+
import Icon from './Icon.svelte';
|
|
6
|
+
|
|
7
|
+
/** Flat shape (from page components) */
|
|
8
|
+
interface ProductItemFlat {
|
|
9
|
+
slug: string;
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
badge?: string;
|
|
13
|
+
activeVersion?: string;
|
|
14
|
+
isDefault: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Nested shape (from SDK getProducts()) */
|
|
18
|
+
interface ProductItemNested {
|
|
19
|
+
slug: string;
|
|
20
|
+
config: {
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
badge?: string;
|
|
24
|
+
activeVersion?: string;
|
|
25
|
+
};
|
|
26
|
+
isDefault: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ProductInput = ProductItemFlat | ProductItemNested;
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
products: ProductInput[];
|
|
33
|
+
currentProduct?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let { products, currentProduct }: Props = $props();
|
|
37
|
+
|
|
38
|
+
/** Normalize either shape to flat */
|
|
39
|
+
function normalize(p: ProductInput): ProductItemFlat {
|
|
40
|
+
if ('config' in p && p.config) {
|
|
41
|
+
return {
|
|
42
|
+
slug: p.slug,
|
|
43
|
+
label: p.config.label,
|
|
44
|
+
icon: p.config.icon,
|
|
45
|
+
badge: p.config.badge,
|
|
46
|
+
activeVersion: p.config.activeVersion,
|
|
47
|
+
isDefault: p.isDefault,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return p as ProductItemFlat;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let normalizedProducts = $derived(products.map(normalize));
|
|
54
|
+
|
|
55
|
+
let isOpen = $state(false);
|
|
56
|
+
let dropdownEl = $state<HTMLDivElement | null>(null);
|
|
57
|
+
|
|
58
|
+
let currentLabel = $derived.by(() => {
|
|
59
|
+
const product = normalizedProducts.find(p =>
|
|
60
|
+
currentProduct ? p.slug === currentProduct : p.isDefault
|
|
61
|
+
);
|
|
62
|
+
return product?.label || 'Docs';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let currentIcon = $derived.by(() => {
|
|
66
|
+
const product = normalizedProducts.find(p =>
|
|
67
|
+
currentProduct ? p.slug === currentProduct : p.isDefault
|
|
68
|
+
);
|
|
69
|
+
return product?.icon;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
$effect(() => {
|
|
73
|
+
if (!browser || !isOpen) return;
|
|
74
|
+
|
|
75
|
+
function handleClickOutside(e: MouseEvent) {
|
|
76
|
+
if (dropdownEl && !dropdownEl.contains(e.target as Node)) {
|
|
77
|
+
isOpen = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleEscape(e: KeyboardEvent) {
|
|
82
|
+
if (e.key === 'Escape') {
|
|
83
|
+
isOpen = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
document.addEventListener('click', handleClickOutside);
|
|
88
|
+
document.addEventListener('keydown', handleEscape);
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
document.removeEventListener('click', handleClickOutside);
|
|
92
|
+
document.removeEventListener('keydown', handleEscape);
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
function switchProduct(product: ProductItemFlat) {
|
|
97
|
+
const isCurrentProduct = currentProduct
|
|
98
|
+
? product.slug === currentProduct
|
|
99
|
+
: product.isDefault;
|
|
100
|
+
|
|
101
|
+
if (isCurrentProduct) {
|
|
102
|
+
isOpen = false;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
isOpen = false;
|
|
107
|
+
|
|
108
|
+
const version = product.activeVersion || 'v1.0.0';
|
|
109
|
+
if (product.isDefault) {
|
|
110
|
+
goto(`/docs/${version}`);
|
|
111
|
+
} else {
|
|
112
|
+
goto(`/docs/${product.slug}/${version}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isCurrentProductItem(product: ProductItemFlat): boolean {
|
|
117
|
+
return currentProduct
|
|
118
|
+
? product.slug === currentProduct
|
|
119
|
+
: product.isDefault;
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
{#if products.length > 1}
|
|
124
|
+
<div class="relative" bind:this={dropdownEl}>
|
|
125
|
+
<button
|
|
126
|
+
onclick={() => (isOpen = !isOpen)}
|
|
127
|
+
class="flex items-center gap-1.5 h-8 px-3 text-sm font-medium rounded-md border border-border bg-background hover:bg-accent transition-colors text-foreground"
|
|
128
|
+
aria-expanded={isOpen}
|
|
129
|
+
aria-haspopup="listbox"
|
|
130
|
+
aria-label="Switch product"
|
|
131
|
+
>
|
|
132
|
+
{#if currentIcon}
|
|
133
|
+
<Icon icon={currentIcon} size={14} />
|
|
134
|
+
{/if}
|
|
135
|
+
<span>{currentLabel}</span>
|
|
136
|
+
<ChevronDown class="h-3.5 w-3.5 text-muted-foreground transition-transform {isOpen ? 'rotate-180' : ''}" />
|
|
137
|
+
</button>
|
|
138
|
+
|
|
139
|
+
{#if isOpen}
|
|
140
|
+
<div
|
|
141
|
+
class="absolute top-full left-0 mt-1 w-56 py-1 bg-popover border border-border rounded-md shadow-lg z-50"
|
|
142
|
+
role="listbox"
|
|
143
|
+
aria-label="Available products"
|
|
144
|
+
>
|
|
145
|
+
{#each normalizedProducts as product}
|
|
146
|
+
<button
|
|
147
|
+
role="option"
|
|
148
|
+
aria-selected={isCurrentProductItem(product)}
|
|
149
|
+
onclick={() => switchProduct(product)}
|
|
150
|
+
class="w-full flex items-center justify-between px-3 py-2 text-sm transition-colors {isCurrentProductItem(product)
|
|
151
|
+
? 'text-primary bg-accent/50 font-medium'
|
|
152
|
+
: 'text-foreground hover:bg-accent'}"
|
|
153
|
+
>
|
|
154
|
+
<span class="flex items-center gap-2">
|
|
155
|
+
{#if product.icon}
|
|
156
|
+
<Icon icon={product.icon} size={14} />
|
|
157
|
+
{/if}
|
|
158
|
+
<span class="flex flex-col items-start">
|
|
159
|
+
<span class="flex items-center gap-1.5">
|
|
160
|
+
{product.label}
|
|
161
|
+
{#if product.badge}
|
|
162
|
+
<span class="px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-primary/10 text-primary leading-none">{product.badge}</span>
|
|
163
|
+
{/if}
|
|
164
|
+
</span>
|
|
165
|
+
</span>
|
|
166
|
+
</span>
|
|
167
|
+
{#if isCurrentProductItem(product)}
|
|
168
|
+
<Check class="h-3.5 w-3.5 text-primary" />
|
|
169
|
+
{/if}
|
|
170
|
+
</button>
|
|
171
|
+
{/each}
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Flat shape (from page components) */
|
|
2
|
+
interface ProductItemFlat {
|
|
3
|
+
slug: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: string;
|
|
6
|
+
badge?: string;
|
|
7
|
+
activeVersion?: string;
|
|
8
|
+
isDefault: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Nested shape (from SDK getProducts()) */
|
|
11
|
+
interface ProductItemNested {
|
|
12
|
+
slug: string;
|
|
13
|
+
config: {
|
|
14
|
+
label: string;
|
|
15
|
+
icon?: string;
|
|
16
|
+
badge?: string;
|
|
17
|
+
activeVersion?: string;
|
|
18
|
+
};
|
|
19
|
+
isDefault: boolean;
|
|
20
|
+
}
|
|
21
|
+
type ProductInput = ProductItemFlat | ProductItemNested;
|
|
22
|
+
interface Props {
|
|
23
|
+
products: ProductInput[];
|
|
24
|
+
currentProduct?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const ProductSwitcher: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type ProductSwitcher = ReturnType<typeof ProductSwitcher>;
|
|
28
|
+
export default ProductSwitcher;
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
interface Props {
|
|
27
27
|
docs: DocItem[];
|
|
28
28
|
version: string;
|
|
29
|
+
product?: string;
|
|
29
30
|
onLinkClick?: () => void;
|
|
30
31
|
config: SpecraConfig;
|
|
31
32
|
activeTabGroup?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
let { docs, version, onLinkClick, config, activeTabGroup }: Props = $props();
|
|
35
|
+
let { docs = [], version, product, onLinkClick, config, activeTabGroup }: Props = $props();
|
|
35
36
|
|
|
36
37
|
let isFlush = $derived(config.navigation?.sidebarStyle === 'flush');
|
|
37
38
|
let containerClass = $derived(
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
<SidebarMenuItems
|
|
54
55
|
{docs}
|
|
55
56
|
{version}
|
|
57
|
+
{product}
|
|
56
58
|
{onLinkClick}
|
|
57
59
|
{config}
|
|
58
60
|
{activeTabGroup}
|