@decocms/start 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/scripts/deco-migrate-cli.ts +444 -0
  3. package/scripts/migrate/analyzers/island-classifier.ts +73 -0
  4. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  5. package/scripts/migrate/analyzers/section-metadata.ts +91 -0
  6. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  7. package/scripts/migrate/phase-analyze.ts +147 -17
  8. package/scripts/migrate/phase-cleanup.ts +124 -2
  9. package/scripts/migrate/phase-report.ts +44 -16
  10. package/scripts/migrate/phase-scaffold.ts +38 -132
  11. package/scripts/migrate/phase-transform.ts +28 -3
  12. package/scripts/migrate/phase-verify.ts +127 -5
  13. package/scripts/migrate/templates/app-css.ts +204 -0
  14. package/scripts/migrate/templates/cache-config.ts +26 -0
  15. package/scripts/migrate/templates/commerce-loaders.ts +124 -0
  16. package/scripts/migrate/templates/hooks.ts +358 -0
  17. package/scripts/migrate/templates/package-json.ts +29 -6
  18. package/scripts/migrate/templates/routes.ts +41 -136
  19. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  20. package/scripts/migrate/templates/section-loaders.ts +108 -0
  21. package/scripts/migrate/templates/server-entry.ts +174 -67
  22. package/scripts/migrate/templates/setup.ts +64 -55
  23. package/scripts/migrate/templates/types-gen.ts +119 -0
  24. package/scripts/migrate/templates/ui-components.ts +113 -0
  25. package/scripts/migrate/templates/vite-config.ts +18 -1
  26. package/scripts/migrate/templates/wrangler.ts +4 -1
  27. package/scripts/migrate/transforms/dead-code.ts +23 -2
  28. package/scripts/migrate/transforms/imports.ts +40 -10
  29. package/scripts/migrate/transforms/jsx.ts +9 -0
  30. package/scripts/migrate/transforms/section-conventions.ts +83 -0
  31. package/scripts/migrate/types.ts +74 -0
  32. package/src/cms/resolve.ts +10 -0
  33. package/src/routes/cmsRoute.ts +13 -0
@@ -10,6 +10,15 @@ import { generateKnipConfig } from "./templates/knip-config.ts";
10
10
  import { generateRoutes } from "./templates/routes.ts";
11
11
  import { generateSetup } from "./templates/setup.ts";
12
12
  import { generateServerEntry } from "./templates/server-entry.ts";
13
+ import { generateAppCss } from "./templates/app-css.ts";
14
+ import { generateTypeFiles } from "./templates/types-gen.ts";
15
+ import { generateUiComponents } from "./templates/ui-components.ts";
16
+ import { generateHooks } from "./templates/hooks.ts";
17
+ import { generateCommerceLoaders } from "./templates/commerce-loaders.ts";
18
+ import { generateSectionLoaders } from "./templates/section-loaders.ts";
19
+ import { generateCacheConfig } from "./templates/cache-config.ts";
20
+ import { generateSdkFiles } from "./templates/sdk-gen.ts";
21
+ import { extractTheme } from "./analyzers/theme-extractor.ts";
13
22
 
14
23
  function writeFile(ctx: MigrationContext, relPath: string, content: string) {
15
24
  const fullPath = path.join(ctx.sourceDir, relPath);
@@ -20,7 +29,6 @@ function writeFile(ctx: MigrationContext, relPath: string, content: string) {
20
29
  return;
21
30
  }
22
31
 
23
- // Ensure directory exists
24
32
  const dir = path.dirname(fullPath);
25
33
  fs.mkdirSync(dir, { recursive: true });
26
34
 
@@ -29,6 +37,12 @@ function writeFile(ctx: MigrationContext, relPath: string, content: string) {
29
37
  ctx.scaffoldedFiles.push(relPath);
30
38
  }
31
39
 
40
+ function writeMultiFile(ctx: MigrationContext, files: Record<string, string>) {
41
+ for (const [filePath, content] of Object.entries(files)) {
42
+ writeFile(ctx, filePath, content);
43
+ }
44
+ }
45
+
32
46
  export function scaffold(ctx: MigrationContext): void {
33
47
  logPhase("Scaffold");
34
48
 
@@ -47,38 +61,41 @@ export function scaffold(ctx: MigrationContext): void {
47
61
  tabWidth: 2,
48
62
  }, null, 2) + "\n");
