specra 0.2.6 → 0.2.8

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 (49) hide show
  1. package/config/specra.config.schema.json +23 -0
  2. package/config/svelte-config.js +32 -1
  3. package/dist/category.d.ts +1 -1
  4. package/dist/category.js +4 -2
  5. package/dist/components/docs/Breadcrumb.svelte +11 -4
  6. package/dist/components/docs/Breadcrumb.svelte.d.ts +1 -0
  7. package/dist/components/docs/CategoryIndex.svelte +11 -2
  8. package/dist/components/docs/CategoryIndex.svelte.d.ts +1 -0
  9. package/dist/components/docs/DocLayout.svelte +9 -8
  10. package/dist/components/docs/DocLayout.svelte.d.ts +1 -0
  11. package/dist/components/docs/DocNavigation.svelte +10 -3
  12. package/dist/components/docs/DocNavigation.svelte.d.ts +1 -0
  13. package/dist/components/docs/Header.svelte +17 -1
  14. package/dist/components/docs/Header.svelte.d.ts +10 -0
  15. package/dist/components/docs/MdxContent.svelte +3 -1
  16. package/dist/components/docs/MobileDocLayout.svelte +5 -1
  17. package/dist/components/docs/MobileDocLayout.svelte.d.ts +1 -0
  18. package/dist/components/docs/MobileSidebar.svelte +3 -1
  19. package/dist/components/docs/MobileSidebar.svelte.d.ts +1 -0
  20. package/dist/components/docs/MobileSidebarWrapper.svelte +3 -1
  21. package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +1 -0
  22. package/dist/components/docs/ProductSwitcher.svelte +175 -0
  23. package/dist/components/docs/ProductSwitcher.svelte.d.ts +28 -0
  24. package/dist/components/docs/SearchModal.svelte +4 -3
  25. package/dist/components/docs/Sidebar.svelte +3 -1
  26. package/dist/components/docs/Sidebar.svelte.d.ts +1 -0
  27. package/dist/components/docs/SidebarMenuItems.svelte +39 -9
  28. package/dist/components/docs/SidebarMenuItems.svelte.d.ts +1 -0
  29. package/dist/components/docs/TabGroups.svelte +9 -2
  30. package/dist/components/docs/TabGroups.svelte.d.ts +1 -0
  31. package/dist/components/docs/VersionSwitcher.svelte +1 -1
  32. package/dist/components/docs/api/ApiParams.svelte +94 -36
  33. package/dist/components/docs/index.d.ts +1 -0
  34. package/dist/components/docs/index.js +1 -0
  35. package/dist/components/ui/Button.svelte +30 -11
  36. package/dist/components/ui/Button.svelte.d.ts +10 -4
  37. package/dist/config.d.ts +1 -1
  38. package/dist/config.schema.json +18 -0
  39. package/dist/config.server.d.ts +35 -7
  40. package/dist/config.server.js +185 -23
  41. package/dist/config.types.d.ts +46 -0
  42. package/dist/mdx-cache.d.ts +3 -6
  43. package/dist/mdx-cache.js +36 -8
  44. package/dist/mdx.d.ts +8 -3
  45. package/dist/mdx.js +117 -10
  46. package/dist/parsers/openapi-parser.js +6 -1
  47. package/dist/redirects.d.ts +2 -1
  48. package/dist/redirects.js +37 -11
  49. package/package.json +1 -1
@@ -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;
@@ -28,7 +28,8 @@
28
28
  let inputEl = $state<HTMLInputElement | null>(null);
29
29
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
30
30
 
31
- const baseUrl = $derived(config.site?.baseUrl || '/');
31
+ const siteBaseUrl = $derived(config.site?.baseUrl || '/');
32
+ const docsBaseUrl = '/docs';
32
33
 
