create-specra 0.2.3 → 0.2.5

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/README.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  The fastest way to create a new Specra documentation site. Scaffold a complete documentation project with a single command.
4
4
 
5
+ ## What is Specra?
6
+
7
+ Specra is a modern documentation library for SvelteKit that provides:
8
+ - Multi-version documentation support
9
+ - API reference generation
10
+ - Full-text search
11
+ - MDX-powered content
12
+ - Beautiful UI components
13
+
14
+ The official Specra site ([specra-docs](https://specra-docs.com)) also offers a SaaS platform with paid tiers (Starter, Pro, Enterprise) including authentication, Stripe/M-Pesa billing, and a user dashboard. The CLI scaffolds free, self-hosted documentation sites — no billing features are included in generated projects.
15
+
5
16
  ## Usage
6
17
 
7
18
  ### With npx (recommended)
@@ -142,16 +153,81 @@ Once your project is created, you can:
142
153
 
143
154
  4. Customize your site in `specra.config.json`
144
155
 
145
- ## What is Specra?
156
+ ## Deployment
146
157
 
147
- Specra is a modern documentation library for SvelteKit that provides:
148
- - Multi-version documentation support
149
- - API reference generation
150
- - Full-text search
151
- - MDX-powered content
152
- - Beautiful UI components
158
+ Specra projects are standard SvelteKit apps, so you can deploy anywhere SvelteKit runs.
153
159
 
154
- The official Specra site ([specra-docs](https://specra-docs.com)) also offers a SaaS platform with paid tiers (Starter, Pro, Enterprise) including authentication, Stripe/M-Pesa billing, and a user dashboard. The CLI scaffolds free, self-hosted documentation sites — no billing features are included in generated projects.
160
+ ### Static Hosting (Vercel, Netlify, Cloudflare Pages)
161
+
162
+ Install the appropriate SvelteKit adapter:
163
+
164
+ ```bash
165
+ # Vercel
166
+ npm install -D @sveltejs/adapter-vercel
167
+
168
+ # Netlify
169
+ npm install -D @sveltejs/adapter-netlify
170
+
171
+ # Cloudflare Pages
172
+ npm install -D @sveltejs/adapter-cloudflare
173
+ ```
174
+
175
+ Update `svelte.config.js` to use the adapter:
176
+
177
+ ```js
178
+ import adapter from '@sveltejs/adapter-vercel'; // or adapter-netlify, etc.
179
+
180
+ export default {
181
+ kit: {
182
+ adapter: adapter()
183
+ }
184
+ };
185
+ ```
186
+
187
+ Then push to your Git provider — the platform handles the rest.
188
+
189
+ ### Node Server (VPS, Docker, Railway)
190
+
191
+ Use `@sveltejs/adapter-node` (included by default):
192
+
193
+ ```bash
194
+ npm run build
195
+ node build
196
+ ```
197
+
198
+ The server listens on port 3000 by default. Configure with environment variables:
199
+
200
+ ```bash
201
+ PORT=8080 HOST=0.0.0.0 node build
202
+ ```
203
+
204
+ ### Static Site Generation
205
+
206
+ For fully static docs with no server needed:
207
+
208
+ ```bash
209
+ npm install -D @sveltejs/adapter-static
210
+ ```
211
+
212
+ Update `svelte.config.js`:
213
+
214
+ ```js
215
+ import adapter from '@sveltejs/adapter-static';
216
+
217
+ export default {
218
+ kit: {
219
+ adapter: adapter({
220
+ fallback: '404.html'
221
+ })
222
+ }
223
+ };
224
+ ```
225
+
226
+ ```bash
227
+ npm run build
228
+ ```
229
+
230
+ Upload the `build/` directory to any static host (GitHub Pages, S3, etc.).
155
231
 
156
232
  ## Learn More
157
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-specra",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "CLI tool to scaffold a new Specra documentation site with Next.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
11
11
  },
12
12
  "dependencies": {
13
- "specra": "^0.2.1"
13
+ "specra": "^0.2.4"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@sveltejs/adapter-static": "^3.0.0",
@@ -1,7 +1,86 @@
1
1
  @import "specra/styles";
2
2
  @source "./**/*.{js,ts,svelte}";
3
3
 
4
- @theme {
4
+ @theme inline {
5
5
  --font-sans: "Geist", "Geist Fallback", system-ui, sans-serif;
6
6
  --font-mono: "Geist Mono", "Geist Mono Fallback", monospace;
7
7
  }
8
+
9
+ /* =============================================
10
+ THEME OVERRIDES
11
+ Uncomment a :root block for light theme and/or
12
+ a .dark block for dark theme. Mix and match!
13
+ ============================================= */
14
+
15
+ /* ─── Mint Light ─── */
16
+ /* :root {
17
+ --primary: oklch(0.45 0.18 162);
18
+ --primary-foreground: oklch(0.98 0 0);
19
+ --ring: oklch(0.45 0.18 162);
20
+ } */
21
+
22
+ /* ─── Ocean Light ─── */
23
+ /* :root {
24
+ --primary: oklch(0.45 0.18 240);
25
+ --primary-foreground: oklch(0.98 0 0);
26
+ --ring: oklch(0.45 0.18 240);
27
+ } */
28
+
29
+ /* ─── Rose Light ─── */
30
+ /* :root {
31
+ --primary: oklch(0.50 0.20 350);
32
+ --primary-foreground: oklch(0.98 0 0);
33
+ --ring: oklch(0.50 0.20 350);
34
+ } */
35
+
36
+ /* ─── Amber Light ─── */
37
+ /* :root {
38
+ --primary: oklch(0.55 0.16 70);
39
+ --primary-foreground: oklch(0.98 0 0);
40
+ --ring: oklch(0.55 0.16 70);
41
+ } */
42
+
43
+ /* ─── Violet Light ─── */
44
+ /* :root {
45
+ --primary: oklch(0.48 0.22 290);
46
+ --primary-foreground: oklch(0.98 0 0);
47
+ --ring: oklch(0.48 0.22 290);
48
+ } */
49
+
50
+ /* ─── Mint Dark ─── */
51
+ /* .dark {
52
+ --primary: oklch(0.72 0.18 162);
53
+ --primary-foreground: oklch(0.10 0.03 160);
54
+ --ring: oklch(0.72 0.18 162);
55
+ } */
56
+
57
+ /* ─── Ocean Dark ─── */
58
+ /* .dark {
59
+ --primary: oklch(0.68 0.16 240);
60
+ --primary-foreground: oklch(0.98 0 0);
61
+ --ring: oklch(0.68 0.16 240);
62
+ } */
63
+
64
+ /* ─── Rose Dark ─── */
65
+ /* .dark {
66
+ --primary: oklch(0.70 0.18 350);
67
+ --primary-foreground: oklch(0.98 0 0);
68
+ --ring: oklch(0.70 0.18 350);
69
+ } */
70
+
71
+ /* ─── Amber Dark ─── */
72
+ /* .dark {
73
+ --primary: oklch(0.75 0.16 70);
74
+ --primary-foreground: oklch(0.10 0.03 70);
75
+ --ring: oklch(0.75 0.16 70);
76
+ } */
77
+
78
+ /* ─── Violet Dark ─── */
79
+ /* .dark {
80
+ --primary: oklch(0.70 0.20 290);
81
+ --primary-foreground: oklch(0.98 0 0);
82
+ --ring: oklch(0.70 0.20 290);
83
+ } */
84
+
85
+ /* For full theme customization (backgrounds, borders, sidebar, etc.)
86
+ see: https://specra-docs.com/docs/v1.0.0/configuration/theming */
@@ -6,7 +6,7 @@ import type { SpecraConfig } from 'specra';
6
6
  initConfig(specraConfig as unknown as Partial<SpecraConfig>);
7
7
 
8
8
  export const prerender = true;
9
- export const trailingSlash = 'always';
9
+ export const trailingSlash = 'never';
10
10
 
11
11
  export const load: LayoutServerLoad = async () => {
12
12
  const config = getConfig();
@@ -0,0 +1,31 @@
1
+ import { getCachedVersions, getCachedAllDocs, getEffectiveConfig, getI18nConfig, getVersionsMeta, loadVersionConfig } from 'specra';
2
+ import { redirect } from '@sveltejs/kit';
3
+ import type { LayoutServerLoad } from './$types';
4
+
5
+ export const load: LayoutServerLoad = async ({ params }) => {
6
+ const { version } = params;
7
+
8
+ const i18nConfig = getI18nConfig();
9
+ const defaultLocale = i18nConfig?.defaultLocale || 'en';
10
+
11
+ // Block access to hidden versions — redirect to active version
12
+ const currentVersionConfig = loadVersionConfig(version);
13
+ if (currentVersionConfig?.hidden) {
14
+ const config = getEffectiveConfig(version);
15
+ const activeVersion = config.site?.activeVersion || 'v1.0.0';
16
+ throw redirect(302, `/docs/${activeVersion}`);
17
+ }
18
+
19
+ const allDocs = await getCachedAllDocs(version, defaultLocale);
20
+ const versions = getCachedVersions();
21
+ const config = getEffectiveConfig(version);
22
+ const versionsMeta = getVersionsMeta(versions);
23
+
24
+ return {
25
+ allDocs,
26
+ versions,
27
+ versionsMeta,
28
+ config,
29
+ versionBanner: currentVersionConfig?.banner,
30
+ };
31
+ };
@@ -2,28 +2,17 @@ import {
2
2
  extractTableOfContents,
3
3
  getAdjacentDocs,
4
4
  isCategoryPage,
5
- getCachedVersions,
6
5
  getCachedAllDocs,
7
6
  getCachedDocBySlug,
8
7
  getI18nConfig,
9
- getConfig,
10
8
  } from 'specra';
11
9
  import type { PageServerLoad } from './$types';
12
10
 
13
- export const load: PageServerLoad = async ({ params }) => {
11
+ export const load: PageServerLoad = async ({ params, parent }) => {
14
12
  const { version, slug: slugArray } = params;
15
13
  const slug = slugArray.replace(/\/$/, '');
14
+ const { allDocs, versions, config, versionsMeta, versionBanner } = await parent();
16
15
 
17
- const i18nConfig = getI18nConfig();
18
- const slugParts = slug.split('/');
19
- let locale: string | undefined;
20
- if (i18nConfig && i18nConfig.locales.includes(slugParts[0])) {
21
- locale = slugParts[0];
22
- }
23
-
24
- const allDocs = await getCachedAllDocs(version, locale);
25
- const versions = getCachedVersions();
26
- const config = getConfig();
27
16
  const isCategory = isCategoryPage(slug, allDocs);
28
17
  const doc = await getCachedDocBySlug(slug, version);
29
18
 
@@ -51,6 +40,8 @@ export const load: PageServerLoad = async ({ params }) => {
51
40
  slug,
52
41
  allDocs,
53
42
  versions,
43
+ versionsMeta,
44
+ versionBanner,
54
45
  config,
55
46
  isCategory: true,
56
47
  isNotFound: false,
@@ -74,6 +65,8 @@ export const load: PageServerLoad = async ({ params }) => {
74
65
  slug,
75
66
  allDocs,
76
67
  versions,
68
+ versionsMeta,
69
+ versionBanner,
77
70
  config,
78
71
  isCategory: false,
79
72
  isNotFound: true,
@@ -102,6 +95,8 @@ export const load: PageServerLoad = async ({ params }) => {
102
95
  slug,
103
96
  allDocs,
104
97
  versions,
98
+ versionsMeta,
99
+ versionBanner,
105
100
  config,
106
101
  isCategory: showCategoryIndex,
107
102
  isNotFound: false,
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  TableOfContents,
4
4
  Header,
5
+ TabGroups,
5
6
  DocLayout,
6
7
  CategoryIndex,
7
8
  HotReloadIndicator,
@@ -44,7 +45,19 @@
44
45
  activeTabGroup={data.categoryTabGroup}
45
46
  >
46
47
  {#snippet header()}
47
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
48
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
49
+ {#snippet subheader()}
50
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
51
+ <TabGroups
52
+ tabGroups={data.config.navigation.tabGroups}
53
+ activeTabId={data.categoryTabGroup}
54
+ docs={allDocsCompat}
55
+ version={data.version}
56
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
57
+ />
58
+ {/if}
59
+ {/snippet}
60
+ </Header>
48
61
  {/snippet}
49
62
  <CategoryIndex
50
63
  categoryPath={data.slug}
@@ -65,7 +78,19 @@
65
78
  config={data.config}
66
79
  >
67
80
  {#snippet header()}
68
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
81
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
82
+ {#snippet subheader()}
83
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
84
+ <TabGroups
85
+ tabGroups={data.config.navigation.tabGroups}
86
+ activeTabId={data.categoryTabGroup}
87
+ docs={allDocsCompat}
88
+ version={data.version}
89
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
90
+ />
91
+ {/if}
92
+ {/snippet}
93
+ </Header>
69
94
  {/snippet}
70
95
  <NotFoundContent version={data.version} />
71
96
  </MobileDocLayout>
@@ -80,7 +105,19 @@
80
105
  activeTabGroup={data.categoryTabGroup}
81
106
  >
82
107
  {#snippet header()}
83
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
108
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
109
+ {#snippet subheader()}
110
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
111
+ <TabGroups
112
+ tabGroups={data.config.navigation.tabGroups}
113
+ activeTabId={data.categoryTabGroup}
114
+ docs={allDocsCompat}
115
+ version={data.version}
116
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
117
+ />
118
+ {/if}
119
+ {/snippet}
120
+ </Header>
84
121
  {/snippet}
85
122
  {#snippet toc()}
86
123
  {#if !data.isCategory}
@@ -10,7 +10,7 @@
10
10
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
11
11
  },
12
12
  "dependencies": {
13
- "specra": "^0.2.1"
13
+ "specra": "^0.2.4"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@sveltejs/adapter-static": "^3.0.0",
@@ -1,7 +1,86 @@
1
1
  @import "specra/styles";
2
2
  @source "./**/*.{js,ts,svelte}";
3
3
 
4
- @theme {
4
+ @theme inline {
5
5
  --font-sans: "Geist", "Geist Fallback", system-ui, sans-serif;
6
6
  --font-mono: "Geist Mono", "Geist Mono Fallback", monospace;
7
7
  }
8
+
9
+ /* =============================================
10
+ THEME OVERRIDES
11
+ Uncomment a :root block for light theme and/or
12
+ a .dark block for dark theme. Mix and match!
13
+ ============================================= */
14
+
15
+ /* ─── Mint Light ─── */
16
+ /* :root {
17
+ --primary: oklch(0.45 0.18 162);
18
+ --primary-foreground: oklch(0.98 0 0);
19
+ --ring: oklch(0.45 0.18 162);
20
+ } */
21
+
22
+ /* ─── Ocean Light ─── */
23
+ /* :root {
24
+ --primary: oklch(0.45 0.18 240);
25
+ --primary-foreground: oklch(0.98 0 0);
26
+ --ring: oklch(0.45 0.18 240);
27
+ } */
28
+
29
+ /* ─── Rose Light ─── */
30
+ /* :root {
31
+ --primary: oklch(0.50 0.20 350);
32
+ --primary-foreground: oklch(0.98 0 0);
33
+ --ring: oklch(0.50 0.20 350);
34
+ } */
35
+
36
+ /* ─── Amber Light ─── */
37
+ /* :root {
38
+ --primary: oklch(0.55 0.16 70);
39
+ --primary-foreground: oklch(0.98 0 0);
40
+ --ring: oklch(0.55 0.16 70);
41
+ } */
42
+
43
+ /* ─── Violet Light ─── */
44
+ /* :root {
45
+ --primary: oklch(0.48 0.22 290);
46
+ --primary-foreground: oklch(0.98 0 0);
47
+ --ring: oklch(0.48 0.22 290);
48
+ } */
49
+
50
+ /* ─── Mint Dark ─── */
51
+ /* .dark {
52
+ --primary: oklch(0.72 0.18 162);
53
+ --primary-foreground: oklch(0.10 0.03 160);
54
+ --ring: oklch(0.72 0.18 162);
55
+ } */
56
+
57
+ /* ─── Ocean Dark ─── */
58
+ /* .dark {
59
+ --primary: oklch(0.68 0.16 240);
60
+ --primary-foreground: oklch(0.98 0 0);
61
+ --ring: oklch(0.68 0.16 240);
62
+ } */
63
+
64
+ /* ─── Rose Dark ─── */
65
+ /* .dark {
66
+ --primary: oklch(0.70 0.18 350);
67
+ --primary-foreground: oklch(0.98 0 0);
68
+ --ring: oklch(0.70 0.18 350);
69
+ } */
70
+
71
+ /* ─── Amber Dark ─── */
72
+ /* .dark {
73
+ --primary: oklch(0.75 0.16 70);
74
+ --primary-foreground: oklch(0.10 0.03 70);
75
+ --ring: oklch(0.75 0.16 70);
76
+ } */
77
+
78
+ /* ─── Violet Dark ─── */
79
+ /* .dark {
80
+ --primary: oklch(0.70 0.20 290);
81
+ --primary-foreground: oklch(0.98 0 0);
82
+ --ring: oklch(0.70 0.20 290);
83
+ } */
84
+
85
+ /* For full theme customization (backgrounds, borders, sidebar, etc.)
86
+ see: https://specra-docs.com/docs/v1.0.0/configuration/theming */
@@ -0,0 +1,31 @@
1
+ import { getCachedVersions, getCachedAllDocs, getEffectiveConfig, getI18nConfig, getVersionsMeta, loadVersionConfig } from 'specra';
2
+ import { redirect } from '@sveltejs/kit';
3
+ import type { LayoutServerLoad } from './$types';
4
+
5
+ export const load: LayoutServerLoad = async ({ params }) => {
6
+ const { version } = params;
7
+
8
+ const i18nConfig = getI18nConfig();
9
+ const defaultLocale = i18nConfig?.defaultLocale || 'en';
10
+
11
+ // Block access to hidden versions — redirect to active version
12
+ const currentVersionConfig = loadVersionConfig(version);
13
+ if (currentVersionConfig?.hidden) {
14
+ const config = getEffectiveConfig(version);
15
+ const activeVersion = config.site?.activeVersion || 'v1.0.0';
16
+ throw redirect(302, `/docs/${activeVersion}`);
17
+ }
18
+
19
+ const allDocs = await getCachedAllDocs(version, defaultLocale);
20
+ const versions = getCachedVersions();
21
+ const config = getEffectiveConfig(version);
22
+ const versionsMeta = getVersionsMeta(versions);
23
+
24
+ return {
25
+ allDocs,
26
+ versions,
27
+ versionsMeta,
28
+ config,
29
+ versionBanner: currentVersionConfig?.banner,
30
+ };
31
+ };
@@ -2,28 +2,17 @@ import {
2
2
  extractTableOfContents,
3
3
  getAdjacentDocs,
4
4
  isCategoryPage,
5
- getCachedVersions,
6
5
  getCachedAllDocs,
7
6
  getCachedDocBySlug,
8
7
  getI18nConfig,
9
- getConfig,
10
8
  } from 'specra';
11
9
  import type { PageServerLoad } from './$types';
12
10
 
13
- export const load: PageServerLoad = async ({ params }) => {
11
+ export const load: PageServerLoad = async ({ params, parent }) => {
14
12
  const { version, slug: slugArray } = params;
15
13
  const slug = slugArray.replace(/\/$/, '');
14
+ const { allDocs, versions, config, versionsMeta, versionBanner } = await parent();
16
15
 
17
- const i18nConfig = getI18nConfig();
18
- const slugParts = slug.split('/');
19
- let locale: string | undefined;
20
- if (i18nConfig && i18nConfig.locales.includes(slugParts[0])) {
21
- locale = slugParts[0];
22
- }
23
-
24
- const allDocs = await getCachedAllDocs(version, locale);
25
- const versions = getCachedVersions();
26
- const config = getConfig();
27
16
  const isCategory = isCategoryPage(slug, allDocs);
28
17
  const doc = await getCachedDocBySlug(slug, version);
29
18
 
@@ -51,6 +40,8 @@ export const load: PageServerLoad = async ({ params }) => {
51
40
  slug,
52
41
  allDocs,
53
42
  versions,
43
+ versionsMeta,
44
+ versionBanner,
54
45
  config,
55
46
  isCategory: true,
56
47
  isNotFound: false,
@@ -74,6 +65,8 @@ export const load: PageServerLoad = async ({ params }) => {
74
65
  slug,
75
66
  allDocs,
76
67
  versions,
68
+ versionsMeta,
69
+ versionBanner,
77
70
  config,
78
71
  isCategory: false,
79
72
  isNotFound: true,
@@ -102,6 +95,8 @@ export const load: PageServerLoad = async ({ params }) => {
102
95
  slug,
103
96
  allDocs,
104
97
  versions,
98
+ versionsMeta,
99
+ versionBanner,
105
100
  config,
106
101
  isCategory: showCategoryIndex,
107
102
  isNotFound: false,
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  TableOfContents,
4
4
  Header,
5
+ TabGroups,
5
6
  DocLayout,
6
7
  CategoryIndex,
7
8
  HotReloadIndicator,
@@ -44,7 +45,19 @@
44
45
  activeTabGroup={data.categoryTabGroup}
45
46
  >
46
47
  {#snippet header()}
47
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
48
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
49
+ {#snippet subheader()}
50
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
51
+ <TabGroups
52
+ tabGroups={data.config.navigation.tabGroups}
53
+ activeTabId={data.categoryTabGroup}
54
+ docs={allDocsCompat}
55
+ version={data.version}
56
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
57
+ />
58
+ {/if}
59
+ {/snippet}
60
+ </Header>
48
61
  {/snippet}
49
62
  <CategoryIndex
50
63
  categoryPath={data.slug}
@@ -65,7 +78,19 @@
65
78
  config={data.config}
66
79
  >
67
80
  {#snippet header()}
68
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
81
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
82
+ {#snippet subheader()}
83
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
84
+ <TabGroups
85
+ tabGroups={data.config.navigation.tabGroups}
86
+ activeTabId={data.categoryTabGroup}
87
+ docs={allDocsCompat}
88
+ version={data.version}
89
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
90
+ />
91
+ {/if}
92
+ {/snippet}
93
+ </Header>
69
94
  {/snippet}
70
95
  <NotFoundContent version={data.version} />
71
96
  </MobileDocLayout>
@@ -80,7 +105,19 @@
80
105
  activeTabGroup={data.categoryTabGroup}
81
106
  >
82
107
  {#snippet header()}
83
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
108
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
109
+ {#snippet subheader()}
110
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
111
+ <TabGroups
112
+ tabGroups={data.config.navigation.tabGroups}
113
+ activeTabId={data.categoryTabGroup}
114
+ docs={allDocsCompat}
115
+ version={data.version}
116
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
117
+ />
118
+ {/if}
119
+ {/snippet}
120
+ </Header>
84
121
  {/snippet}
85
122
  {#snippet toc()}
86
123
  {#if !data.isCategory}
@@ -10,7 +10,7 @@
10
10
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
11
11
  },
12
12
  "dependencies": {
13
- "specra": "^0.2.1"
13
+ "specra": "^0.2.4"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@sveltejs/adapter-static": "^3.0.0",
@@ -1,7 +1,86 @@
1
1
  @import "specra/styles";
2
2
  @source "./**/*.{js,ts,svelte}";
3
3
 
4
- @theme {
4
+ @theme inline {
5
5
  --font-sans: "Geist", "Geist Fallback", system-ui, sans-serif;
6
6
  --font-mono: "Geist Mono", "Geist Mono Fallback", monospace;
7
7
  }
8
+
9
+ /* =============================================
10
+ THEME OVERRIDES
11
+ Uncomment a :root block for light theme and/or
12
+ a .dark block for dark theme. Mix and match!
13
+ ============================================= */
14
+
15
+ /* ─── Mint Light ─── */
16
+ /* :root {
17
+ --primary: oklch(0.45 0.18 162);
18
+ --primary-foreground: oklch(0.98 0 0);
19
+ --ring: oklch(0.45 0.18 162);
20
+ } */
21
+
22
+ /* ─── Ocean Light ─── */
23
+ /* :root {
24
+ --primary: oklch(0.45 0.18 240);
25
+ --primary-foreground: oklch(0.98 0 0);
26
+ --ring: oklch(0.45 0.18 240);
27
+ } */
28
+
29
+ /* ─── Rose Light ─── */
30
+ /* :root {
31
+ --primary: oklch(0.50 0.20 350);
32
+ --primary-foreground: oklch(0.98 0 0);
33
+ --ring: oklch(0.50 0.20 350);
34
+ } */
35
+
36
+ /* ─── Amber Light ─── */
37
+ /* :root {
38
+ --primary: oklch(0.55 0.16 70);
39
+ --primary-foreground: oklch(0.98 0 0);
40
+ --ring: oklch(0.55 0.16 70);
41
+ } */
42
+
43
+ /* ─── Violet Light ─── */
44
+ /* :root {
45
+ --primary: oklch(0.48 0.22 290);
46
+ --primary-foreground: oklch(0.98 0 0);
47
+ --ring: oklch(0.48 0.22 290);
48
+ } */
49
+
50
+ /* ─── Mint Dark ─── */
51
+ /* .dark {
52
+ --primary: oklch(0.72 0.18 162);
53
+ --primary-foreground: oklch(0.10 0.03 160);
54
+ --ring: oklch(0.72 0.18 162);
55
+ } */
56
+
57
+ /* ─── Ocean Dark ─── */
58
+ /* .dark {
59
+ --primary: oklch(0.68 0.16 240);
60
+ --primary-foreground: oklch(0.98 0 0);
61
+ --ring: oklch(0.68 0.16 240);
62
+ } */
63
+
64
+ /* ─── Rose Dark ─── */
65
+ /* .dark {
66
+ --primary: oklch(0.70 0.18 350);
67
+ --primary-foreground: oklch(0.98 0 0);
68
+ --ring: oklch(0.70 0.18 350);
69
+ } */
70
+
71
+ /* ─── Amber Dark ─── */
72
+ /* .dark {
73
+ --primary: oklch(0.75 0.16 70);
74
+ --primary-foreground: oklch(0.10 0.03 70);
75
+ --ring: oklch(0.75 0.16 70);
76
+ } */
77
+
78
+ /* ─── Violet Dark ─── */
79
+ /* .dark {
80
+ --primary: oklch(0.70 0.20 290);
81
+ --primary-foreground: oklch(0.98 0 0);
82
+ --ring: oklch(0.70 0.20 290);
83
+ } */
84
+
85
+ /* For full theme customization (backgrounds, borders, sidebar, etc.)
86
+ see: https://specra-docs.com/docs/v1.0.0/configuration/theming */
@@ -6,7 +6,7 @@ import type { SpecraConfig } from 'specra';
6
6
  initConfig(specraConfig as unknown as Partial<SpecraConfig>);
7
7
 
8
8
  export const prerender = true;
9
- export const trailingSlash = 'always';
9
+ export const trailingSlash = 'never';
10
10
 
11
11
  export const load: LayoutServerLoad = async () => {
12
12
  const config = getConfig();
@@ -0,0 +1,31 @@
1
+ import { getCachedVersions, getCachedAllDocs, getEffectiveConfig, getI18nConfig, getVersionsMeta, loadVersionConfig } from 'specra';
2
+ import { redirect } from '@sveltejs/kit';
3
+ import type { LayoutServerLoad } from './$types';
4
+
5
+ export const load: LayoutServerLoad = async ({ params }) => {
6
+ const { version } = params;
7
+
8
+ const i18nConfig = getI18nConfig();
9
+ const defaultLocale = i18nConfig?.defaultLocale || 'en';
10
+
11
+ // Block access to hidden versions — redirect to active version
12
+ const currentVersionConfig = loadVersionConfig(version);
13
+ if (currentVersionConfig?.hidden) {
14
+ const config = getEffectiveConfig(version);
15
+ const activeVersion = config.site?.activeVersion || 'v1.0.0';
16
+ throw redirect(302, `/docs/${activeVersion}`);
17
+ }
18
+
19
+ const allDocs = await getCachedAllDocs(version, defaultLocale);
20
+ const versions = getCachedVersions();
21
+ const config = getEffectiveConfig(version);
22
+ const versionsMeta = getVersionsMeta(versions);
23
+
24
+ return {
25
+ allDocs,
26
+ versions,
27
+ versionsMeta,
28
+ config,
29
+ versionBanner: currentVersionConfig?.banner,
30
+ };
31
+ };
@@ -2,28 +2,17 @@ import {
2
2
  extractTableOfContents,
3
3
  getAdjacentDocs,
4
4
  isCategoryPage,
5
- getCachedVersions,
6
5
  getCachedAllDocs,
7
6
  getCachedDocBySlug,
8
7
  getI18nConfig,
9
- getConfig,
10
8
  } from 'specra';
11
9
  import type { PageServerLoad } from './$types';
12
10
 
13
- export const load: PageServerLoad = async ({ params }) => {
11
+ export const load: PageServerLoad = async ({ params, parent }) => {
14
12
  const { version, slug: slugArray } = params;
15
13
  const slug = slugArray.replace(/\/$/, '');
14
+ const { allDocs, versions, config, versionsMeta, versionBanner } = await parent();
16
15
 
17
- const i18nConfig = getI18nConfig();
18
- const slugParts = slug.split('/');
19
- let locale: string | undefined;
20
- if (i18nConfig && i18nConfig.locales.includes(slugParts[0])) {
21
- locale = slugParts[0];
22
- }
23
-
24
- const allDocs = await getCachedAllDocs(version, locale);
25
- const versions = getCachedVersions();
26
- const config = getConfig();
27
16
  const isCategory = isCategoryPage(slug, allDocs);
28
17
  const doc = await getCachedDocBySlug(slug, version);
29
18
 
@@ -51,6 +40,8 @@ export const load: PageServerLoad = async ({ params }) => {
51
40
  slug,
52
41
  allDocs,
53
42
  versions,
43
+ versionsMeta,
44
+ versionBanner,
54
45
  config,
55
46
  isCategory: true,
56
47
  isNotFound: false,
@@ -74,6 +65,8 @@ export const load: PageServerLoad = async ({ params }) => {
74
65
  slug,
75
66
  allDocs,
76
67
  versions,
68
+ versionsMeta,
69
+ versionBanner,
77
70
  config,
78
71
  isCategory: false,
79
72
  isNotFound: true,
@@ -102,6 +95,8 @@ export const load: PageServerLoad = async ({ params }) => {
102
95
  slug,
103
96
  allDocs,
104
97
  versions,
98
+ versionsMeta,
99
+ versionBanner,
105
100
  config,
106
101
  isCategory: showCategoryIndex,
107
102
  isNotFound: false,
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  TableOfContents,
4
4
  Header,
5
+ TabGroups,
5
6
  DocLayout,
6
7
  CategoryIndex,
7
8
  HotReloadIndicator,
@@ -44,7 +45,19 @@
44
45
  activeTabGroup={data.categoryTabGroup}
45
46
  >
46
47
  {#snippet header()}
47
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
48
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
49
+ {#snippet subheader()}
50
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
51
+ <TabGroups
52
+ tabGroups={data.config.navigation.tabGroups}
53
+ activeTabId={data.categoryTabGroup}
54
+ docs={allDocsCompat}
55
+ version={data.version}
56
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
57
+ />
58
+ {/if}
59
+ {/snippet}
60
+ </Header>
48
61
  {/snippet}
49
62
  <CategoryIndex
50
63
  categoryPath={data.slug}
@@ -65,7 +78,19 @@
65
78
  config={data.config}
66
79
  >
67
80
  {#snippet header()}
68
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
81
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
82
+ {#snippet subheader()}
83
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
84
+ <TabGroups
85
+ tabGroups={data.config.navigation.tabGroups}
86
+ activeTabId={data.categoryTabGroup}
87
+ docs={allDocsCompat}
88
+ version={data.version}
89
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
90
+ />
91
+ {/if}
92
+ {/snippet}
93
+ </Header>
69
94
  {/snippet}
70
95
  <NotFoundContent version={data.version} />
71
96
  </MobileDocLayout>
@@ -80,7 +105,19 @@
80
105
  activeTabGroup={data.categoryTabGroup}
81
106
  >
82
107
  {#snippet header()}
83
- <Header currentVersion={data.version} versions={data.versions} config={data.config} />
108
+ <Header currentVersion={data.version} versions={data.versions} versionsMeta={data.versionsMeta} versionBanner={data.versionBanner} config={data.config}>
109
+ {#snippet subheader()}
110
+ {#if data.config.navigation?.tabGroups && data.config.navigation.tabGroups.length > 0}
111
+ <TabGroups
112
+ tabGroups={data.config.navigation.tabGroups}
113
+ activeTabId={data.categoryTabGroup}
114
+ docs={allDocsCompat}
115
+ version={data.version}
116
+ flush={data.config.navigation?.sidebarStyle === 'flush'}
117
+ />
118
+ {/if}
119
+ {/snippet}
120
+ </Header>
84
121
  {/snippet}
85
122
  {#snippet toc()}
86
123
  {#if !data.isCategory}