49
63
 
50
- // Server entry files
51
- const serverEntryFiles = generateServerEntry(ctx);
52
- for (const [filePath, content] of Object.entries(serverEntryFiles)) {
53
- writeFile(ctx, filePath, content);
54
- }
64
+ // Server entry files (server.ts, worker-entry.ts, router.tsx, runtime.ts, context.ts)
65
+ writeMultiFile(ctx, generateServerEntry(ctx));
55
66
 
56
67
  // Route files
57
- const routeFiles = generateRoutes(ctx);
58
- for (const [filePath, content] of Object.entries(routeFiles)) {
59
- writeFile(ctx, filePath, content);
60
- }
68
+ writeMultiFile(ctx, generateRoutes(ctx));
61
69
 
62
- // Setup
70
+ // Setup infrastructure
63
71
  writeFile(ctx, "src/setup.ts", generateSetup(ctx));
72
+ writeFile(ctx, "src/cache-config.ts", generateCacheConfig(ctx));
73
+ writeFile(ctx, "src/setup/commerce-loaders.ts", generateCommerceLoaders(ctx));
74
+ writeFile(ctx, "src/setup/section-loaders.ts", generateSectionLoaders(ctx));
64
75
 
65
- // Styles
66
- writeFile(ctx, "src/styles/app.css", generateAppCss(ctx));
76
+ // Theme extraction + Styles
77
+ const theme = extractTheme(ctx);
78
+ writeFile(ctx, "src/styles/app.css", generateAppCss(ctx, theme));
67
79
 
68
- // SDK — signal shim (replaces @preact/signals)
69
- writeFile(ctx, "src/sdk/signal.ts", generateSignalShim());
80
+ // Type definitions
81
+ writeMultiFile(ctx, generateTypeFiles(ctx));
70
82
 
71
- // SDK clx (class name joiner, with default export for compat)
72
- writeFile(ctx, "src/sdk/clx.ts", generateClxShim());
83
+ // UI components (Image, Picture, Video)
84
+ writeMultiFile(ctx, generateUiComponents(ctx));
73
85
 
74
- // SDK debounce (replaces Deno std/async/debounce)
86
+ // Platform hooks (useCart, useUser, useWishlist)
87
+ writeMultiFile(ctx, generateHooks(ctx));
88
+
89
+ // SDK shims + generated utilities
90
+ writeFile(ctx, "src/sdk/signal.ts", generateSignalShim());
91
+ writeFile(ctx, "src/sdk/clx.ts", generateClxShim());
75
92
  writeFile(ctx, "src/sdk/debounce.ts", generateDebounceShim());
93
+ writeMultiFile(ctx, generateSdkFiles(ctx));
76
94
 
77
95
  // Apps
78
96
  writeFile(ctx, "src/apps/site.ts", generateSiteApp(ctx));
79
97
 
80
98
  // SiteTheme component (replaces apps/website/components/Theme.tsx)
