@decocms/start 0.39.0 → 0.40.1

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 (34) hide show
  1. package/.agents/skills/deco-migrate-script/SKILL.md +434 -0
  2. package/.agents/skills/deco-to-tanstack-migration/SKILL.md +382 -0
  3. package/.agents/skills/deco-to-tanstack-migration/references/admin-cms.md +154 -0
  4. package/{.cursor/skills/deco-async-rendering-site-guide/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/async-rendering.md} +296 -31
  5. package/.agents/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  6. package/.agents/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  7. package/.agents/skills/deco-to-tanstack-migration/references/css-styling.md +156 -0
  8. package/.agents/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  9. package/.agents/skills/deco-to-tanstack-migration/references/gotchas.md +13 -0
  10. package/{.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/hydration-fixes.md} +139 -4
  11. package/.agents/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  12. package/{.cursor/skills/deco-islands-migration/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/islands.md} +0 -14
  13. package/.agents/skills/deco-to-tanstack-migration/references/jsx-migration.md +80 -0
  14. package/.agents/skills/deco-to-tanstack-migration/references/matchers.md +1064 -0
  15. package/{.cursor/skills/deco-tanstack-navigation/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/navigation.md} +1 -16
  16. package/.agents/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  17. package/.agents/skills/deco-to-tanstack-migration/references/react-hooks-patterns.md +142 -0
  18. package/.agents/skills/deco-to-tanstack-migration/references/react-signals-state.md +72 -0
  19. package/{.cursor/skills/deco-tanstack-search/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/search.md} +1 -13
  20. package/.agents/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  21. package/{.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/storefront-patterns.md} +1 -137
  22. package/.agents/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  23. package/.agents/skills/deco-to-tanstack-migration/references/vtex-commerce.md +165 -0
  24. package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +209 -0
  25. package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  26. package/.agents/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  27. package/.agents/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  28. package/.agents/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  29. package/.agents/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  30. package/.agents/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  31. package/README.md +45 -0
  32. package/package.json +1 -1
  33. package/src/routes/cmsRoute.ts +7 -4
  34. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +0 -270
