specra 0.2.7 → 0.2.9

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.
@@ -0,0 +1,52 @@
1
+ {
2
+ "$ref": "#/definitions/CategoryConfig",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "CategoryConfig": {
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "collapsed": {
9
+ "type": "boolean"
10
+ },
11
+ "collapsible": {
12
+ "type": "boolean"
13
+ },
14
+ "icon": {
15
+ "type": "string"
16
+ },
17
+ "label": {
18
+ "type": "string"
19
+ },
20
+ "link": {
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "slug": {
24
+ "type": "string"
25
+ },
26
+ "type": {
27
+ "enum": [
28
+ "generated-index",
29
+ "doc"
30
+ ],
31
+ "type": "string"
32
+ }
33
+ },
34
+ "required": [
35
+ "type"
36
+ ],
37
+ "type": "object"
38
+ },
39
+ "position": {
40
+ "type": "number"
41
+ },
42
+ "sidebar_position": {
43
+ "type": "number"
44
+ },
45
+ "tab_group": {
46
+ "type": "string"
47
+ }
48
+ },
49
+ "type": "object"
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "$ref": "#/definitions/ProductConfig",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "ProductConfig": {
6
+ "additionalProperties": false,
7
+ "description": "Per-product configuration loaded from docs/{product}/_product_.json Products sit above versions in the config hierarchy.",
8
+ "properties": {
9
+ "activeVersion": {
10
+ "description": "Default version for this product (overrides global site.activeVersion)",
11
+ "type": "string"
12
+ },
13
+ "badge": {
14
+ "description": "Badge text shown next to the product (e.g., \"New\", \"Beta\")",
15
+ "type": "string"
16
+ },
17
+ "description": {
18
+ "description": "Short description of the product",
19
+ "type": "string"
20
+ },
21
+ "icon": {
22
+ "description": "Icon identifier for the product dropdown (lucide icon name)",
23
+ "type": "string"
24
+ },
25
+ "label": {
26
+ "description": "Display name in the product switcher",
27
+ "type": "string"
28
+ },
29
+ "position": {
30
+ "description": "Order in the product dropdown (lower = first)",
31
+ "type": "number"
32
+ },
33
+ "tabGroups": {
34
+ "description": "Product-level tab group overrides",
35
+ "items": {
36
+ "$ref": "#/definitions/TabGroup"
37
+ },
38
+ "type": "array"
39
+ }
40
+ },
41
+ "required": [
42
+ "label"
43
+ ],
44
+ "type": "object"
45
+ },
46
+ "TabGroup": {
47
+ "additionalProperties": false,
48
+ "description": "Tab group for organizing documentation",
49
+ "properties": {
50
+ "icon": {
51
+ "description": "Optional icon name (lucide-react icon)",
52
+ "type": "string"
53
+ },
54
+ "id": {
55
+ "description": "Unique identifier for the tab group",
56
+ "type": "string"
57
+ },
58
+ "label": {
59
+ "description": "Display label for the tab",
60
+ "type": "string"
61
+ }
62
+ },
63
+ "required": [
64
+ "id",
65
+ "label"
66
+ ],
67
+ "type": "object"
68
+ }
69
+ }
70
+ }
@@ -17,6 +17,8 @@ import remarkMath from 'remark-math'
17
17
  import rehypeSlug from 'rehype-slug'
18
18
  import rehypeKatex from 'rehype-katex'
19
19
  import rehypeRaw from 'rehype-raw'
20
+ import fs from 'fs'
21
+ import path from 'path'
20
22
 