81
- // Check if any source file uses SiteTheme
82
99
  const usesSiteTheme = ctx.files.some((f) => {
83
100
  if (f.action === "delete") return false;
84
101
  try {
@@ -112,10 +129,6 @@ export interface Props {
112
129
  variables?: Array<{ name: string; value: string }>;
113
130
  }
114
131
 
115
- /**
116
- * SiteTheme — injects CSS custom properties and font stylesheets into the page.
117
- * This replaces the old apps/website/components/Theme.tsx from the Deno stack.
118
- */
119
132
  export default function SiteTheme({ variables, fonts, colorScheme }: Props) {
120
133
  const cssVars = variables?.length
121
134
  ? \`:root { \${variables.map((v) => \`\${v.name}: \${v.value};\`).join(" ")} }\`
@@ -143,57 +156,6 @@ export { type Font as SiteThemeFont };
143
156
  `;
144
157
  }
145
158
 
146
- function generateAppCss(ctx: MigrationContext): string {
147
- const c = ctx.themeColors;
148
- // Map CMS color names to DaisyUI v5 CSS variables
149
- const colors: Record<string, string> = {
150
- "--color-primary": c["primary"] || "#6B21A8",
151
- "--color-secondary": c["secondary"] || "#141414",
152
- "--color-accent": c["tertiary"] || "#FFF100",
153
- "--color-neutral": c["neutral"] || "#393939",
154
- "--color-base-100": c["base-100"] || "#FFFFFF",
155
- "--color-base-200": c["base-200"] || "#F3F3F3",
156
- "--color-base-300": c["base-300"] || "#868686",
157
- "--color-info": c["info"] || "#006CA1",
158
- "--color-success": c["success"] || "#007552",
159
- "--color-warning": c["warning"] || "#F8D13A",
160
- "--color-error": c["error"] || "#CF040A",
161
- };
162
- // Add content colors if specified
163
- if (c["primary-content"]) colors["--color-primary-content"] = c["primary-content"];
164
- if (c["secondary-content"]) colors["--color-secondary-content"] = c["secondary-content"];
165
- if (c["base-content"]) colors["--color-base-content"] = c["base-content"];
166
-
167
- const colorLines = Object.entries(colors)
168
- .map(([k, v]) => ` ${k}: ${v};`)
169
- .join("\n");
170
-
171
- return `@import "tailwindcss";
172
- @plugin "daisyui";
173
- @plugin "daisyui/theme" {
174
- name: "light";
175
- default: true;
176
- color-scheme: light;
177
-
178
- ${colorLines}
179
- }
180
-
181
- @theme {
182
- --color-*: initial;
183
- --color-white: #fff;
184
- --color-black: #000;
185
- --color-transparent: transparent;
186
- --color-current: currentColor;
187
- --color-inherit: inherit;${ctx.fontFamily ? `\n --font-sans: "${ctx.fontFamily}", ui-sans-serif, system-ui, sans-serif;` : ""}
188
- }
189
-
190
- /* View transitions */
191
- @view-transition {
192
- navigation: auto;
193
- }
194
- `;
195
- }
196
-
197
159
  function generateGitignore(): string {
198
160
  return `# Dependencies
199
161
  node_modules/
@@ -224,9 +186,6 @@ vite.config.timestamp_*
224
186
  # Deco CMS
225
187
  .deco/metadata/*
226
188
 
227
- # Bun lock file (if using npm, keep package-lock.json instead)
228
- # package-lock.json
229
-
230
189
  # IDE
231
190
  .vscode/
232
191
  .idea/
@@ -276,66 +235,13 @@ export default debounce;
276
235
  }
277
236
 
278
237
  function generateSignalShim(): string {
279
- return `import { Store } from "@tanstack/store";
280
- import { useSyncExternalStore, useMemo, useEffect } from "react";
281
-
282
- export interface Signal<T> {
283
- readonly store: Store<T>;
284
- value: T;
285
- peek(): T;
286
- subscribe(fn: () => void): () => void;
287
- }
288
-
289
- export function signal<T>(initialValue: T): Signal<T> {
290
- const store = new Store<T>(initialValue);
291
- return {
292
- store,
293
- get value() { return store.state; },
294
- set value(v: T) { store.setState(() => v); },
295
- peek() { return store.state; },
296
- subscribe(fn) {
297
- // @tanstack/store@0.9.x returns { unsubscribe: Function },
298
- // NOT a plain function. React's useSyncExternalStore cleanup
299
- // expects a bare function — unwrap it.
300
- const sub = store.subscribe(() => fn());
301
- return typeof sub === "function" ? sub : sub.unsubscribe;
302
- },
303
- };
304
- }
305
-
306
- export function useSignal<T>(initialValue: T): Signal<T> {
307
- const sig = useMemo(() => signal(initialValue), []);
308
- useSyncExternalStore(
309
- (cb) => sig.subscribe(cb),
310
- () => sig.value,
311
- () => sig.value,
312
- );
313
- return sig;
314
- }
315
-
316
- export function useComputed<T>(fn: () => T): Signal<T> {
317
- const sig = useMemo(() => signal(fn()), [fn]);
318
- return sig;
319
- }
320
-
321
- export function computed<T>(fn: () => T): Signal<T> {
322
- return signal(fn());
323
- }
238
+ return `export { signal, type ReactiveSignal } from "@decocms/start/sdk/signal";
324
239
 
240
+ /** Run a function immediately. Kept for legacy module-level side effects. */
325
241
  export function effect(fn: () => void | (() => void)): () => void {
326
242
  const cleanup = fn();
327
243
  return typeof cleanup === "function" ? cleanup : () => {};
328
244
  }
329
-
330
- export function batch(fn: () => void): void {
331
- fn();
332
- }
333
-
334
- export function useSignalEffect(fn: () => void | (() => void)): void {
335
- useEffect(fn);
336
- }
337
-
338
- export type { Signal as ReadonlySignal };
339
245
  `;
340
246
  }
341
247
 
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext, TransformResult } from "./types.ts";
3
+ import type { MigrationContext, TransformResult, SectionMeta } from "./types.ts";
4
4
  import { log, logPhase } from "./types.ts";
5
5
  import { transformImports } from "./transforms/imports.ts";
6
6
  import { transformJsx } from "./transforms/jsx.ts";
@@ -8,11 +8,25 @@ import { transformFreshApis } from "./transforms/fresh-apis.ts";
8
8
  import { transformDenoIsms } from "./transforms/deno-isms.ts";
9
9
  import { transformTailwind } from "./transforms/tailwind.ts";
10
10
  import { transformDeadCode } from "./transforms/dead-code.ts";
11
+ import { transformSectionConventions } from "./transforms/section-conventions.ts";
12
+
13
+ /** Map of section path → metadata, populated per-run */
14
+ let sectionMetaMap: Map<string, SectionMeta> | null = null;
15
+
16
+ function getSectionMeta(ctx: MigrationContext, relPath: string): SectionMeta | undefined {
17
+ if (!sectionMetaMap) {
18
+ sectionMetaMap = new Map();
19
+ for (const m of ctx.sectionMetas) {
20
+ sectionMetaMap.set(m.path, m);
21
+ }
22
+ }
23
+ return sectionMetaMap.get(relPath);
24
+ }
11
25
 
12
26
  /**
13
27
  * Apply all transforms to a file's content in the correct order.
14
28
  */
15
- function applyTransforms(content: string, filePath: string): TransformResult {
29
+ function applyTransforms(content: string, filePath: string, ctx?: MigrationContext, relPath?: string): TransformResult {
16
30
  const allNotes: string[] = [];
17
31
  let currentContent = content;
18
32
  let anyChanged = false;
@@ -42,6 +56,17 @@ function applyTransforms(content: string, filePath: string): TransformResult {
42
56
  }
43
57
  }
44
58
 
59
+ // Section conventions (sync/eager/layout/cache) — only for section files
60
+ if (ctx && relPath && relPath.startsWith("sections/")) {
61
+ const meta = getSectionMeta(ctx, relPath);
62
+ const result = transformSectionConventions(currentContent, meta);
63
+ if (result.changed) {
64
+ anyChanged = true;
65
+ currentContent = result.content;
66
+ allNotes.push(...result.notes.map((n) => `[section-conventions] ${n}`));
67
+ }
68
+ }
69
+
45
70
  return { content: currentContent, changed: anyChanged, notes: allNotes };
46
71
  }
47
72
 
@@ -59,7 +84,7 @@ export function transform(ctx: MigrationContext): void {
59
84
  const content = fs.readFileSync(absPath, "utf-8");
60
85
 
61
86
  // Apply transforms
62
- const result = applyTransforms(content, absPath);
87
+ const result = applyTransforms(content, absPath, ctx, record.path);
63
88
 
64
89
  // Add manual review items
65
90
  for (const note of result.notes) {
@@ -22,8 +22,19 @@ const REQUIRED_FILES = [
22
22
  "src/runtime.ts",
23
23
  "src/context.ts",
24
24
  "src/setup.ts",
25
+ "src/cache-config.ts",
26
+ "src/setup/commerce-loaders.ts",
27
+ "src/setup/section-loaders.ts",
25
28
  "src/styles/app.css",
26
29
  "src/apps/site.ts",
30
+ "src/hooks/useCart.ts",
31
+ "src/hooks/useUser.ts",
32
+ "src/hooks/useWishlist.ts",
33
+ "src/types/widgets.ts",
34
+ "src/types/deco.ts",
35
+ "src/types/commerce-app.ts",
36
+ "src/components/ui/Image.tsx",
37
+ "src/components/ui/Picture.tsx",
27
38
  "src/routes/__root.tsx",
28
39
  "src/routes/index.tsx",
29
40
  "src/routes/$.tsx",
@@ -38,8 +49,8 @@ const MUST_NOT_EXIST = [
38
49
  "manifest.gen.ts",
39
50
  "dev.ts",
40
51
  "main.ts",
41
- "islands/BlogFeed.tsx",
42
52
  "routes/_app.tsx",
53
+ "routes/_middleware.ts",
43
54
  ];
44
55
 
45
56
  const checks: Check[] = [
@@ -261,14 +272,16 @@ const checks: Check[] = [
261
272
  },
262
273
  },
263
274
  {
264
- name: "No dead cache/cacheKey exports",
275
+ name: "No dead cache/cacheKey exports (old SWR system)",
265
276
  severity: "warning",
266
277
  fn: (ctx) => {
267
278
  const srcDir = path.join(ctx.sourceDir, "src");
268
279
  if (!fs.existsSync(srcDir)) return true;
269
- const bad = findFilesWithPattern(srcDir, /^export\s+const\s+(?:cache|cacheKey)\s*=/m);
280
+ // Only flag the OLD cache patterns: cache = "stale-while-revalidate" or cache = { maxAge: ... }
281
+ // NOT the new-stack section convention: cache = "listing" / "product" / "search" / "static"
282
+ const bad = findFilesWithPattern(srcDir, /^export\s+const\s+cacheKey\s*=/m);
270
283
  if (bad.length > 0) {
271
- console.log(` Dead exports found (old cache system): ${bad.join(", ")}`);
284
+ console.log(` Dead cacheKey exports found: ${bad.join(", ")}`);
272
285
  return false;
273
286
  }
274
287
  return true;
@@ -339,6 +352,110 @@ const checks: Check[] = [
339
352
  return true;
340
353
  },
341
354
  },
355
+ {
356
+ name: "No @deco/deco imports in src/",
357
+ severity: "error",
358
+ fn: (ctx) => {
359
+ const srcDir = path.join(ctx.sourceDir, "src");
360
+ if (!fs.existsSync(srcDir)) return true;
361
+ const bad = findFilesWithPattern(srcDir, /from\s+["']@deco\/deco/);
362
+ if (bad.length > 0) {
363
+ console.log(` Still has @deco/deco imports: ${bad.join(", ")}`);
364
+ return false;
365
+ }
366
+ return true;
367
+ },
368
+ },
369
+ {
370
+ name: "No apps/ imports in src/ (should be @decocms/apps or ~/)",
371
+ severity: "error",
372
+ fn: (ctx) => {
373
+ const srcDir = path.join(ctx.sourceDir, "src");
374
+ if (!fs.existsSync(srcDir)) return true;
375
+ const bad = findFilesWithPattern(srcDir, /from\s+["']apps\//);
376
+ if (bad.length > 0) {
377
+ console.log(` Still has apps/ imports: ${bad.join(", ")}`);
378
+ return false;
379
+ }
380
+ return true;
381
+ },
382
+ },
383
+ {
384
+ name: "Setup infrastructure is complete",
385
+ severity: "error",
386
+ fn: (ctx) => {
387
+ const setupFiles = [
388
+ "src/setup.ts",
389
+ "src/cache-config.ts",
390
+ "src/setup/commerce-loaders.ts",
391
+ "src/setup/section-loaders.ts",
392
+ ];
393
+ const missing = setupFiles.filter(
394
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
395
+ );
396
+ if (missing.length > 0) {
397
+ console.log(` Missing setup infrastructure: ${missing.join(", ")}`);
398
+ return false;
399
+ }
400
+ return true;
401
+ },
402
+ },
403
+ {
404
+ name: "No islands/ directory (should be eliminated)",
405
+ severity: "warning",
406
+ fn: (ctx) => {
407
+ const islandsDir = path.join(ctx.sourceDir, "src", "islands");
408
+ if (fs.existsSync(islandsDir)) {
409
+ try {
410
+ const files = fs.readdirSync(islandsDir, { recursive: true });
411
+ const tsxFiles = (files as string[]).filter((f: string) => f.endsWith(".tsx") || f.endsWith(".ts"));
412
+ if (tsxFiles.length > 0) {
413
+ console.log(` src/islands/ still has ${tsxFiles.length} files — should be moved to components/`);
414
+ return false;
415
+ }
416
+ } catch {}
417
+ }
418
+ return true;
419
+ },
420
+ },
421
+ {
422
+ name: "Hooks are scaffolded",
423
+ severity: "warning",
424
+ fn: (ctx) => {
425
+ const hookFiles = [
426
+ "src/hooks/useCart.ts",
427
+ "src/hooks/useUser.ts",
428
+ "src/hooks/useWishlist.ts",
429
+ ];
430
+ const missing = hookFiles.filter(
431
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
432
+ );
433
+ if (missing.length > 0) {
434
+ console.log(` Missing hooks: ${missing.join(", ")}`);
435
+ return false;
436
+ }
437
+ return true;
438
+ },
439
+ },
440
+ {
441
+ name: "Type files are scaffolded",
442
+ severity: "warning",
443
+ fn: (ctx) => {
444
+ const typeFiles = [
445
+ "src/types/widgets.ts",
446
+ "src/types/deco.ts",
447
+ "src/types/commerce-app.ts",
448
+ ];
449
+ const missing = typeFiles.filter(
450
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
451
+ );
452
+ if (missing.length > 0) {
453
+ console.log(` Missing type files: ${missing.join(", ")}`);
454
+ return false;
455
+ }
456
+ return true;
457
+ },
458
+ },
342
459
  ];
343
460
 
344
461
  function findFilesWithPattern(
@@ -356,7 +473,12 @@ function findFilesWithPattern(
356
473
  findFilesWithPattern(fullPath, pattern, results, root);
357
474
  } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
358
475
  const content = fs.readFileSync(fullPath, "utf-8");
359
- if (pattern.test(content)) {
476
+ // Only test non-comment lines
477
+ const uncommented = content
478
+ .split("\n")
479
+ .filter((line) => !line.trimStart().startsWith("//") && !line.trimStart().startsWith("*"))
480
+ .join("\n");
481
+ if (pattern.test(uncommented)) {
360
482
  results.push(path.basename(fullPath));
361
483
  }
362
484
  }
@@ -0,0 +1,204 @@
1
+ import type { MigrationContext } from "../types.ts";
2
+ import type { ExtractedTheme } from "../analyzers/theme-extractor.ts";
3
+
4
+ export function generateAppCss(ctx: MigrationContext, theme?: ExtractedTheme): string {
5
+ const sections: string[] = [];
6
+
7
+ // ── DaisyUI theme plugin ──────────────────────────────────────────
8
+ const daisyColors = theme?.daisyUiColors ?? {};
9
+ const c = ctx.themeColors;
10
+
11
+ const semanticColors: Record<string, string> = {
12
+ "--color-primary": daisyColors["--color-primary"] || c["primary"] || "#B10200",
13
+ "--color-secondary": daisyColors["--color-secondary"] || c["secondary"] || "#141414",
14
+ "--color-accent": daisyColors["--color-accent"] || c["tertiary"] || "#FFF100",
15
+ "--color-neutral": daisyColors["--color-neutral"] || c["neutral"] || "#393939",
16
+ "--color-base-100": daisyColors["--color-base-100"] || c["base-100"] || "#FFFFFF",
17
+ "--color-base-200": daisyColors["--color-base-200"] || c["base-200"] || "#F3F3F3",
18
+ "--color-base-300": daisyColors["--color-base-300"] || c["base-300"] || "#868686",
19
+ "--color-info": daisyColors["--color-info"] || c["info"] || "#006CA1",
20
+ "--color-success": daisyColors["--color-success"] || c["success"] || "#007552",
21
+ "--color-warning": daisyColors["--color-warning"] || c["warning"] || "#F8D13A",
22
+ "--color-error": daisyColors["--color-error"] || c["error"] || "#CF040A",
23
+ };
24
+
25
+ const colorLines = Object.entries(semanticColors)
26
+ .map(([k, v]) => ` ${k}: ${v};`)
27
+ .join("\n");
28
+
29
+ sections.push(`@import "tailwindcss";
30
+ @plugin "daisyui";
31
+ @plugin "daisyui/theme" {
32
+ name: "light";
33
+ default: true;
34
+ color-scheme: light;
35
+ ${colorLines}
36
+ }`);
37
+
38
+ // ── @theme block: Tailwind v3->v4 color migration ─────────────────
39
+ const fontFamily = theme?.fontFamily || ctx.fontFamily;
40
+ let fontLine = "";
41
+ if (fontFamily) {
42
+ const firstFont = fontFamily.split(",")[0].trim().replace(/['"]/g, "");
43
+ fontLine = `\n --font-sans: "${firstFont}", ui-sans-serif, system-ui, sans-serif;`;
44
+ }
45
+
46
+ let themeBlock = `/* Tailwind v4: reset default palette (old sites replaced it entirely via theme.colors)
47
+ then re-add only the colors used by this site. */
48
+ @theme {
49
+ --color-*: initial;
50
+
51
+ --color-white: #fff;
52
+ --color-black: #000;
53
+ --color-transparent: transparent;
54
+ --color-current: currentColor;
55
+ --color-inherit: inherit;${fontLine}`;
56
+
57
+ // Add extracted theme variables to @theme
58
+ const vars = theme?.variables ?? {};
59
+ if (Object.keys(vars).length > 0) {
60
+ themeBlock += `\n`;
61
+ const grouped: Record<string, Array<[string, string]>> = {};
62
+ for (const [k, v] of Object.entries(vars)) {
63
+ const prefix = k.replace(/^--/, "").split("-").slice(0, 2).join("-");
64
+ if (!grouped[prefix]) grouped[prefix] = [];
65
+ grouped[prefix].push([k, v]);
66
+ }
67
+
68
+ for (const [prefix, entries] of Object.entries(grouped)) {
69
+ themeBlock += `\n /* ${prefix} */`;
70
+ for (const [k, v] of entries) {
71
+ themeBlock += `\n ${k}: ${v};`;
72
+ }
73
+ }
74
+ }
75
+
76
+ // Gray scale compat (Tailwind v3 had gray-50..gray-950 by default)
77
+ themeBlock += `
78
+
79
+ /* Gray scale (Tailwind v3 default, required for bg-gray-*, text-gray-*, etc.) */
80
+ --color-gray-50: #f9fafb;
81
+ --color-gray-100: #f3f4f6;
82
+ --color-gray-200: #e5e7eb;
83
+ --color-gray-300: #d1d5db;
84
+ --color-gray-400: #9ca3af;
85
+ --color-gray-500: #6b7280;
86
+ --color-gray-600: #4b5563;
87
+ --color-gray-700: #374151;
88
+ --color-gray-800: #1f2937;
89
+ --color-gray-900: #111827;
90
+ --color-gray-950: #030712;
91
+ }`;
92
+ sections.push(themeBlock);
93
+
94
+ // ── DaisyUI v5 compat ─────────────────────────────────────────────
95
+ sections.push(`/* DaisyUI v5: flatten depth/noise to match v4 look */
96
+ :root {
97
+ --depth: 0;
98
+ --noise: 0;
99
+ }`);
100
+
101
+ // ── :root theme variables ─────────────────────────────────────────
102
+ if (Object.keys(vars).length > 0) {
103
+ let rootBlock = `:root {\n`;
104
+ for (const [k, v] of Object.entries(vars)) {
105
+ rootBlock += ` ${k}: ${v};\n`;
106
+ }
107
+ if (fontFamily) {
108
+ const firstFont = fontFamily.split(",")[0].trim().replace(/['"]/g, "");
109
+ rootBlock += ` --font-family: ${firstFont}, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;\n`;
110
+ }
111
+ rootBlock += `}`;
112
+ sections.push(rootBlock);
113
+ }
114
+
115
+ // ── DaisyUI v5 carousel overflow fix ──────────────────────────────
116
+ sections.push(`/* DaisyUI v5 removed carousel overflow — re-add for horizontal scroll */
117
+ .carousel {
118
+ -webkit-overflow-scrolling: touch;
119
+ overflow-x: auto;
120
+ scroll-snap-type: x mandatory;
121
+ scroll-behavior: smooth;
122
+ }
123
+
124
+ .carousel > * {
125
+ scroll-snap-align: start;
126
+ }
127
+
128
+ ul.carousel,
129
+ ol.carousel {
130
+ list-style: none;
131
+ padding: 0;
132
+ }`);
133
+
134
+ // ── Container utility (v3 -> v4 migration) ────────────────────────
135
+ sections.push(`/* Container: replaces Tailwind v3 container plugin config */
136
+ @utility container {
137
+ margin-inline: auto;
138
+ padding-inline: 1rem;
139
+ width: 100%;
140
+
141
+ @media (width >= 640px) { max-width: 640px; }
142
+ @media (width >= 768px) { max-width: 768px; }
143
+ @media (width >= 1024px) { max-width: 1024px; }
144
+ @media (width >= 1280px) { max-width: 1280px; }
145
+ @media (width >= 1536px) { max-width: 1536px; }
146
+ }`);
147
+
148
+ // ── Deferred section visibility ───────────────────────────────────
149
+ sections.push(`/* Deferred section visibility — reduces layout shift while loading */
150
+ section[data-deferred="true"] {
151
+ content-visibility: auto;
152
+ contain-intrinsic-size: auto 300px;
153
+ }
154
+
155
+ .deferred-section {
156
+ content-visibility: auto;
157
+ contain-intrinsic-size: auto 300px;
158
+ }`);
159
+
160
+ // ── View transitions ──────────────────────────────────────────────
161
+ sections.push(`@view-transition {
162
+ navigation: auto;
163
+ }`);
164
+
165
+ // ── Base layer resets ─────────────────────────────────────────────
166
+ sections.push(`@layer base {
167
+ *,
168
+ *::before,
169
+ *::after {
170
+ box-sizing: border-box;
171
+ }
172
+
173
+ body {
174
+ -webkit-font-smoothing: antialiased;
175
+ -moz-osx-font-smoothing: grayscale;
176
+ }
177
+
178
+ img,
179
+ picture,
180
+ video,
181
+ canvas,
182
+ svg {
183
+ display: block;
184
+ max-width: 100%;
185
+ }
186
+
187
+ /* Drawer / modal scroll lock */
188
+ body:has(dialog[open]),
189
+ body:has(.drawer-toggle:checked) {
190
+ overflow: hidden;
191
+ }
192
+ }`);
193
+
194
+ // ── Scrollbar utility ─────────────────────────────────────────────
195
+ sections.push(`.scrollbar-none {
196
+ scrollbar-width: none;
197
+ -ms-overflow-style: none;
198
+ }
199
+ .scrollbar-none::-webkit-scrollbar {
200
+ display: none;
201
+ }`);
202
+
203
+ return sections.join("\n\n") + "\n";
204
+ }