@@ -0,0 +1,78 @@
1
+ # Commerce & Widget Types Migration
2
+
3
+ ## Commerce Types
4
+
5
+ Commerce types live in `@decocms/apps/commerce/types`. Import directly:
6
+
7
+ ```typescript
8
+ import type { Product, AnalyticsItem, BreadcrumbList } from "@decocms/apps/commerce/types";
9
+ ```
10
+
11
+ Key types available: `Product`, `ProductGroup`, `ProductListingPage`, `ProductDetailsPage`, `Offer`, `AggregateOffer`, `UnitPriceSpecification`, `ImageObject`, `PropertyValue`, `BreadcrumbList`, `SiteNavigationElement`, `Brand`, `Review`, `AggregateRating`, `Filter`, `FilterToggle`, `FilterRange`, `SortOption`, `PageInfo`, `Suggestion`, `Search`, `AnalyticsItem`, `AddToCartParams`.
12
+
13
+ Replace old imports:
14
+ ```bash
15
+ sed -i '' 's|from "apps/commerce/types.ts"|from "@decocms/apps/commerce/types"|g'
16
+ sed -i '' 's|from "~/types/commerce"|from "@decocms/apps/commerce/types"|g'
17
+ ```
18
+
19
+ ## Commerce Utilities
20
+
21
+ Also from `@decocms/apps`:
22
+
23
+ ```typescript
24
+ import { mapProductToAnalyticsItem } from "@decocms/apps/commerce/utils/productToAnalyticsItem";
25
+ import { parseRange, formatRange } from "@decocms/apps/commerce/utils/filters";
26
+ ```
27
+
28
+ Replace old imports:
29
+ ```bash
30
+ sed -i '' 's|from "apps/commerce/utils/productToAnalyticsItem.ts"|from "@decocms/apps/commerce/utils/productToAnalyticsItem"|g'
31
+ sed -i '' 's|from "apps/commerce/utils/filters.ts"|from "@decocms/apps/commerce/utils/filters"|g'
32
+ ```
33
+
34
+ ## Widget Types
35
+
36
+ CMS widget types are site-local since they're just string aliases. Create `~/types/widgets.ts`:
37
+
38
+ ```typescript
39
+ export type ImageWidget = string;
40
+ export type HTMLWidget = string;
41
+ export type VideoWidget = string;
42
+ export type TextWidget = string;
43
+ export type RichText = string;
44
+ export type Secret = string;
45
+ export type Color = string;
46
+ export type ButtonWidget = string;
47
+ ```
48
+
49
+ Replace:
50
+ ```bash
51
+ sed -i '' 's|from "apps/admin/widgets.ts"|from "~/types/widgets"|g'
52
+ ```
53
+
54
+ ## UI Components (Site-Local)
55
+
56
+ Image, Picture, Seo, Theme etc. are **site-local components** -- they do NOT belong in `@decocms/apps`.
57
+
58
+ Create these in `~/components/ui/`:
59
+ - `Image.tsx` - thin `<img>` wrapper (accepts `preload`, `fit` props for API compat)
60
+ - `Picture.tsx` - `<picture>` + `<Source>` wrapper
61
+ - `Seo.tsx` - head meta tags (stub or real implementation)
62
+ - `Theme.tsx` - CSS variable injection (stub or real)
63
+ - `PoweredByDeco.tsx` - footer badge
64
+ - `Video.tsx` - `<video>` wrapper
65
+ - `SeoPreview.tsx` - admin SEO preview (stub)
66
+
67
+ Replace:
68
+ ```bash
69
+ sed -i '' 's|from "apps/website/components/Image.tsx"|from "~/components/ui/Image"|g'
70
+ sed -i '' 's|from "apps/website/components/Picture.tsx"|from "~/components/ui/Picture"|g'
71
+ ```
72
+
73
+ ## Verification
74
+
75
+ ```bash
76
+ grep -r 'from "apps/' src/ --include='*.ts' --include='*.tsx'
77
+ # Should return ZERO matches
78
+ ```
@@ -0,0 +1,156 @@
1
+ # CSS / Tailwind v4 / DaisyUI Gotchas
2
+
3
+ > oklch triplets, logical properties, DaisyUI collapse, theme prefixes, sidebar.
4
+
5
+
6
+ ## 15. DaisyUI v4 Theme in Preview Shell
7
+
8
+ DaisyUI v4 with Tailwind v4's `@plugin "daisyui/theme"` scopes all color variables to `[data-theme="light"]`. The admin preview HTML shell (`/live/previews/*`) must include this attribute, or colors will be wrong.
9
+
10
+ **Symptom**: Preview in admin shows default/missing colors while production looks correct.
11
+
12
+ **Fix**: Configure the preview shell in `setup.ts`:
13
+
14
+ ```typescript
15
+ setRenderShell({
16
+ css: appCss,
17
+ fonts: [...],
18
+ theme: "light", // adds data-theme="light" to <html>
19
+ bodyClass: "bg-base-100 text-base-content",
20
+ lang: "pt-BR",
21
+ });
22
+ ```
23
+
24
+ The production HTML has `<html lang="pt-BR" data-theme="light">` set by the TanStack root layout. The preview shell must replicate this.
25
+
26
+
27
+ ## 17. SiteTheme is a Stub
28
+
29
+ `Theme.tsx` returns `null`. Colors come from CSS at build time, not CMS at runtime.
30
+
31
+
32
+ ## 31. CSS Theme Class Prefixes Must Not Be Renamed
33
+
34
+ **Severity**: HIGH — breaks all theme colors
35
+
36
+ The original site uses `seasonal-*` CSS class prefixes for theme variables (e.g., `bg-seasonal-brand-terciary-1`, `text-seasonal-neutral-1`). During migration, do NOT rename these to `header-*`, `footer-*`, or any other prefix. The theme variables are defined centrally and all components reference the same `seasonal-*` namespace.
37
+
38
+ **Fix**: Only change what React strictly requires: `class` → `className`, `for` → `htmlFor`. Preserve all original CSS class names exactly.
39
+
40
+
41
+ ## 37. DaisyUI v4 Collapse Broken with Tailwind v4
42
+
43
+ **Severity**: MEDIUM — filter sidebars, FAQ accordions, any collapsible section renders collapsed
44
+
45
+ DaisyUI v4's collapse component uses `grid-template-rows: auto 0fr` with `content-visibility: hidden` and expands via `:has(>input:checked)`. In combination with Tailwind v4, the expand chain breaks — content stays collapsed regardless of checkbox state.
46
+
47
+ **Symptom**: Filter sidebar shows as empty space. Collapse titles may render but content is permanently hidden. Custom CSS overrides on `.collapse` conflict with DaisyUI's generated styles.
48
+
49
+ **Fix**: Replace DaisyUI collapse with native `<details>/<summary>` HTML elements:
50
+
51
+ ```typescript
52
+ // Before: DaisyUI collapse with hidden checkbox
53
+ <div className="collapse">
54
+ <input type="checkbox" defaultChecked />
55
+ <div className="collapse-title">Category</div>
56
+ <div className="collapse-content">...filters...</div>
57
+ </div>
58
+
59
+ // After: Native HTML, works everywhere
60
+ <details open className="group">
61
+ <summary className="cursor-pointer font-semibold">Category</summary>
62
+ <div className="mt-2">...filters...</div>
63
+ </details>
64
+ ```
65
+
66
+
67
+ ## 40. Filter Sidebar Invisible Due to Background Color Match
68
+
69
+ **Severity**: LOW — cosmetic, but confusing during development
70
+
71
+ The aside element for search/category filters renders correctly in the DOM (proper width, height, content) but appears invisible because its background matches the page background (e.g., both `#E9E9E9`).
72
+
73
+ **Symptom**: Filters appear "non-existent" even though they're in the DOM. Filter links are accessible but invisible.
74
+
75
+ **Fix**: Add a contrasting background to the filter aside:
76
+
77
+ ```typescript
78
+ <aside className="... bg-white rounded-lg p-4">
79
+ ```
80
+
81
+
82
+ ## 42. Tailwind v4 Logical vs Physical Property Cascade Conflict
83
+
84
+ **Severity**: CRITICAL — causes container width mismatches across the entire site
85
+
86
+ Tailwind v4 generates **logical CSS properties** (`padding-inline`, `margin-inline`) while Tailwind v3 generated **physical properties** (`padding-left`, `padding-right`). When an element has BOTH shorthand (`px-*`) and longhand (`pl-*`/`pr-*`) responsive classes, the cascade breaks silently.
87
+
88
+ **Symptom**: Containers are narrower or have asymmetric padding compared to production. The layout "looks off" at certain breakpoints but works at others.
89
+
90
+ **Root cause**: In Tailwind v3, `md:px-6` and `sm:pl-0` both target `padding-left` — same CSS property, media query specificity decides the winner. In Tailwind v4, `md:px-6` targets `padding-inline` (shorthand) while `sm:pl-0` targets `padding-inline-start` (longhand). These are different CSS properties. If `padding-inline-start` appears later in the compiled stylesheet, it overrides the shorthand's start value, creating asymmetric padding.
91
+
92
+ **Example**:
93
+ ```html
94
+ <!-- This pattern exists in many Deco storefronts -->
95
+ <div class="pl-4 sm:pl-0 md:px-6 xl-b:px-0 max-w-[1280px] mx-auto">
96
+ ```
97
+
98
+ In Tailwind v3: at `md` viewport, `px-6` sets `padding-left: 1.5rem` and `padding-right: 1.5rem`, cleanly overriding `sm:pl-0`.
99
+
100
+ In Tailwind v4: at `md` viewport, `px-6` sets `padding-inline: 1.5rem`, but `pl-0` (from `sm:`) may still override `padding-inline-start` depending on stylesheet order.
101
+
102
+ **Fix**: Replace mixed shorthand + longhand patterns with consistent longhand properties:
103
+
104
+ ```
105
+ md:px-6 xl-b:px-0 → md:pl-6 md:pr-6 xl-b:pl-0 xl-b:pr-0
106
+ px-4 lg:px-6 xl-b:px-0 → pl-4 pr-4 lg:pl-6 lg:pr-6 xl-b:pl-0 xl-b:pr-0
107
+ ```
108
+
109
+ **Detection**: Find all elements with mixed patterns:
110
+ ```bash
111
+ grep -rn 'px-[0-9].*pl-\|pl-.*px-[0-9]\|px-[0-9].*pr-\|pr-.*px-[0-9]' src/ --include='*.tsx'
112
+ ```
113
+
114
+ Only convert `px-*` on elements that ALSO have `pl-*` or `pr-*`. Don't blindly replace all `px-*` across the codebase — elements with only `px-*` (no mixed longhand) work fine.
115
+
116
+ Also check for the same issue with `mx-*` mixed with `ml-*`/`mr-*`, and `my-*` mixed with `mt-*`/`mb-*`.
117
+
118
+
119
+ ## 43. CSS oklch() Color Variables Must Store Triplets, Not Hex
120
+
121
+ **Severity**: HIGH — all SVG icons render as black, brand colors break
122
+
123
+ Sites that use `oklch(var(--variable))` in SVG fill/stroke attributes (common in Deco storefronts with seasonal/theme color systems) require the CSS variables to store **oklch triplets** (`100% 0.00 0deg`), NOT hex values (`#FFF`). `oklch(#FFF)` is invalid CSS — the browser ignores it and falls back to black.
124
+
125
+ **Symptom**: Slider arrows, footer icons, search icons, filter icons — anything using `oklch(var(--...))` — renders as black circles/shapes instead of the brand colors.
126
+
127
+ **Root cause**: The original site's Theme section (via Deco CMS) outputs oklch triplets into CSS variables. During migration, if the CSS variables are manually set to hex values, every `oklch()` wrapper produces invalid CSS.
128
+
129
+ **Fix**: Convert all theme CSS variables from hex to oklch triplets:
130
+ ```css
131
+ /* WRONG — invalid CSS when used as oklch(var(--bg-seasonal-2)) */
132
+ --bg-seasonal-2: #FFF;
133
+
134
+ /* CORRECT — oklch(100% 0.00 0deg) is valid */
135
+ --bg-seasonal-2: 100% 0.00 0deg;
136
+ ```
137
+
138
+ **Dual-usage caveat**: Variables used BOTH inside `oklch()` wrappers AND directly in CSS properties need different handling:
139
+
140
+ ```css
141
+ /* @theme entries for Tailwind utilities — need oklch() wrapper */
142
+ --color-bg-seasonal-1: oklch(var(--bg-seasonal-1));
143
+
144
+ /* Direct CSS usage — also needs oklch() wrapper */
145
+ background-color: oklch(var(--bg-seasonal-1));
146
+ ```
147
+
148
+ The DaisyUI v4 pattern: `@theme` entries map `--color-X` to `var(--Y)`. Tailwind generates `background-color: var(--color-X)` which resolves to the raw triplet — invalid without the `oklch()` wrapper. Wrap all `@theme` entries that reference oklch-triplet variables.
149
+
150
+ **Python conversion helper**:
151
+ ```python
152
+ from colorjs import Color
153
+ c = Color("#EE4F31")
154
+ l, c_val, h = c.convert("oklch").coords()
155
+ print(f"{l*100:.2f}% {c_val:.2f} {h:.0f}deg") # 64.42% 0.20 33deg
156
+ ```
@@ -0,0 +1,128 @@
1
+ # Deco Framework Import Elimination
2
+
3
+ Replace all `@deco/deco/*` and `$fresh/*` imports with inline equivalents. No shim files needed.
4
+
5
+ ## $fresh/runtime.ts
6
+
7
+ ### asset(url)
8
+
9
+ The `asset()` function in Fresh prepends the build hash path. In Vite, static assets are handled automatically. Just remove the wrapper:
10
+
11
+ ```typescript
12
+ // OLD
13
+ import { asset } from "$fresh/runtime.ts";
14
+ <img src={asset("/sprites.svg")} />
15
+
16
+ // NEW
17
+ <img src="/sprites.svg" />
18
+ ```
19
+
20
+ ### IS_BROWSER
21
+
22
+ ```typescript
23
+ // OLD
24
+ import { IS_BROWSER } from "$fresh/runtime.ts";
25
+
26
+ // NEW (inline)
27
+ const IS_BROWSER = typeof document !== "undefined";
28
+ ```
29
+
30
+ ## @deco/deco (bare import)
31
+
32
+ ### SectionProps
33
+
34
+ ```typescript
35
+ // OLD
36
+ import type { SectionProps } from "@deco/deco";
37
+ type Props = SectionProps<typeof loader>;
38
+
39
+ // NEW (inline type)
40
+ type SectionProps<T extends (...args: any[]) => any> = ReturnType<T>;
41
+ ```
42
+
43
+ ### Resolved
44
+
45
+ ```typescript
46
+ // OLD
47
+ import type { Resolved } from "@deco/deco";
48
+
49
+ // NEW
50
+ type Resolved<T = any> = T;
51
+ ```
52
+
53
+ ### context
54
+
55
+ ```typescript
56
+ // OLD
57
+ import { context } from "@deco/deco";
58
+ if (context.isDeploy) { ... }
59
+
60
+ // NEW
61
+ const context = { isDeploy: false, platform: "tanstack-start", site: "my-store", siteId: 0 };
62
+ ```
63
+
64
+ ## @deco/deco/blocks
65
+
66
+ ### Section, Block
67
+
68
+ ```typescript
69
+ // OLD
70
+ import type { Section } from "@deco/deco/blocks";
71
+
72
+ // NEW
73
+ type Section = any;
74
+ type Block = any;
75
+ ```
76
+
77
+ These were used for Deco CMS section composition. In TanStack Start, sections are just React components.
78
+
79
+ ## @deco/deco/hooks
80
+
81
+ ### useScript / useScriptAsDataURI
82
+
83
+ These serialize a function into an inline `<script>` string. Create `~/sdk/useScript.ts`:
84
+
85
+ ```typescript
86
+ export function useScript(fn: (...args: any[]) => void, ...args: any[]): string {
87
+ const serializedArgs = args.map((a) => JSON.stringify(a)).join(",");
88
+ return `(${fn.toString()})(${serializedArgs})`;
89
+ }
90
+
91
+ export function useScriptAsDataURI(fn: (...args: any[]) => void, ...args: any[]): string {
92
+ const code = useScript(fn, ...args);
93
+ return `data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`;
94
+ }
95
+ ```
96
+
97
+ ### usePartialSection
98
+
99
+ Deco partial sections don't apply in TanStack Start. Stub:
100
+
101
+ ```typescript
102
+ export function usePartialSection(props?: Record<string, unknown>) {
103
+ return props || {};
104
+ }
105
+ ```
106
+
107
+ ## @deco/deco/o11y
108
+
109
+ ```typescript
110
+ // OLD
111
+ import { logger } from "@deco/deco/o11y";
112
+ logger.error("failed", err);
113
+
114
+ // NEW
115
+ console.error("failed", err);
116
+ ```
117
+
118
+ ## Automation
119
+
120
+ All of these are safe for bulk sed (each import pattern maps to exactly one replacement).
121
+
122
+ ## Verification
123
+
124
+ ```bash
125
+ grep -r '@deco/deco' src/ --include='*.ts' --include='*.tsx'
126
+ grep -r '\$fresh/' src/ --include='*.ts' --include='*.tsx'
127
+ # Both should return ZERO matches
128
+ ```
@@ -0,0 +1,13 @@
1
+ # Gotchas Index
2
+
3
+ This file is an index. Each topic has its own focused file.
4
+
5
+ | File | Topic | Key Gotchas |
6
+ |------|-------|-------------|
7
+ | [react-hooks-patterns.md](react-hooks-patterns.md) | useEffect, useQuery, useMemo, lazy init | #2, #33, #46, #47 |
8
+ | [react-signals-state.md](react-signals-state.md) | TanStack Store, signal.value, subscribe() | #3, #19, #38 |
9
+ | [jsx-migration.md](jsx-migration.md) | Preact→React JSX differences | #4–6, #11, #20–22, #41 |
10
+ | [vtex-commerce.md](vtex-commerce.md) | VTEX loaders, cart, facets, price specs | #1, #7–8, #32, #34–36, #39 |
11
+ | [worker-cloudflare.md](worker-cloudflare.md) | Worker entry, build, Cloudflare, npm | #9–10, #12–14, #19, #24–28, #30, #44–45 |
12
+ | [css-styling.md](css-styling.md) | Tailwind v4, oklch, DaisyUI | #15, #17, #31, #37, #40, #42–43 |
13
+ | [admin-cms.md](admin-cms.md) | Admin routes, schema, device context | #16, #18, #23, #26, #29 |
@@ -1,7 +1,3 @@
1
- ---
2
- name: deco-tanstack-hydration-fixes
3
- description: Fix hydration mismatches, flash-of-white, CLS from third-party scripts, scroll-to-top bugs, and React DOM warnings in Deco storefronts on TanStack Start/React/Cloudflare Workers. Use when debugging hydration errors, page flicker during navigation, blank screen on F5, layout shifts from external scripts, or React console warnings about invalid DOM properties.
4
- ---
5
1
 
