specra 0.2.5 → 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.
Files changed (44) hide show
  1. package/config/specra.config.schema.json +23 -0
  2. package/dist/category.d.ts +1 -1
  3. package/dist/category.js +4 -2
  4. package/dist/components/docs/Breadcrumb.svelte +11 -4
  5. package/dist/components/docs/Breadcrumb.svelte.d.ts +1 -0
  6. package/dist/components/docs/CategoryIndex.svelte +7 -2
  7. package/dist/components/docs/CategoryIndex.svelte.d.ts +1 -0
  8. package/dist/components/docs/DocLayout.svelte +9 -8
  9. package/dist/components/docs/DocLayout.svelte.d.ts +1 -0
  10. package/dist/components/docs/DocNavigation.svelte +10 -3
  11. package/dist/components/docs/DocNavigation.svelte.d.ts +1 -0
  12. package/dist/components/docs/Header.svelte +17 -1
  13. package/dist/components/docs/Header.svelte.d.ts +10 -0
  14. package/dist/components/docs/MobileDocLayout.svelte +5 -1
  15. package/dist/components/docs/MobileDocLayout.svelte.d.ts +1 -0
  16. package/dist/components/docs/MobileSidebar.svelte +3 -1
  17. package/dist/components/docs/MobileSidebar.svelte.d.ts +1 -0
  18. package/dist/components/docs/MobileSidebarWrapper.svelte +3 -1
  19. package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +1 -0
  20. package/dist/components/docs/ProductSwitcher.svelte +175 -0
  21. package/dist/components/docs/ProductSwitcher.svelte.d.ts +28 -0
  22. package/dist/components/docs/Sidebar.svelte +3 -1
  23. package/dist/components/docs/Sidebar.svelte.d.ts +1 -0
  24. package/dist/components/docs/SidebarMenuItems.svelte +16 -8
  25. package/dist/components/docs/SidebarMenuItems.svelte.d.ts +1 -0
  26. package/dist/components/docs/TabGroups.svelte +9 -2
  27. package/dist/components/docs/TabGroups.svelte.d.ts +1 -0
  28. package/dist/components/docs/VersionSwitcher.svelte +1 -1
  29. package/dist/components/docs/index.d.ts +1 -0
  30. package/dist/components/docs/index.js +1 -0
  31. package/dist/components/ui/Button.svelte.d.ts +1 -1
  32. package/dist/config.d.ts +1 -1
  33. package/dist/config.schema.json +18 -0
  34. package/dist/config.server.d.ts +35 -7
  35. package/dist/config.server.js +185 -23
  36. package/dist/config.types.d.ts +46 -0
  37. package/dist/mdx-cache.d.ts +3 -6
  38. package/dist/mdx-cache.js +36 -8
  39. package/dist/mdx.d.ts +8 -3
  40. package/dist/mdx.js +40 -9
  41. package/dist/redirects.d.ts +2 -1
  42. package/dist/redirects.js +37 -11
  43. package/dist/styles/globals.css +2 -1
  44. 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"
@@ -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 = path.join(DOCS_DIR, version);
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 ? `/docs/${version}/${potentialLocale}` : `/docs/${version}`;
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: `/docs/${version}/${currentPath}`
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: `/docs/${version}/${slug}`
62
+ href: `${docsBase}/${slug}`
56
63
  });
57
64
 
58
65
  return crumbs;
@@ -2,6 +2,7 @@ interface Props {
2
2
  version: string;
3
3
  slug: string;
4
4
  title: string;
5
+ product?: string;
5
6
  }
6
7
  declare const Breadcrumb: import("svelte").Component<Props, {}, "">;
7
8
  type Breadcrumb = ReturnType<typeof Breadcrumb>;
@@ -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(config.site?.baseUrl?.replace(/\/$/, '') || '');
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">
@@ -18,6 +18,7 @@ interface DocItem {
18
18
  interface Props {
19
19
  categoryPath: string;
20
20
  version: string;
21
+ product?: string;
21
22
  allDocs: DocItem[];
22
23
  title?: string;
23
24
  description?: string;
@@ -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 && typeof config.features.editUrl === 'string'
46
- ? `${config.features.editUrl}/${version}/${slug}.mdx`
47
- : null
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>
@@ -24,6 +24,7 @@ interface Props {
24
24
  nextDoc?: NavDoc;
25
25
  version: string;
26
26
  slug: string;
27
+ product?: string;
27
28
  config: SpecraConfig;
28
29
  children: Snippet;
29
30
  }
@@ -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="/docs/{version}/{previousDoc.slug}"
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="/docs/{version}/{nextDoc.slug}"
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
  >
@@ -6,6 +6,7 @@ interface Props {
6
6
  previousDoc?: NavDoc;
7
7
  nextDoc?: NavDoc;
8
8
  version: string;
9
+ product?: string;
9
10
  }
10
11
  declare const DocNavigation: import("svelte").Component<Props, {}, "">;
11
12
  type DocNavigation = ReturnType<typeof DocNavigation>;
@@ -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
  />
@@ -23,6 +23,7 @@ interface DocItem {
23
23
  interface Props {
24
24
  docs: DocItem[];
25
25
  version: string;
26
+ product?: string;
26
27
  config: SpecraConfig;
27
28
  activeTabGroup?: string;
28
29
  onTabChange?: (tabId: string) => void;
@@ -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}
@@ -22,6 +22,7 @@ interface DocItem {
22
22
  interface Props {
23
23
  docs: DocItem[];
24
24
  version: string;
25
+ product?: string;
25
26
  config: SpecraConfig;
26
27
  activeTabGroup?: string;
27
28
  onTabChange?: (tabId: string) => void;
@@ -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}
@@ -24,6 +24,7 @@ interface Props {
24
24
  header: Snippet;
25
25
  docs: DocItem[];
26
26
  version: string;
27
+ product?: string;
27
28
  config: SpecraConfig;
28
29
  activeTabGroup?: string;
29
30
  }
@@ -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}
@@ -22,6 +22,7 @@ interface DocItem {
22
22
  interface Props {
23
23
  docs: DocItem[];
24
24
  version: string;
25
+ product?: string;
25
26
  onLinkClick?: () => void;
26
27
  config: SpecraConfig;
27
28
  activeTabGroup?: string;