21
23
  /**
22
24
  * Get mdsvex preprocessor config with all Specra remark/rehype plugins
@@ -42,11 +44,33 @@ export function specraMdsvexConfig(options = {}) {
42
44
  }
43
45
  }
44
46
 
47
+ /**
48
+ * Scan the docs/ directory and return prerender entries for all version root pages.
49
+ * This ensures adapter-static discovers and prerenders every version, not just the active one.
50
+ */
51
+ function discoverVersionEntries(docsDir = path.join(process.cwd(), 'docs')) {
52
+ const entries = ['/']
53
+ try {
54
+ if (!fs.existsSync(docsDir)) return entries
55
+
56
+ const items = fs.readdirSync(docsDir, { withFileTypes: true })
57
+ for (const item of items) {
58
+ if (item.isDirectory() && /^v\d/.test(item.name)) {
59
+ entries.push(`/docs/${item.name}`)
60
+ }
61
+ }
62
+ } catch {
63
+ // Ignore errors — fall back to just '/'
64
+ }
65
+ return entries
66
+ }
67
+
45
68
  /**
46
69
  * Create a full SvelteKit config with Specra defaults
47
70
  */
48
71
  export function specraConfig(options = {}) {
49
72
  const { vitePreprocess } = options.vitePreprocess || {}
73
+ const userPrerender = options.kit?.prerender || {}
50
74
 
51
75
  return {
52
76
  extensions: ['.svelte', '.md', '.svx', '.mdx'],
@@ -55,7 +79,14 @@ export function specraConfig(options = {}) {
55
79
  mdsvex(specraMdsvexConfig(options.mdsvex || {}))
56
80
  ],
57
81
  kit: {
58
- ...(options.kit || {})
82
+ ...options.kit,
83
+ prerender: {
84
+ handleHttpError: 'warn',
85
+ handleMissingId: 'warn',
86
+ handleUnseenRoutes: 'warn',
87
+ entries: discoverVersionEntries(),
88
+ ...userPrerender,
89
+ }
59
90
  }
60
91
  }
61
92
  }
@@ -0,0 +1,83 @@
1
+ {
2
+ "$ref": "#/definitions/VersionConfig",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "BannerConfig": {
6
+ "additionalProperties": false,
7
+ "description": "Banner configuration for version-level or site-level banners. Site-wide banner configuration",
8
+ "properties": {
9
+ "text": {
10
+ "description": "Banner message text. Supports markdown links like [text](/url).",
11
+ "type": "string"
12
+ },
13
+ "type": {
14
+ "description": "Banner style: info, warning, error, success Banner type",
15
+ "enum": [
16
+ "info",
17
+ "warning",
18
+ "error",
19
+ "success"
20
+ ],
21
+ "type": "string"
22
+ }
23
+ },
24
+ "required": [
25
+ "text"
26
+ ],
27
+ "type": "object"
28
+ },
29
+ "TabGroup": {
30
+ "additionalProperties": false,
31
+ "description": "Tab group for organizing documentation",
32
+ "properties": {
33
+ "icon": {
34
+ "description": "Optional icon name (lucide-react icon)",
35
+ "type": "string"
36
+ },
37
+ "id": {
38
+ "description": "Unique identifier for the tab group",
39
+ "type": "string"
40
+ },
41
+ "label": {
42
+ "description": "Display label for the tab",
43
+ "type": "string"
44
+ }
45
+ },
46
+ "required": [
47
+ "id",
48
+ "label"
49
+ ],
50
+ "type": "object"
51
+ },
52
+ "VersionConfig": {
53
+ "additionalProperties": false,
54
+ "description": "Per-version configuration that can override global config settings. Loaded from docs/{version}/_version_.json",
55
+ "properties": {
56
+ "badge": {
57
+ "description": "Short badge text shown next to the version (e.g., \"Beta\", \"LTS\", \"Deprecated\").",
58
+ "type": "string"
59
+ },
60
+ "banner": {
61
+ "$ref": "#/definitions/BannerConfig",
62
+ "description": "Banner shown at the top of every page in this version. Overrides global banner."
63
+ },
64
+ "hidden": {
65
+ "description": "Hide this version from the version switcher. Useful for unreleased versions.",
66
+ "type": "boolean"
67
+ },
68
+ "label": {
69
+ "description": "Display label for this version (e.g., \"v1.0 (Stable)\"). Defaults to directory name.",
70
+ "type": "string"
71
+ },
72
+ "tabGroups": {
73
+ "description": "Override tab groups for this version. Empty array = no tabs.",
74
+ "items": {
75
+ "$ref": "#/definitions/TabGroup"
76
+ },
77
+ "type": "array"
78
+ }
79
+ },
80
+ "type": "object"
81
+ }
82
+ }
83
+ }
package/dist/category.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Note: This file uses server-only APIs (fs, path) and should only be imported in Server Components
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- const DOCS_DIR = path.join(process.cwd(), "docs");
4
+ const DOCS_DIR = typeof process !== 'undefined' ? path.join(process.cwd(), "docs") : "docs";
5
5
  /**
6
6
  * Read category.json from a folder
7
7
  */
