kitfly 0.1.2

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 (62) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -0
  4. package/VERSION +1 -0
  5. package/package.json +63 -0
  6. package/schemas/README.md +32 -0
  7. package/schemas/site.schema.json +5 -0
  8. package/schemas/theme.schema.json +5 -0
  9. package/schemas/v0/site.schema.json +172 -0
  10. package/schemas/v0/theme.schema.json +210 -0
  11. package/scripts/build-all.ts +121 -0
  12. package/scripts/build.ts +601 -0
  13. package/scripts/bundle.ts +781 -0
  14. package/scripts/dev.ts +777 -0
  15. package/scripts/generate-checksums.sh +78 -0
  16. package/scripts/release/export-release-key.sh +28 -0
  17. package/scripts/release/release-guard-tag-version.sh +79 -0
  18. package/scripts/release/sign-release-assets.sh +123 -0
  19. package/scripts/release/upload-release-assets.sh +76 -0
  20. package/scripts/release/upload-release-provenance.sh +52 -0
  21. package/scripts/release/verify-public-key.sh +48 -0
  22. package/scripts/release/verify-signatures.sh +117 -0
  23. package/scripts/version-sync.ts +82 -0
  24. package/src/__tests__/build.test.ts +240 -0
  25. package/src/__tests__/bundle.test.ts +786 -0
  26. package/src/__tests__/cli.test.ts +706 -0
  27. package/src/__tests__/crucible.test.ts +1043 -0
  28. package/src/__tests__/engine.test.ts +157 -0
  29. package/src/__tests__/init.test.ts +450 -0
  30. package/src/__tests__/pipeline.test.ts +1087 -0
  31. package/src/__tests__/productbook.test.ts +1206 -0
  32. package/src/__tests__/runbook.test.ts +974 -0
  33. package/src/__tests__/server-registry.test.ts +1251 -0
  34. package/src/__tests__/servicebook.test.ts +1248 -0
  35. package/src/__tests__/shared.test.ts +2005 -0
  36. package/src/__tests__/styles.test.ts +14 -0
  37. package/src/__tests__/theme-schema.test.ts +47 -0
  38. package/src/__tests__/theme.test.ts +554 -0
  39. package/src/cli.ts +582 -0
  40. package/src/commands/init.ts +92 -0
  41. package/src/commands/update.ts +444 -0
  42. package/src/engine.ts +20 -0
  43. package/src/logger.ts +15 -0
  44. package/src/migrations/0000_schema_versioning.ts +67 -0
  45. package/src/migrations/0001_server_port.ts +52 -0
  46. package/src/migrations/0002_brand_logo.ts +49 -0
  47. package/src/migrations/index.ts +26 -0
  48. package/src/migrations/schema.ts +24 -0
  49. package/src/server-registry.ts +405 -0
  50. package/src/shared.ts +1239 -0
  51. package/src/site/styles.css +931 -0
  52. package/src/site/template.html +193 -0
  53. package/src/templates/crucible.ts +1163 -0
  54. package/src/templates/driver.ts +876 -0
  55. package/src/templates/handbook.ts +339 -0
  56. package/src/templates/minimal.ts +139 -0
  57. package/src/templates/pipeline.ts +966 -0
  58. package/src/templates/productbook.ts +1032 -0
  59. package/src/templates/runbook.ts +829 -0
  60. package/src/templates/schema.ts +119 -0
  61. package/src/templates/servicebook.ts +1242 -0
  62. package/src/theme.ts +245 -0