6
2
  # Hydration & Navigation Fixes for Deco TanStack Storefronts
7
3
 
@@ -466,3 +462,142 @@ When investigating hydration/flash issues on a Deco TanStack storefront:
466
462
  8. **Test with F5 (hard reload)** — SPA navigation may hide issues that appear on cold load
467
463
  9. **Check scroll behavior** — navigate from shelf to PDP; verify page scrolls to top
468
464
  10. **Compare SSR HTML vs client render** — view source (`Ctrl+U`) and compare with inspected DOM to find hydration diffs
465
+
466
+ ---
467
+
468
+ ## 13. `useDevice()` Hydration Mismatch in Eager Sections
469
+
470
+ ### Root Cause
471
+
472
+ `@decocms/start` shell-renders sections in a **separate React root** that does NOT include providers from `__root.tsx`. This means `useDevice()` — which reads from `Device.Provider` — falls back to the context default (`isMobile: true`) on the server, while the client goes through `__root.tsx` and gets whatever value the Provider has.
473
+
474
+ Result: server renders mobile layout, client renders desktop layout → structural hydration mismatch.
475
+
476
+ This affects ONLY **eager sections** (sections in `alwaysEager` or not wrapped in CMS `Lazy.tsx`). Deferred sections render a skeleton server-side, and the real component loads client-side AFTER hydration — no mismatch.
477
+
478
+ ### Which sections are affected
479
+
480
+ Check `alwaysEager` in `setup.ts`:
481
+ ```typescript
482
+ setAsyncRenderingConfig({
483
+ alwaysEager: [
484
+ "site/sections/Header/Header.tsx",
485
+ "site/sections/Header/NewHeader.tsx",
486
+ "site/sections/Footer/Footer.tsx",
487
+ "site/sections/Theme/Theme.tsx",
488
+ "site/sections/Images/Carousel.tsx",
489
+ "site/sections/Tipbar.tsx",
490
+ ],
491
+ });
492
+ ```
493
+
494
+ Any of these that call `useDevice()` and render different HTML structure based on `isMobile` will have a hydration error.
495
+
496
+ `LoadingFallback` components are also server-rendered (they're the skeleton while deferred section loads). LoadingFallbacks that use `useDevice()` to change the count or structure of elements will cause hydration mismatches on the skeleton itself.
497
+
498
+ ### Fix Pattern A: Use `device` prop from loader (structural branches)
499
+
500
+ For sections that already receive `device` from their loader (`ctx.device`), use the prop instead of the hook:
501
+
502
+ ```typescript
503
+ // Before — useDevice() reads from context, missing during shell render
504
+ function Header({ device, ...props }: Props) {
505
+ const { isMobile } = useDevice(); // ← wrong: context not available in shell render
506
+ if (isMobile) return <MobileLayout />;
507
+ return <DesktopLayout />;
508
+ }
509
+
510
+ // After — device prop comes from loaderData, consistent on server + client
511
+ function Header({ device, ...props }: Props) {
512
+ const isMobile = device === "mobile" || device === "tablet"; // ← correct
513
+ if (isMobile) return <MobileLayout />;
514
+ return <DesktopLayout />;
515
+ }
516
+ ```
517
+
518
+ The `device` prop comes from the section loader:
519
+ ```typescript
520
+ export async function loader(props: Props, req: Request, ctx: AppContext) {
521
+ return {
522
+ ...props,
523
+ device: ctx.device as "mobile" | "desktop" | "tablet",
524
+ };
525
+ }
526
+ ```
527
+
528
+ `ctx.device` is detected from the request `User-Agent` header server-side. TanStack Start serializes loaderData and sends it to the client, so both server and client always use the same value. No mismatch.
529
+
530
+ Also fix the section's `LoadingFallback` to use `props.device` instead of `useDevice()`:
531
+ ```tsx
532
+ // Before
533
+ export const LoadingFallback = (props: LoadingFallbackProps & Props) => {
534
+ const device = useDevice();
535
+ const deviceProp = device.isMobile ? "mobile" : "desktop";
536
+ return <Header {...props} device={deviceProp} />;
537
+ };
538
+
539
+ // After
540
+ export const LoadingFallback = (props: LoadingFallbackProps & Props) => {
541
+ return <Header {...props} device={props.device ?? "desktop"} />;
542
+ };
543
+ ```
544
+
545
+ ### Fix Pattern B: Remove redundant JS conditionals (show/hide with CSS)
546
+
547
+ When a section renders show/hide elements based on `isMobile` but the containing elements ALREADY have responsive CSS classes (`hidden lg:block`, `hidden lg:flex`, `lg:hidden`), the JS conditional is redundant and causes the mismatch. Simply remove it:
548
+
549
+ ```tsx
550
+ // Before — JS conditional + CSS class (redundant, causes mismatch)
551
+ {!isMobile && (
552
+ <div className="hidden lg:flex">...</div>
553
+ )}
554
+
555
+ // After — CSS alone handles responsive behavior
556
+ <div className="hidden lg:flex">...</div>
557
+ ```
558
+
559
+ This is the correct fix for Footer and similar layout sections where mobile/desktop differences are already handled by Tailwind responsive prefixes.
560
+
561
+ ### Fix Pattern C: CSS-only responsive sizing (LoadingFallback skeletons)
562
+
563
+ For `LoadingFallback` components that use `useDevice()` to vary sizes or counts, replace JS with responsive Tailwind classes:
564
+
565
+ ```tsx
566
+ // Before — causes hydration mismatch on skeleton
567
+ function LoadingCard() {
568
+ const device = useDevice();
569
+ return (
570
+ <div className={`max-w-[${device.isMobile ? "160px" : "320px"}]`}>
571
+ <div className={`h-${device.isMobile ? "40" : "60"}`} />
572
+ </div>
573
+ );
574
+ }
575
+
576
+ // After — CSS handles responsive sizing, no JS needed
577
+ function LoadingCard({ className }: { className?: string }) {
578
+ return (
579
+ <div className={`max-w-[160px] sm:max-w-[320px] ${className ?? ""}`}>
580
+ <div className="h-40 sm:h-60" />
581
+ </div>
582
+ );
583
+ }
584
+
585
+ // For count variations in LoadingFallback:
586
+ // Before (renders 2 on mobile / 4 on desktop)
587
+ {Array(device.isMobile ? 2 : 4).fill(0).map((_, i) => <LoadingCard key={i} />)}
588
+
589
+ // After (always renders 4, CSS hides extras on mobile)
590
+ <LoadingCard />
591
+ <LoadingCard />
592
+ <LoadingCard className="hidden sm:flex" />
593
+ <LoadingCard className="hidden sm:flex" />
594
+ ```
595
+
596
+ ### Quick diagnosis
597
+
598
+ Search for `useDevice` in eager sections to find candidates:
599
+ ```bash
600
+ rg "useDevice" src/sections/ src/components/header/ src/components/footer/ --glob "*.tsx"
601
+ ```
602
+
603
+ Any result in a component that's always-eager and renders different element types/counts based on `isMobile` needs one of the fix patterns above.
@@ -0,0 +1,70 @@
1
+ # Preact -> React Import Migration
2
+
3
+ Mechanical find-and-replace. Safe to automate with `sed`.
4
+
5
+ ## Replacements
6
+
7
+ | Find | Replace |
8
+ |------|---------|
9
+ | `from "preact/hooks"` | `from "react"` |
10
+ | `from "preact/compat"` | `from "react"` |
11
+ | `from "preact"` | `from "react"` |
12
+
13
+ ## Special Cases
14
+
15
+ ### ComponentChildren -> ReactNode
16
+
17
+ Preact's `ComponentChildren` maps to React's `ReactNode`:
18
+
19
+ ```typescript
20
+ // OLD
21
+ import type { ComponentChildren } from "preact";
22
+
23
+ // NEW
24
+ import type { ReactNode as ComponentChildren } from "react";
25
+ ```
26
+
27
+ If you want to modernize fully, rename `ComponentChildren` to `ReactNode` across the codebase.
28
+
29
+ ### JSX type
30
+
31
+ ```typescript
32
+ // OLD
33
+ import type { JSX } from "preact";
34
+
35
+ // NEW (works unchanged)
36
+ import type { JSX } from "react";
37
+ ```
38
+
39
+ ### FunctionalComponent -> FC
40
+
41
+ ```typescript
42
+ // OLD
43
+ const Foo: preact.FunctionalComponent<Props> = ...
44
+
45
+ // NEW
46
+ import React from "react";
47
+ const Foo: React.FC<Props> = ...
48
+ ```
49
+
50
+ ## Automation
51
+
52
+ ```bash
53
+ # Bulk replace (macOS sed)
54
+ find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
55
+ -e 's|from "preact/hooks"|from "react"|g' \
56
+ -e 's|from "preact/compat"|from "react"|g'
57
+
58
+ # Bare preact requires care (don't match preact/hooks or preact/compat)
59
+ find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
60
+ 's|from "preact"|from "react"|g'
61
+ ```
62
+
63
+ Then handle `ComponentChildren` files individually.
64
+
65
+ ## Verification
66
+
67
+ ```bash
68
+ grep -r 'from "preact' src/ --include='*.ts' --include='*.tsx'
69
+ # Should return ZERO matches
70
+ ```
@@ -1,7 +1,3 @@
1
- ---
2
- name: deco-islands-migration
3
- description: Eliminate the src/islands/ directory when migrating Deco storefronts from Fresh/Preact to TanStack Start/React. Explains why islands don't make sense in the new stack, the problems they cause, how to systematically repoint or move them, and how to fix vanilla-JS DOM patterns that break React hydration. Use when auditing or removing islands from a migrated storefront.
4
- ---
5
1
 
6
2
  # Deco Islands Migration — From Fresh Islands to React Components
7
3
 
@@ -239,13 +235,3 @@ function Drawer({ class: classProp = "", className = "" }: Props) {
239
235
  - [ ] Build succeeds (`npm run build` / `bun run build`)
240
236
  - [ ] Dev server starts without errors
241
237
  - [ ] Interactive elements work: add to cart, sliders, drawers, modals
242
-
243
- ## Related Skills
244
-
245
- | Skill | Purpose |
246
- |-------|---------|
247
- | `deco-to-tanstack-migration` | Full migration playbook (imports, signals, architecture) |
248
- | `deco-tanstack-navigation` | SPA navigation patterns (`<a>` → `<Link>`, `useNavigate`, `loaderDeps`, forms) |
249
- | `deco-tanstack-storefront-patterns` | Runtime fixes post-migration (nested sections, caching, SliderJS, async_hooks, cart, server functions) |
250
- | `deco-storefront-test-checklist` | Context-aware QA checklist generation |
251
- | `deco-typescript-fixes` | TypeScript error patterns and fixes |