@@ -55,8 +55,12 @@
55
55
  const baseUrl = $derived(
56
56
  product && product !== '_default_'
57
57
  ? `/docs/${product}`
58
- : (config.site?.baseUrl?.replace(/\/$/, '') || '')
58
+ : '/docs'
59
59
  );
60
+
61
+ // Note: We always use '/docs' as the base for non-product routes.
62
+ // Do NOT use config.site?.baseUrl here — that field (e.g. "/") refers to
63
+ // the site root, not the docs route prefix.
60
64
  </script>
61
65
 
62
66
  <div class="space-y-8">
@@ -26,7 +26,9 @@
26
26
  {#if Comp}
27
27
  {#if node.children && node.children.length > 0}
28
28
  <svelte:component this={Comp} {...node.props}>
29
- <MdxContent nodes={node.children} {components} />
29
+ {#snippet children()}
30
+ <MdxContent nodes={node.children} {components} />
31
+ {/snippet}
30
32
  </svelte:component>
31
33
  {:else}
32
34
  <svelte:component this={Comp} {...node.props} />
@@ -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
  }
@@ -57,7 +57,28 @@
57
57
  : `/docs/${version}`
58
58
  );
59
59
 
60
- let collapsed: Record<string, boolean> = $state({});
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());
61
82
  let pathname = $derived($page.url.pathname.replace(/\/$/, ''));
62
83
 
63
84
  // Filter docs by active tab group if tab groups are configured
@@ -180,6 +201,7 @@
180
201
 
181
202
  function toggleSection(section: string) {
182
203
  collapsed = { ...collapsed, [section]: !collapsed[section] };
204
+ saveCollapsedState(collapsed);
183
205
  }
184
206
 