@@ -0,0 +1,14 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ describe("sidebar folder indicator CSS", () => {
6
+ it("uses chevron + rotate transition for open/close state", async () => {
7
+ const css = await readFile(join(process.cwd(), "src/site/styles.css"), "utf-8");
8
+ expect(css).toContain(".sidebar-nav .nav-group::before");
9
+ expect(css).toContain('content: "›"');
10
+ expect(css).toContain("transition: transform 150ms ease");
11
+ expect(css).toContain(".sidebar-nav details[open] > .nav-group::before");
12
+ expect(css).toContain("transform: rotate(90deg)");
13
+ });
14
+ });
@@ -0,0 +1,47 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ describe("theme schema layout.sidebarWidth pattern", () => {
6
+ it("accepts supported CSS length units", async () => {
7
+ const raw = await readFile(join(process.cwd(), "schemas/v0/theme.schema.json"), "utf-8");
8
+ const schema = JSON.parse(raw) as Record<string, unknown>;
9
+ const pattern =
10
+ ((schema.properties as Record<string, unknown>).layout as Record<string, unknown>)
11
+ .properties &&
12
+ (
13
+ ((schema.properties as Record<string, unknown>).layout as Record<string, unknown>)
14
+ .properties as Record<string, unknown>
15
+ ).sidebarWidth
16
+ ? (
17
+ (
18
+ ((schema.properties as Record<string, unknown>).layout as Record<string, unknown>)
19
+ .properties as Record<string, unknown>
20
+ ).sidebarWidth as Record<string, unknown>
21
+ ).pattern
22
+ : undefined;
23
+
24
+ expect(typeof pattern).toBe("string");
25
+ const re = new RegExp(pattern as string);
26
+ expect(re.test("320px")).toBe(true);
27
+ expect(re.test("18rem")).toBe(true);
28
+ expect(re.test("75%")).toBe(true);
29
+ expect(re.test("20em")).toBe(true);
30
+ });
31
+
32
+ it("rejects unsupported width expressions", async () => {
33
+ const raw = await readFile(join(process.cwd(), "schemas/v0/theme.schema.json"), "utf-8");
34
+ const schema = JSON.parse(raw) as Record<string, unknown>;
35
+ const pattern = (
36
+ (
37
+ ((schema.properties as Record<string, unknown>).layout as Record<string, unknown>)
38
+ .properties as Record<string, unknown>
39
+ ).sidebarWidth as Record<string, unknown>
40
+ ).pattern as string;
41
+
42
+ const re = new RegExp(pattern);
43
+ expect(re.test("280")).toBe(false);
44
+ expect(re.test("calc(100% - 20px)")).toBe(false);
45
+ expect(re.test("12vw")).toBe(false);
46
+ });
47
+ });
@@ -0,0 +1,554 @@
1
+ /**
2
+ * Tests for theme loading and CSS generation
3
+ */
4
+
5
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { afterEach, describe, expect, it } from "vitest";
9
+ import { DEFAULT_THEME, generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../theme.ts";
10
+
11
+ const tempDirs: string[] = [];
12
+
13
+ async function makeTempDir(): Promise<string> {
14
+ const dir = await mkdtemp(join(tmpdir(), "kitfly-theme-test-"));
15
+ tempDirs.push(dir);
16
+ return dir;
17
+ }
18
+
19
+ afterEach(async () => {
20
+ for (const d of tempDirs) {
21
+ await rm(d, { recursive: true, force: true }).catch(() => {});
22
+ }
23
+ tempDirs.length = 0;
24
+ });
25
+
26
+ describe("DEFAULT_THEME", () => {
27
+ it("has required structure", () => {
28
+ expect(DEFAULT_THEME.name).toBe("Kitfly Default");
29
+ expect(DEFAULT_THEME.colors).toBeDefined();
30
+ expect(DEFAULT_THEME.colors.light).toBeDefined();
31
+ expect(DEFAULT_THEME.colors.dark).toBeDefined();
32
+ expect(DEFAULT_THEME.code).toBeDefined();
33
+ expect(DEFAULT_THEME.typography).toBeDefined();
34
+ });
35
+
36
+ it("has complete light color palette", () => {
37
+ const light = DEFAULT_THEME.colors.light;
38
+ expect(light.background).toBe("#ffffff");
39
+ expect(light.surface).toBe("#f5f7f8");
40
+ expect(light.text).toBe("#374151");
41
+ expect(light.textMuted).toBe("#6b7280");
42
+ expect(light.heading).toBe("#152F46");
43
+ expect(light.primary).toBe("#007182");
44
+ expect(light.primaryHover).toBe("#0a6172");
45
+ expect(light.accent).toBe("#D17059");
46
+ expect(light.border).toBe("#e5e7eb");
47
+ });
48
+
49
+ it("has complete dark color palette", () => {
50
+ const dark = DEFAULT_THEME.colors.dark;
51
+ expect(dark.background).toBe("#0d1117");
52
+ expect(dark.surface).toBe("#152F46");
53
+ expect(dark.text).toBe("#e5e7eb");
54
+ expect(dark.textMuted).toBe("#9ca3af");
55
+ expect(dark.heading).toBe("#f9fafb");
56
+ expect(dark.primary).toBe("#709EA6");
57
+ expect(dark.primaryHover).toBe("#8fb5bc");
58
+ expect(dark.accent).toBe("#e8947f");
59
+ expect(dark.border).toBe("#374151");
60
+ });
61
+
62
+ it("has code theme defaults", () => {
63
+ expect(DEFAULT_THEME.code?.light).toBe("default");
64
+ expect(DEFAULT_THEME.code?.dark).toBe("okaidia");
65
+ });
66
+
67
+ it("has typography defaults", () => {
68
+ const typo = DEFAULT_THEME.typography;
69
+ expect(typo?.body).toBe("system");
70
+ expect(typo?.headings).toBe("system");
71
+ expect(typo?.code).toBe("mono");
72
+ expect(typo?.baseSize).toBe("16px");
73
+ expect(typo?.scale).toBe("1.25");
74
+ });
75
+
76
+ it("has layout defaults", () => {
77
+ expect(DEFAULT_THEME.layout?.sidebarWidth).toBe("280px");
78
+ });
79
+ });
80
+
81
+ describe("loadTheme", () => {
82
+ it("returns DEFAULT_THEME when no theme.yaml exists", async () => {
83
+ const projectDir = await makeTempDir();
84
+ const theme = await loadTheme(projectDir);
85
+
86
+ expect(theme).toEqual(DEFAULT_THEME);
87
+ });
88
+
89
+ it("merges custom theme with defaults", async () => {
90
+ const projectDir = await makeTempDir();
91
+ const customTheme = `
92
+ name: Custom Theme
93
+ colors:
94
+ light:
95
+ background: "#f0f0f0"
96
+ primary: "#ff0000"
97
+ `;
98
+ await writeFile(join(projectDir, "theme.yaml"), customTheme);
99
+
100
+ const theme = await loadTheme(projectDir);
101
+
102
+ expect(theme.name).toBe("Custom Theme");
103
+ expect(theme.colors.light.background).toBe("#f0f0f0");
104
+ expect(theme.colors.light.primary).toBe("#ff0000");
105
+ // Should keep defaults for unspecified values
106
+ expect(theme.colors.light.text).toBe("#374151");
107
+ expect(theme.colors.dark.background).toBe("#0d1117");
108
+ });
109
+
110
+ it("parses quoted values correctly", async () => {
111
+ const projectDir = await makeTempDir();
112
+ const themeYaml = `
113
+ colors:
114
+ light:
115
+ background: "#ffffff"
116
+ text: '#333333'
117
+ `;
118
+ await writeFile(join(projectDir, "theme.yaml"), themeYaml);
119
+
120
+ const theme = await loadTheme(projectDir);
121
+
122
+ expect(theme.colors.light.background).toBe("#ffffff");
123
+ expect(theme.colors.light.text).toBe("#333333");
124
+ });
125
+
126
+ it("handles nested typography settings", async () => {
127
+ const projectDir = await makeTempDir();
128
+ const themeYaml = `
129
+ typography:
130
+ body: serif
131
+ baseSize: 18px
132
+ `;
133
+ await writeFile(join(projectDir, "theme.yaml"), themeYaml);
134
+
135
+ const theme = await loadTheme(projectDir);
136
+
137
+ expect(theme.typography?.body).toBe("serif");
138
+ expect(theme.typography?.baseSize).toBe("18px");
139
+ // Should keep other defaults
140
+ expect(theme.typography?.headings).toBe("system");
141
+ });
142
+
143
+ it("handles nested layout settings", async () => {
144
+ const projectDir = await makeTempDir();
145
+ const themeYaml = `
146
+ layout:
147
+ sidebarWidth: 320px
148
+ `;
149
+ await writeFile(join(projectDir, "theme.yaml"), themeYaml);
150
+
151
+ const theme = await loadTheme(projectDir);
152
+
153
+ expect(theme.layout?.sidebarWidth).toBe("320px");
154
+ });
155
+
156
+ it("handles code theme settings", async () => {
157
+ const projectDir = await makeTempDir();
158
+ const themeYaml = `
159
+ code:
160
+ light: solarized-light
161
+ dark: dracula
162
+ `;
163
+ await writeFile(join(projectDir, "theme.yaml"), themeYaml);
164
+
165
+ const theme = await loadTheme(projectDir);
166
+
167
+ expect(theme.code?.light).toBe("solarized-light");
168
+ expect(theme.code?.dark).toBe("dracula");
169
+ });
170
+
171
+ it("ignores comments in YAML", async () => {
172
+ const projectDir = await makeTempDir();
173
+ const themeYaml = `
174
+ # This is a comment
175
+ name: Test Theme
176
+ # Another comment
177
+ colors:
178
+ light:
179
+ # Color comment
180
+ background: "#fff"
181
+ `;
182
+ await writeFile(join(projectDir, "theme.yaml"), themeYaml);
183
+
184
+ const theme = await loadTheme(projectDir);
185
+
186
+ expect(theme.name).toBe("Test Theme");
187
+ expect(theme.colors.light.background).toBe("#fff");
188
+ });
189
+
190
+ it("handles empty lines in YAML", async () => {
191
+ const projectDir = await makeTempDir();
192
+ const themeYaml = `
193
+ name: Test Theme
194
+
195
+ colors:
196
+
197
+ light:
198
+ background: "#fff"
199
+
200
+ `;
201
+ await writeFile(join(projectDir, "theme.yaml"), themeYaml);
202
+
203
+ const theme = await loadTheme(projectDir);
204
+
205
+ expect(theme.name).toBe("Test Theme");
206
+ expect(theme.colors.light.background).toBe("#fff");
207
+ });
208
+ });
209
+
210
+ describe("generateThemeCSS", () => {
211
+ it("generates valid CSS with style tag", () => {
212
+ const css = generateThemeCSS(DEFAULT_THEME);
213
+
214
+ expect(css).toContain('<style id="kitfly-theme">');
215
+ expect(css).toContain("</style>");
216
+ });
217
+
218
+ it("includes light theme CSS variables in :root", () => {
219
+ const css = generateThemeCSS(DEFAULT_THEME);
220
+
221
+ expect(css).toContain(":root {");
222
+ expect(css).toContain("--color-bg: #ffffff");
223
+ expect(css).toContain("--color-text: #374151");
224
+ expect(css).toContain("--color-link: #007182");
225
+ });
226
+
227
+ it("includes dark theme in media query", () => {
228
+ const css = generateThemeCSS(DEFAULT_THEME);
229
+
230
+ expect(css).toContain("@media (prefers-color-scheme: dark)");
231
+ expect(css).toContain("--color-bg: #0d1117");
232
+ });
233
+
234
+ it("includes data-theme selectors for manual toggle", () => {
235
+ const css = generateThemeCSS(DEFAULT_THEME);
236
+
237
+ expect(css).toContain('[data-theme="dark"]');
238
+ expect(css).toContain('[data-theme="light"]');
239
+ });
240
+
241
+ it("includes font variables", () => {
242
+ const css = generateThemeCSS(DEFAULT_THEME);
243
+
244
+ expect(css).toContain("--font-sans:");
245
+ expect(css).toContain("--font-headings:");
246
+ expect(css).toContain("--font-mono:");
247
+ });
248
+
249
+ it("includes default sidebar width variable", () => {
250
+ const css = generateThemeCSS(DEFAULT_THEME);
251
+ expect(css).toContain("--sidebar-width: 280px");
252
+ });
253
+
254
+ it("includes custom sidebar width variable", () => {
255
+ const css = generateThemeCSS({
256
+ ...DEFAULT_THEME,
257
+ layout: { sidebarWidth: "320px" },
258
+ });
259
+ expect(css).toContain("--sidebar-width: 320px");
260
+ });
261
+
262
+ it("sets html font-size from baseSize", () => {
263
+ const css = generateThemeCSS(DEFAULT_THEME);
264
+
265
+ expect(css).toContain("html { font-size: 16px; }");
266
+ });
267
+
268
+ it("uses custom theme colors", () => {
269
+ const customTheme: Theme = {
270
+ colors: {
271
+ light: {
272
+ background: "#f0f0f0",
273
+ surface: "#e0e0e0",
274
+ text: "#111111",
275
+ heading: "#000000",
276
+ primary: "#0066cc",
277
+ border: "#cccccc",
278
+ },
279
+ dark: {
280
+ background: "#1a1a1a",
281
+ surface: "#2a2a2a",
282
+ text: "#eeeeee",
283
+ heading: "#ffffff",
284
+ primary: "#66aaff",
285
+ border: "#444444",
286
+ },
287
+ },
288
+ };
289
+
290
+ const css = generateThemeCSS(customTheme);
291
+
292
+ expect(css).toContain("--color-bg: #f0f0f0");
293
+ expect(css).toContain("--color-text: #111111");
294
+ expect(css).toContain("--color-bg: #1a1a1a");
295
+ });
296
+
297
+ it("falls back to text color when textMuted is not defined", () => {
298
+ const themeWithoutMuted: Theme = {
299
+ colors: {
300
+ light: {
301
+ background: "#ffffff",
302
+ surface: "#f5f5f5",
303
+ text: "#333333",
304
+ heading: "#000000",
305
+ primary: "#0066cc",
306
+ border: "#cccccc",
307
+ },
308
+ dark: {
309
+ background: "#1a1a1a",
310
+ surface: "#2a2a2a",
311
+ text: "#eeeeee",
312
+ heading: "#ffffff",
313
+ primary: "#66aaff",
314
+ border: "#444444",
315
+ },
316
+ },
317
+ };
318
+
319
+ const css = generateThemeCSS(themeWithoutMuted);
320
+
321
+ // textMuted should fall back to text color
322
+ expect(css).toContain("--color-text-muted: #333333");
323
+ });
324
+
325
+ it("falls back to primary color when primaryHover is not defined", () => {
326
+ const themeWithoutHover: Theme = {
327
+ colors: {
328
+ light: {
329
+ background: "#ffffff",
330
+ surface: "#f5f5f5",
331
+ text: "#333333",
332
+ heading: "#000000",
333
+ primary: "#0066cc",
334
+ border: "#cccccc",
335
+ },
336
+ dark: {
337
+ background: "#1a1a1a",
338
+ surface: "#2a2a2a",
339
+ text: "#eeeeee",
340
+ heading: "#ffffff",
341
+ primary: "#66aaff",
342
+ border: "#444444",
343
+ },
344
+ },
345
+ };
346
+
347
+ const css = generateThemeCSS(themeWithoutHover);
348
+
349
+ // primaryHover should fall back to primary color
350
+ expect(css).toContain("--color-link-hover: #0066cc");
351
+ });
352
+
353
+ it("uses custom typography settings", () => {
354
+ const customTheme: Theme = {
355
+ colors: DEFAULT_THEME.colors,
356
+ typography: {
357
+ body: "serif",
358
+ headings: "readable",
359
+ baseSize: "18px",
360
+ },
361
+ };
362
+
363
+ const css = generateThemeCSS(customTheme);
364
+
365
+ expect(css).toContain("html { font-size: 18px; }");
366
+ // Should include serif font stack for body
367
+ expect(css).toContain("Georgia");
368
+ // Should include readable font stack for headings
369
+ expect(css).toContain("Charter");
370
+ });
371
+ });
372
+
373
+ describe("getPrismUrls", () => {
374
+ it("returns default URLs for DEFAULT_THEME", () => {
375
+ const urls = getPrismUrls(DEFAULT_THEME);
376
+
377
+ expect(urls.light).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css");
378
+ expect(urls.dark).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css");
379
+ });
380
+
381
+ it("returns correct URLs for standard Prism themes", () => {
382
+ const theme: Theme = {
383
+ colors: DEFAULT_THEME.colors,
384
+ code: {
385
+ light: "coy",
386
+ dark: "tomorrow",
387
+ },
388
+ };
389
+
390
+ const urls = getPrismUrls(theme);
391
+
392
+ expect(urls.light).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-coy.min.css");
393
+ expect(urls.dark).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-tomorrow.min.css");
394
+ });
395
+
396
+ it("returns correct URLs for prism-themes package", () => {
397
+ const theme: Theme = {
398
+ colors: DEFAULT_THEME.colors,
399
+ code: {
400
+ light: "nord",
401
+ dark: "dracula",
402
+ },
403
+ };
404
+
405
+ const urls = getPrismUrls(theme);
406
+
407
+ expect(urls.light).toBe(
408
+ "https://cdn.jsdelivr.net/npm/prism-themes@1/themes/prism-nord.min.css",
409
+ );
410
+ expect(urls.dark).toBe(
411
+ "https://cdn.jsdelivr.net/npm/prism-themes@1/themes/prism-dracula.min.css",
412
+ );
413
+ });
414
+
415
+ it("falls back to defaults for unknown themes", () => {
416
+ const theme: Theme = {
417
+ colors: DEFAULT_THEME.colors,
418
+ code: {
419
+ light: "nonexistent-theme",
420
+ dark: "also-nonexistent",
421
+ },
422
+ };
423
+
424
+ const urls = getPrismUrls(theme);
425
+
426
+ expect(urls.light).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css");
427
+ expect(urls.dark).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css");
428
+ });
429
+
430
+ it("falls back to defaults when code config is missing", () => {
431
+ const theme: Theme = {
432
+ colors: DEFAULT_THEME.colors,
433
+ };
434
+
435
+ const urls = getPrismUrls(theme);
436
+
437
+ expect(urls.light).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css");
438
+ expect(urls.dark).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css");
439
+ });
440
+
441
+ it("handles partial code config", () => {
442
+ const themeWithOnlyLight: Theme = {
443
+ colors: DEFAULT_THEME.colors,
444
+ code: {
445
+ light: "solarized-light",
446
+ },
447
+ };
448
+
449
+ const urls = getPrismUrls(themeWithOnlyLight);
450
+
451
+ expect(urls.light).toBe(
452
+ "https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-solarizedlight.min.css",
453
+ );
454
+ expect(urls.dark).toBe("https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css");
455
+ });
456
+
457
+ it("supports all documented Prism themes", () => {
458
+ const supportedThemes = [
459
+ "default",
460
+ "coy",
461
+ "solarized-light",
462
+ "tomorrow",
463
+ "okaidia",
464
+ "tomorrow-night",
465
+ "nord",
466
+ "dracula",
467
+ "one-dark",
468
+ "synthwave84",
469
+ ];
470
+
471
+ for (const themeName of supportedThemes) {
472
+ const theme: Theme = {
473
+ colors: DEFAULT_THEME.colors,
474
+ code: { light: themeName, dark: themeName },
475
+ };
476
+
477
+ const urls = getPrismUrls(theme);
478
+
479
+ expect(urls.light).toContain("cdn.jsdelivr.net");
480
+ expect(urls.dark).toContain("cdn.jsdelivr.net");
481
+ }
482
+ });
483
+ });
484
+
485
+ describe("FONT_STACKS mapping", () => {
486
+ it("system font stack is used by default", () => {
487
+ const css = generateThemeCSS(DEFAULT_THEME);
488
+
489
+ expect(css).toContain("-apple-system");
490
+ expect(css).toContain("BlinkMacSystemFont");
491
+ expect(css).toContain("Segoe UI");
492
+ });
493
+
494
+ it("serif font stack contains proper fonts", () => {
495
+ const theme: Theme = {
496
+ colors: DEFAULT_THEME.colors,
497
+ typography: { body: "serif" },
498
+ };
499
+
500
+ const css = generateThemeCSS(theme);
501
+
502
+ expect(css).toContain("Georgia");
503
+ expect(css).toContain("Times New Roman");
504
+ });
505
+
506
+ it("readable font stack contains Charter", () => {
507
+ const theme: Theme = {
508
+ colors: DEFAULT_THEME.colors,
509
+ typography: { body: "readable" },
510
+ };
511
+
512
+ const css = generateThemeCSS(theme);
513
+
514
+ expect(css).toContain("Charter");
515
+ expect(css).toContain("Bitstream Charter");
516
+ });
517
+
518
+ it("mono font stack is always included", () => {
519
+ const css = generateThemeCSS(DEFAULT_THEME);
520
+
521
+ expect(css).toContain("--font-mono:");
522
+ expect(css).toContain("ui-monospace");
523
+ expect(css).toContain("SFMono-Regular");
524
+ expect(css).toContain("Menlo");
525
+ expect(css).toContain("Consolas");
526
+ });
527
+ });
528
+
529
+ describe("PRISM_THEMES mapping", () => {
530
+ it("default theme uses prismjs package", () => {
531
+ const urls = getPrismUrls({ colors: DEFAULT_THEME.colors, code: { light: "default" } });
532
+ expect(urls.light).toContain("prismjs@1/themes/prism.min.css");
533
+ });
534
+
535
+ it("nord theme uses prism-themes package", () => {
536
+ const urls = getPrismUrls({ colors: DEFAULT_THEME.colors, code: { dark: "nord" } });
537
+ expect(urls.dark).toContain("prism-themes@1/themes/prism-nord.min.css");
538
+ });
539
+
540
+ it("dracula theme uses prism-themes package", () => {
541
+ const urls = getPrismUrls({ colors: DEFAULT_THEME.colors, code: { dark: "dracula" } });
542
+ expect(urls.dark).toContain("prism-themes@1/themes/prism-dracula.min.css");
543
+ });
544
+
545
+ it("one-dark theme uses prism-themes package", () => {
546
+ const urls = getPrismUrls({ colors: DEFAULT_THEME.colors, code: { dark: "one-dark" } });
547
+ expect(urls.dark).toContain("prism-themes@1/themes/prism-one-dark.min.css");
548
+ });
549
+
550
+ it("synthwave84 theme uses prism-themes package", () => {
551
+ const urls = getPrismUrls({ colors: DEFAULT_THEME.colors, code: { dark: "synthwave84" } });
552
+ expect(urls.dark).toContain("prism-themes@1/themes/prism-synthwave84.min.css");
553
+ });
554
+ });