33
34
  $effect(() => {
34
35
  if (isOpen && inputEl) {
@@ -75,7 +76,7 @@
75
76
  debounceTimer = setTimeout(async () => {
76
77
  try {
77
78
  const response = await fetch(
78
- `${baseUrl.replace(/\/$/, '')}/api/search?q=${encodeURIComponent(value.trim())}`
79
+ `${siteBaseUrl.replace(/\/$/, '')}/api/search?q=${encodeURIComponent(value.trim())}`
79
80
  );
80
81
  if (response.ok) {
81
82
  const data = await response.json();
@@ -93,7 +94,7 @@
93
94
 
94
95
  function navigateToResult(result: SearchResult) {
95
96
  const version = result.version || config.site?.activeVersion || 'v1';
96
- const url = `${baseUrl.replace(/\/$/, '')}/${version}/${result.slug}`;
97
+ const url = `${docsBaseUrl}/${version}/${result.slug}`;
97
98
  goto(url);
98
99
  onClose();
99
100
  }
@@ -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;
@@ -42,14 +42,43 @@
42
42
  interface Props {
43
43
  docs: DocItem[];
44
44
  version: string;
45
+ product?: string;
45
46
  onLinkClick?: () => void;
46
47
  config: SpecraConfig;
47
48
  activeTabGroup?: string;
48
49
  }
49
50
 
50
- let { docs, version, onLinkClick, config, activeTabGroup }: Props = $props();
51
+ let { docs = [], version, product, onLinkClick, config, activeTabGroup }: Props = $props();
51
52
 
52
- let collapsed: Record<string, boolean> = $state({});
53
+ /** URL prefix: /docs/{product}/{version} for named products, /docs/{version} for default */
54
+ let docsBase = $derived(
55
+ product && product !== '_default_'
56
+ ? `/docs/${product}/${version}`
57
+ : `/docs/${version}`
58
+ );
59
+
60
+ const STORAGE_KEY = 'specra-sidebar-collapsed';
61
+
62
+ function loadCollapsedState(): Record<string, boolean> {
63
+ if (typeof window === 'undefined') return {};
64
+ try {
65
+ const stored = localStorage.getItem(STORAGE_KEY);
66
+ return stored ? JSON.parse(stored) : {};
67
+ } catch {
68
+ return {};
69
+ }
70
+ }
71
+
72
+ function saveCollapsedState(state: Record<string, boolean>) {
73
+ if (typeof window === 'undefined') return;
74
+ try {
75
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
76
+ } catch {
77
+ // localStorage unavailable
78
+ }
79
+ }
80
+
81
+ let collapsed: Record<string, boolean> = $state(loadCollapsedState());
53
82
  let pathname = $derived($page.url.pathname.replace(/\/$/, ''));
54
83
 
55
84
  // Filter docs by active tab group if tab groups are configured
@@ -172,18 +201,19 @@
172
201
 
173
202
  function toggleSection(section: string) {
174
203
  collapsed = { ...collapsed, [section]: !collapsed[section] };
204
+ saveCollapsedState(collapsed);
175
205
  }
176
206
 
177
207
  function isActiveInGroup(group: SidebarGroup): boolean {
178
208
  const hasActiveItem = group.items.some(
179
- (doc) => pathname === `/docs/${version}/${doc.slug}`
209
+ (doc) => pathname === `${docsBase}/${doc.slug}`
180
210
  );
181
211
  if (hasActiveItem) return true;
182
212
  return Object.values(group.children).some((child) => isActiveInGroup(child));
183
213
  }
184
214
 
185
215
  function getGroupHref(group: SidebarGroup): string {
186
- let groupHref = `/docs/${version}/${group.path}`;
216
+ let groupHref = `${docsBase}/${group.path}`;
187
217
 
188
218
  if (config.features?.i18n) {
189
219
  const i18n = config.features.i18n;
@@ -192,7 +222,7 @@
192
222
  const potentialLocale = pathParts[3];
193
223
 
194
224
  if (potentialLocale && locales.includes(potentialLocale)) {
195
- groupHref = `/docs/${version}/${potentialLocale}/${group.path}`;
225
+ groupHref = `${docsBase}/${potentialLocale}/${group.path}`;
196
226
  }
197
227
  }
198
228
 
@@ -201,7 +231,7 @@
201
231
 
202
232
  function isGroupCollapsed(groupKey: string, group: SidebarGroup): boolean {
203
233
  const hasActive = isActiveInGroup(group);
204
- const isGroupActive = pathname === `/docs/${version}/${group.path}`;
234
+ const isGroupActive = pathname === `${docsBase}/${group.path}`;
205
235
  if (hasActive || isGroupActive) return false;
206
236
  return collapsed[groupKey] ?? group.defaultCollapsed;
207
237
  }
@@ -240,7 +270,7 @@
240
270
  {@const hasChildren = sortedChildren.length > 0}
241
271
  {@const hasItems = sortedItems.length > 0}
242
272
  {@const hasContent = hasChildren || hasItems}
243
- {@const isGroupActive = pathname === `/docs/${version}/${group.path}`}
273
+ {@const isGroupActive = pathname === `${docsBase}/${group.path}`}
244
274
  {@const isCollapsed = isGroupCollapsed(groupKey, group)}
245
275
  {@const marginLeft = depth > 0 ? 'ml-4' : ''}
246
276
  {@const groupHref = getGroupHref(group)}
@@ -289,7 +319,7 @@
289
319
  {#if item.type === 'group'}
290
320
  {@render renderGroup(`${groupKey}/${item.key}`, item.group, depth + 1)}
291
321
  {:else}
292
- {@const href = `/docs/${version}/${item.doc.slug}`}
322
+ {@const href = `${docsBase}/${item.doc.slug}`}
293
323
  {@const isActive = pathname === href}
294
324
  <a
295
325
  {href}
@@ -316,7 +346,7 @@
316
346
  <nav class="space-y-1">
317
347
  {#if sortedStandalone.length > 0}
318
348
  {#each sortedStandalone as doc (doc.slug)}
319
- {@const href = `/docs/${version}/${doc.slug}`}
349
+ {@const href = `${docsBase}/${doc.slug}`}
320
350
  {@const isActive = pathname === href}
321
351
  <a
322
352
  {href}
@@ -24,6 +24,7 @@ interface DocItem {
24
24
  interface Props {
25
25
  docs: DocItem[];
26
26
  version: string;
27
+ product?: string;
27
28
  onLinkClick?: () => void;
28
29
  config: SpecraConfig;
29
30
  activeTabGroup?: string;
@@ -24,10 +24,17 @@
24
24
  mobileOnly?: boolean;
25
25
  docs?: DocItem[];
26
26
  version?: string;
27
+ product?: string;
27
28
  flush?: boolean;
28
29
  }
29
30
 
30
- let { tabGroups, activeTabId, onTabChange, mobileOnly = false, docs, version, flush = false }: Props = $props();
31
+ let { tabGroups, activeTabId, onTabChange, mobileOnly = false, docs, version, product, flush = false }: Props = $props();
32
+
33
+ let docsBase = $derived(
34
+ product && product !== '_default_' && version
35
+ ? `/docs/${product}/${version}`
36
+ : version ? `/docs/${version}` : '/docs'
37
+ );
31
38
 
32
39
  let dropdownOpen = $state(false);
33
40
 
@@ -58,7 +65,7 @@
58
65
  });
59
66
 
60
67
  if (firstDocInTab) {
61
- goto(`/docs/${version}/${firstDocInTab.slug}`);
68
+ goto(`${docsBase}/${firstDocInTab.slug}`);
62
69
  }
63
70
  }
64
71
  }
@@ -18,6 +18,7 @@ interface Props {
18
18
  mobileOnly?: boolean;
19
19
  docs?: DocItem[];
20
20
  version?: string;
21
+ product?: string;
21
22
  flush?: boolean;
22
23
  }
23
24
  declare const TabGroups: import("svelte").Component<Props, {}, "">;
@@ -27,7 +27,7 @@
27
27
  if (versionsMeta && versionsMeta.length > 0) {
28
28
  return versionsMeta.filter(v => !v.hidden);
29
29
  }
30
- return versions.map(id => ({ id, label: id }));
30
+ return (versions ?? []).map(id => ({ id, label: id }));
31
31
  });
32
32
 
33
33
  // Get current version display label
@@ -13,62 +13,54 @@
13
13
  }
14
14
 
15
15
  let { title = 'Parameters', params }: Props = $props();
16
+
17
+ let filteredParams = $derived(
18
+ Array.isArray(params) ? params.filter(p => p && p.name) : []
19
+ );
16
20
  </script>
17
21
 
18
- {#if params && params.length > 0}
22
+ {#if filteredParams.length > 0}
19
23
  <div class="mb-6">
20
24
  <h4 class="text-sm font-semibold text-foreground mb-3">{title}</h4>
21
- <div class="overflow-x-auto">
22
- <table class="w-full border-collapse">
25
+ <div class="specra-params-table">
26
+ <table>
23
27
  <thead>
24
- <tr class="border-b border-border">
25
- <th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
26
- Property
27
- </th>
28
- <th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
29
- Type
30
- </th>
31
- <th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
32
- Required
33
- </th>
34
- <th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
35
- Default
36
- </th>
37
- <th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
38
- Description
39
- </th>
28
+ <tr>
29
+ <th>Property</th>
30
+ <th>Type</th>
31
+ <th>Required</th>
32
+ <th>Default</th>
33
+ <th>Description</th>
40
34
  </tr>
41
35
  </thead>
42
36
  <tbody>
43
- {#each params as param, index}
44
- <tr
45
- class={index !== params.length - 1 ? 'border-b border-border/50' : ''}
46
- >
47
- <td class="py-2.5 px-3">
48
- <code class="text-sm font-mono text-foreground">{param.name}</code>
37
+ {#each filteredParams as param, index}
38
+ <tr>
39
+ <td>
40
+ <code>{param.name}</code>
49
41
  </td>
50
- <td class="py-2.5 px-3">
51
- <span class="text-sm text-muted-foreground font-mono">{param.type}</span>
42
+ <td>
43
+ <span class="font-mono">{param.type}</span>
52
44
  </td>
53
- <td class="py-2.5 px-3">
45
+ <td>
54
46
  {#if param.required}
55
- <span class="text-sm text-red-600 dark:text-red-400">Yes</span>
47
+ <span class="text-red-600 dark:text-red-400">Yes</span>
56
48
  {:else}
57
- <span class="text-sm text-muted-foreground">No</span>
49
+ No
58
50
  {/if}
59
51
  </td>
60
- <td class="py-2.5 px-3">
52
+ <td>
61
53
  {#if param.default}
62
- <code class="text-sm font-mono text-muted-foreground">{param.default}</code>
54
+ <code>{param.default}</code>
63
55
  {:else}
64
- <span class="text-sm text-muted-foreground">-</span>
56
+ -
65
57
  {/if}
66
58
  </td>
67
- <td class="py-2.5 px-3">
59
+ <td>
68
60
  {#if param.description}
69
- <span class="text-sm text-muted-foreground">{param.description}</span>
61
+ {param.description}
70
62
  {:else}
71
- <span class="text-sm text-muted-foreground">-</span>
63
+ -
72
64
  {/if}
73
65
  </td>
74
66
  </tr>
@@ -78,3 +70,69 @@
78
70
  </div>
79
71
  </div>
80
72
  {/if}
73
+
74
+ <style>
75
+ .specra-params-table table {
76
+ border-collapse: separate;
77
+ border-spacing: 0;
78
+ border: 1px solid var(--border);
79
+ border-radius: 0.75rem;
80
+ overflow: hidden;
81
+ width: max-content;
82
+ max-width: 100%;
83
+ display: block;
84
+ overflow-x: auto;
85
+ -webkit-overflow-scrolling: touch;
86
+ }
87
+
88
+ .specra-params-table thead {
89
+ background: var(--muted);
90
+ }
91
+
92
+ .specra-params-table thead th {
93
+ border-bottom: 1px solid var(--border);
94
+ border-right: 1px solid var(--border);
95
+ padding: 0.625rem 1rem;
96
+ font-weight: 600;
97
+ font-size: 0.75rem;
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.05em;
100
+ text-align: left;
101
+ color: var(--muted-foreground);
102
+ white-space: nowrap;
103
+ }
104
+
105
+ .specra-params-table thead th:last-child {
106
+ border-right: none;
107
+ }
108
+
109
+ .specra-params-table tbody td {
110
+ border-bottom: 1px solid var(--border);
111
+ border-right: 1px solid var(--border);
112
+ padding: 0.625rem 1rem;
113
+ font-size: 0.875rem;
114
+ color: var(--muted-foreground);
115
+ }
116
+
117
+ .specra-params-table tbody td:last-child {
118
+ border-right: none;
119
+ }
120
+
121
+ .specra-params-table tbody tr:last-child td {
122
+ border-bottom: none;
123
+ }
124
+
125
+ .specra-params-table tbody tr:hover {
126
+ background: var(--muted);
127
+ }
128
+
129
+ .specra-params-table code {
130
+ font-size: 0.8125rem;
131
+ font-family: var(--font-mono, ui-monospace, monospace);
132
+ color: var(--foreground);
133
+ background: var(--muted);
134
+ padding: 0.125rem 0.375rem;
135
+ border-radius: 0.25rem;
136
+ border: 1px solid var(--border);
137
+ }
138
+ </style>
@@ -51,6 +51,7 @@ export { default as TimelineItem } from './TimelineItem.svelte';
51
51
  export { default as Tabs } from './Tabs.svelte';
52
52
  export { default as ThemeToggle } from './ThemeToggle.svelte';
53
53
  export { default as Tooltip } from './Tooltip.svelte';
54
+ export { default as ProductSwitcher } from './ProductSwitcher.svelte';
54
55
  export { default as VersionBanner } from './VersionBanner.svelte';
55
56
  export { default as VersionSwitcher } from './VersionSwitcher.svelte';
56
57
  export { default as Video } from './Video.svelte';
@@ -52,6 +52,7 @@ export { default as TimelineItem } from './TimelineItem.svelte';
52
52
  export { default as Tabs } from './Tabs.svelte';
53
53
  export { default as ThemeToggle } from './ThemeToggle.svelte';
54
54
  export { default as Tooltip } from './Tooltip.svelte';
55
+ export { default as ProductSwitcher } from './ProductSwitcher.svelte';
55
56
  export { default as VersionBanner } from './VersionBanner.svelte';
56
57
  export { default as VersionSwitcher } from './VersionSwitcher.svelte';
57
58
  export { default as Video } from './Video.svelte';
@@ -36,23 +36,42 @@
36
36
 
37
37
  <script lang="ts">
38
38
  import { cn } from '../../utils.js';
39
- import type { HTMLButtonAttributes } from 'svelte/elements';
39
+ import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
40
40
  import type { Snippet } from 'svelte';
41
41
 
42
- interface Props extends HTMLButtonAttributes {
42
+ type ButtonProps = HTMLButtonAttributes & {
43
+ href?: never;
44
+ };
45
+
46
+ type AnchorProps = HTMLAnchorAttributes & {
47
+ href: string;
48
+ };
49
+
50
+ type Props = (ButtonProps | AnchorProps) & {
43
51
  variant?: ButtonVariants['variant'];
44
52
  size?: ButtonVariants['size'];
45
53
  class?: string;
46
54
  children?: Snippet;
47
- }
55
+ };
48
56
 
49
- let { variant = 'default', size = 'default', class: className, children, ...restProps }: Props = $props();
57
+ let { variant = 'default', size = 'default', class: className, children, href, ...restProps }: Props = $props();
50
58
  </script>
51
59
 
52
- <button
53
- data-slot="button"
54
- class={cn(buttonVariants({ variant, size, className }))}
55
- {...restProps}
56
- >
57
- {@render children?.()}
58
- </button>
60
+ {#if href}
61
+ <a
62
+ {href}
63
+ data-slot="button"
64
+ class={cn(buttonVariants({ variant, size, className }))}
65
+ {...restProps}
66
+ >
67
+ {@render children?.()}
68
+ </a>
69
+ {:else}
70
+ <button
71
+ data-slot="button"
72
+ class={cn(buttonVariants({ variant, size, className }))}
73
+ {...restProps}
74
+ >
75
+ {@render children?.()}
76
+ </button>
77
+ {/if}
@@ -1,17 +1,23 @@
1
1
  import { type VariantProps } from 'class-variance-authority';
2
2
  export declare const buttonVariants: (props?: ({
3
3
  variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
4
- size?: "default" | "icon" | "sm" | "lg" | "icon-sm" | "icon-lg" | null | undefined;
4
+ size?: "icon" | "default" | "sm" | "lg" | "icon-sm" | "icon-lg" | null | undefined;
5
5
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
6
  export type ButtonVariants = VariantProps<typeof buttonVariants>;
7
- import type { HTMLButtonAttributes } from 'svelte/elements';
7
+ import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
8
8
  import type { Snippet } from 'svelte';
9
- interface Props extends HTMLButtonAttributes {
9
+ type ButtonProps = HTMLButtonAttributes & {
10
+ href?: never;
11
+ };
12
+ type AnchorProps = HTMLAnchorAttributes & {
13
+ href: string;
14
+ };
15
+ type Props = (ButtonProps | AnchorProps) & {
10
16
  variant?: ButtonVariants['variant'];
11
17
  size?: ButtonVariants['size'];
12
18
  class?: string;
13
19
  children?: Snippet;
14
- }
20
+ };
15
21
  declare const Button: import("svelte").Component<Props, {}, "">;
16
22
  type Button = ReturnType<typeof Button>;
17
23
  export default Button;
package/dist/config.d.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  * The actual config loading happens on the server and is passed as props
5
5
  */
6
6
  export { defaultConfig } from "./config.types";
7
- export type { SpecraConfig, VersionConfig, BannerConfig } from "./config.types";
7
+ export type { SpecraConfig, VersionConfig, BannerConfig, ProductConfig, Product, DefaultProductConfig } from "./config.types";
8
8
  export { getConfig, getConfigValue, loadConfig, processContentWithEnv, replaceEnvVariables, validateConfig, reloadConfig, } from "./config.server";
@@ -74,6 +74,24 @@
74
74
  "hideLogo": {
75
75
  "type": "boolean",
76
76
  "description": "Whether to hide the site logo in the header"
77
+ },
78
+ "defaultProduct": {
79
+ "type": "object",
80
+ "description": "Configuration for the default product in multi-product mode. Falls back to site title and activeVersion if not set.",
81
+ "properties": {
82
+ "label": {
83
+ "type": "string",
84
+ "description": "Display name for the default product in the switcher"
85
+ },
86
+ "icon": {
87
+ "type": "string",
88
+ "description": "Icon for the default product (lucide icon name)"
89
+ },
90
+ "activeVersion": {
91
+ "type": "string",
92
+ "description": "Override active version for the default product"
93
+ }
94
+ }
77
95
  }
78
96
  }
79
97
  },
@@ -1,4 +1,4 @@
1
- import type { SpecraConfig, VersionConfig } from "./config.types";
1
+ import type { SpecraConfig, VersionConfig, ProductConfig, Product } from "./config.types";
2
2
  /**
3
3
  * Load and parse the Specra configuration file
4
4
  * Falls back to default configuration if file doesn't exist or is invalid
@@ -41,13 +41,13 @@ export declare function getConfig(): SpecraConfig;
41
41
  * Reload the configuration (useful for development) (SERVER ONLY)
42
42
  */
43
43
  export declare function reloadConfig(userConfig: Partial<SpecraConfig>): SpecraConfig;
44
- export declare function loadVersionConfig(version: string): VersionConfig | null;
44
+ export declare function loadVersionConfig(version: string, product?: string): VersionConfig | null;
45
45
  /**
46
- * Get the effective config for a specific version.
47
- * Merges global config with per-version overrides from _version_.json.
48
- * If no _version_.json exists, returns the global config unchanged.
46
+ * Get the effective config for a specific version and optional product.
47
+ * Merges in priority order: global product ← version.
48
+ * If no overrides exist, returns the global config unchanged.
49
49
  */
50
- export declare function getEffectiveConfig(version: string): SpecraConfig;
50
+ export declare function getEffectiveConfig(version: string, product?: string): SpecraConfig;
51
51
  /**
52
52
  * Version metadata for display in the version switcher.
53
53
  */
@@ -67,7 +67,35 @@ export interface VersionMeta {
67
67
  * Get metadata for all versions, enriched with _version_.json data.
68
68
  * Hidden versions are included but marked — the UI decides whether to show them.
69
69
  */
70
- export declare function getVersionsMeta(versions: string[]): VersionMeta[];
70
+ export declare function getVersionsMeta(versions: string[], product?: string): VersionMeta[];
71
+ /**
72
+ * Load and parse a _product_.json file for a given product slug.
73
+ * Returns null if the file doesn't exist or is invalid.
74
+ */
75
+ export declare function loadProductConfig(product: string): ProductConfig | null;
76
+ /**
77
+ * Scan docs/ top-level directories for _product_.json files.
78
+ * Returns the full list of products including the default product.
79
+ *
80
+ * Detection logic:
81
+ * 1. Single readdir + stat calls — no recursive walks
82
+ * 2. If no _product_.json found → single-product mode (returns empty array)
83
+ * 3. If any found → multi-product mode; bare version folders become the default product
84
+ * 4. Product slugs that match version patterns (e.g., v1.0.0) are rejected with a clear error
85
+ */
86
+ export declare function scanProducts(): Product[];
87
+ /**
88
+ * Get all products (cached). Returns empty array in single-product mode.
89
+ */
90
+ export declare function getProducts(): Product[];
91
+ /**
92
+ * Check if the site is in multi-product mode.
93
+ */
94
+ export declare function isMultiProductMode(): boolean;
95
+ /**
96
+ * Clear product-related caches. Called by file watchers when _product_.json changes.
97
+ */
98
+ export declare function clearProductCaches(): void;
71
99
  /**
72
100
  * Export the loaded config as default (SERVER ONLY)
73
101
  */