185
207
  function isActiveInGroup(group: SidebarGroup): boolean {
@@ -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>
@@ -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}
@@ -4,14 +4,20 @@ export declare const buttonVariants: (props?: ({
4
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;
@@ -150,7 +150,7 @@ export function reloadConfig(userConfig) {
150
150
  * Returns null if the file doesn't exist or is invalid.
151
151
  */
152
152
  const versionConfigCache = new Map();
153
- const VCFG_TTL = process.env.NODE_ENV === 'development' ? 5000 : 60000;
153
+ const VCFG_TTL = (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ? 5000 : 60000;
154
154
  export function loadVersionConfig(version, product) {
155
155
  const cacheKey = product && product !== "_default_" ? `${product}:${version}` : version;
156
156
  const cached = versionConfigCache.get(cacheKey);
@@ -240,7 +240,7 @@ const productsCache = {
240
240
  data: null,
241
241
  timestamp: 0,
242
242
  };
243
- const PCFG_TTL = process.env.NODE_ENV === 'development' ? 5000 : 60000;
243
+ const PCFG_TTL = (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ? 5000 : 60000;
244
244
  /** Cache for individual product configs */
245
245
  const productConfigCache = new Map();
246
246
  /**
package/dist/dev-utils.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Development utilities for debugging and performance monitoring
3
3
  * Only active in development mode
4
4
  */
5
- const isDevelopment = process.env.NODE_ENV === 'development';
5
+ const isDevelopment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
6
6
  /**
7
7
  * Performance timer for measuring operation duration
8
8
  */
package/dist/mdx-cache.js CHANGED
@@ -10,7 +10,7 @@ import { clearProductCaches } from './config.server';
10
10
  import { watch } from 'fs';
11
11
  import { join } from 'path';
12
12
  import { PerfTimer, logCacheOperation } from './dev-utils';
13
- const isDevelopment = process.env.NODE_ENV === 'development';
13
+ const isDevelopment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
14
14
  // Cache stores
15
15
  const versionsCache = {
16
16
  data: null,
package/dist/mdx.js CHANGED
@@ -15,7 +15,7 @@ import { getAllCategoryConfigs } from "./category";
15
15
  import { sortSidebarItems, sortSidebarGroups, buildSidebarStructure } from "./sidebar-utils";
16
16
  import { sanitizePath, validatePathWithinDirectory, validateMDXSecurity } from "./mdx-security";
17
17
  import { getConfig } from "./config";
18
- const DOCS_DIR = path.join(process.cwd(), "docs");
18
+ const DOCS_DIR = typeof process !== 'undefined' ? path.join(process.cwd(), "docs") : "docs";
19
19
  /**
20
20
  * Resolve the docs directory for a given version and optional product.
21
21
  * - Default product or omitted: docs/{version}/
@@ -379,7 +379,14 @@ function parseJsxExpression(expr) {
379
379
  return JSON.parse(trimmed);
380
380
  }
381
381
  catch {
382
- return trimmed;
382
+ // Convert JS object notation inside arrays to JSON
383
+ const jsonStr = trimmed.replace(/(\w+)\s*:/g, '"$1":').replace(/:\s*'([^']*)'/g, ': "$1"');
384
+ try {
385
+ return JSON.parse(jsonStr);
386
+ }
387
+ catch {
388
+ return trimmed;
389
+ }
383
390
  }
384
391
  }
385
392
  return trimmed;
@@ -449,6 +456,61 @@ function extractCodeBlockProps(node) {
449
456
  const filename = node.properties?.['data-filename'] || codeChild.properties?.['data-filename'];
450
457
  return { code, language, ...(filename ? { filename } : {}) };
451
458
  }
459
+ /**
460
+ * Detect GitHub-style alert blockquotes: > [!WARNING] content
461
+ * Returns the alert type and remaining content children, or null if not an alert.
462
+ */
463
+ const ALERT_TYPE_MAP = {
464
+ NOTE: 'note',
465
+ TIP: 'tip',
466
+ IMPORTANT: 'info',
467
+ WARNING: 'warning',
468
+ CAUTION: 'danger',
469
+ INFO: 'info',
470
+ SUCCESS: 'success',
471
+ ERROR: 'error',
472
+ DANGER: 'danger',
473
+ };
474
+ function extractBlockquoteAlert(node) {
475
+ if (node.type !== 'element' || node.tagName !== 'blockquote')
476
+ return null;
477
+ if (!node.children || node.children.length === 0)
478
+ return null;
479
+ // Find the first paragraph child
480
+ const firstP = node.children.find((c) => c.type === 'element' && c.tagName === 'p');
481
+ if (!firstP || !firstP.children || firstP.children.length === 0)
482
+ return null;
483
+ // Check first text node for [!TYPE] pattern
484
+ const firstText = firstP.children[0];
485
+ if (firstText.type !== 'text')
486
+ return null;
487
+ const match = firstText.value.match(/^\s*\[!(\w+)\]\s*\n?/);
488
+ if (!match)
489
+ return null;
490
+ const alertType = ALERT_TYPE_MAP[match[1].toUpperCase()];
491
+ if (!alertType)
492
+ return null;
493
+ // Build remaining content: modify the first paragraph to remove the alert marker
494
+ const remainingFirstPChildren = [...firstP.children];
495
+ const remainingText = firstText.value.slice(match[0].length);
496
+ if (remainingText.trim()) {
497
+ remainingFirstPChildren[0] = { ...firstText, value: remainingText };
498
+ }
499
+ else {
500
+ remainingFirstPChildren.shift();
501
+ }
502
+ const contentChildren = [];
503
+ if (remainingFirstPChildren.length > 0) {
504
+ contentChildren.push({ ...firstP, children: remainingFirstPChildren });
505
+ }
506
+ // Add any remaining blockquote children (paragraphs after the first)
507
+ for (const child of node.children) {
508
+ if (child !== firstP) {
509
+ contentChildren.push(child);
510
+ }
511
+ }
512
+ return { type: alertType, contentChildren };
513
+ }
452
514
  /**
453
515
  * Recursively extract text content from a hast node.
454
516
  */
@@ -720,6 +782,20 @@ async function hastChildrenToMdxNodes(children) {
720
782
  children: childNodes,
721
783
  });
722
784
  }
785
+ else if (extractBlockquoteAlert(child)) {
786
+ // GitHub-style alert blockquotes: > [!WARNING] content
787
+ flushHtmlBuffer();
788
+ const alert = extractBlockquoteAlert(child);
789
+ const contentNodes = alert.contentChildren.length > 0
790
+ ? await hastChildrenToMdxNodes(alert.contentChildren)
791
+ : [];
792
+ nodes.push({
793
+ type: 'component',
794
+ name: 'Callout',
795
+ props: { type: alert.type },
796
+ children: contentNodes,
797
+ });
798
+ }
723
799
  else {
724
800
  // Check if this regular element contains any component elements nested within
725
801
  if (hasNestedComponent(child)) {
@@ -116,8 +116,13 @@ export class OpenApiParser {
116
116
  parseParameters(parameters, spec) {
117
117
  const result = { path: [], query: [], header: [] };
118
118
  for (const param of parameters) {
119
+ if (!param)
120
+ continue;
119
121
  // Resolve $ref if present
120
122
  const resolved = param.$ref ? this.resolveRef(param.$ref, spec) : param;
123
+ // Skip params that failed to resolve or have no name
124
+ if (!resolved || !resolved.name || !resolved.in)
125
+ continue;
121
126
  const apiParam = {
122
127
  name: resolved.name,
123
128
  type: resolved.schema?.type || resolved.type || "string",
@@ -202,7 +207,7 @@ export class OpenApiParser {
202
207
  for (const segment of path) {
203
208
  current = current[segment];
204
209
  if (!current)
205
- return {};
210
+ return null;
206
211
  }
207
212
  return current;
208
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specra",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -34,7 +34,10 @@
34
34
  },
35
35
  "./styles": "./dist/styles/globals.css",
36
36
  "./styles.css": "./dist/styles/globals.css",
37
- "./config.schema.json": "./config/specra.config.schema.json"
37
+ "./config.schema.json": "./config/specra.config.schema.json",
38
+ "./version.schema.json": "./config/version.schema.json",
39
+ "./product.schema.json": "./config/product.schema.json",
40
+ "./category.schema.json": "./config/category.schema.json"
38
41
  },
39
42
  "files": [
40
43
  "dist",
@@ -45,7 +48,7 @@
45
48
  "dev": "svelte-package --watch",
46
49
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
47
50
  "typecheck": "svelte-check --tsconfig ./tsconfig.json",
48
- "generate:schema": "ts-json-schema-generator --path src/lib/config.types.ts --type SpecraConfig --out config/specra.config.schema.json"
51
+ "generate:schema": "ts-json-schema-generator --path src/lib/config.types.ts --type SpecraConfig --out config/specra.config.schema.json && ts-json-schema-generator --path src/lib/config.types.ts --type VersionConfig --out config/version.schema.json && ts-json-schema-generator --path src/lib/config.types.ts --type ProductConfig --out config/product.schema.json && ts-json-schema-generator --path src/lib/category.ts --type CategoryConfig --out config/category.schema.json"
49
52
  },
50
53
  "keywords": [
51
54
  "documentation",