kmcom-nuxt-layers 6.4.0 → 6.5.0

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 (129) hide show
  1. package/docs/MIGRATION-v6.5.0.md +87 -0
  2. package/docs/MIGRATION.md +2 -1
  3. package/docs/USAGE.md +35 -10
  4. package/layers/content/app/components/content/Figure.vue +1 -1
  5. package/layers/content/app/composables/useContentSection.ts +1 -1
  6. package/layers/core/app/app.config.ts +7 -3
  7. package/layers/core/app/components/LoadingScreen.vue +35 -21
  8. package/layers/core/app/composables/useCache.ts +2 -2
  9. package/layers/core/app/composables/useErrorLog.ts +5 -5
  10. package/layers/core/app/composables/useLoading.ts +19 -10
  11. package/layers/core/app/plugins/loading.client.ts +11 -3
  12. package/layers/core/app/types/loading.ts +10 -3
  13. package/layers/core/app/utils/loading.ts +6 -15
  14. package/layers/core/nuxt.config.ts +14 -14
  15. package/layers/core/server/utils/app-config.ts +17 -0
  16. package/layers/data/auth/app/middleware/auth.ts +1 -1
  17. package/layers/data/forms/app/components/Form/Contact.vue +1 -1
  18. package/layers/data/forms/server/api/contact.post.ts +16 -8
  19. package/layers/data/forms/server/api/forms/status.get.ts +3 -0
  20. package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
  21. package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
  22. package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
  23. package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
  24. package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
  25. package/layers/delivery/feeds/nuxt.config.ts +6 -1
  26. package/layers/delivery/feeds/package.json +2 -2
  27. package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
  28. package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
  29. package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
  30. package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
  31. package/layers/design-system/theming/nuxt.config.ts +0 -2
  32. package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
  33. package/layers/render/canvas/nuxt.config.ts +0 -4
  34. package/layers/render/shader/nuxt.config.ts +0 -4
  35. package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
  36. package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
  37. package/layers/structure/routing/nuxt.config.ts +19 -12
  38. package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
  39. package/package.json +35 -5
  40. package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
  41. package/docs/IMPROVE-AUDIT-README.md +0 -29
  42. package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
  43. package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
  44. package/docs/fallow-refactor/apps-debug.md +0 -27
  45. package/docs/fallow-refactor/apps-playground.md +0 -46
  46. package/docs/fallow-refactor/apps-visual-identity.md +0 -41
  47. package/docs/fallow-refactor/layers-animations.md +0 -34
  48. package/docs/fallow-refactor/layers-canvas.md +0 -32
  49. package/docs/fallow-refactor/layers-content.md +0 -33
  50. package/docs/fallow-refactor/layers-core.md +0 -39
  51. package/docs/fallow-refactor/layers-feeds.md +0 -39
  52. package/docs/fallow-refactor/layers-forms.md +0 -30
  53. package/docs/fallow-refactor/layers-layout.md +0 -42
  54. package/docs/fallow-refactor/layers-mailer.md +0 -32
  55. package/docs/fallow-refactor/layers-motion.md +0 -27
  56. package/docs/fallow-refactor/layers-navigation.md +0 -31
  57. package/docs/fallow-refactor/layers-page-transitions.md +0 -30
  58. package/docs/fallow-refactor/layers-routing.md +0 -33
  59. package/docs/fallow-refactor/layers-scripts.md +0 -35
  60. package/docs/fallow-refactor/layers-scroll.md +0 -38
  61. package/docs/fallow-refactor/layers-seo.md +0 -32
  62. package/docs/fallow-refactor/layers-shader.md +0 -53
  63. package/docs/fallow-refactor/layers-theme.md +0 -33
  64. package/docs/fallow-refactor/layers-transitions.md +0 -27
  65. package/docs/fallow-refactor/layers-typography.md +0 -29
  66. package/docs/fallow-refactor/layers-visual.md +0 -34
  67. package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
  68. package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
  69. package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
  70. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
  71. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
  72. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
  73. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
  74. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
  75. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
  76. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
  77. package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
  78. package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
  79. package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
  80. package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
  81. package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
  82. package/layers/core/app/components/Element.test.ts +0 -90
  83. package/layers/core/app/components/ElementTw.test.ts +0 -64
  84. package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
  85. package/layers/core/app/utils/browserInfo.test.ts +0 -81
  86. package/layers/core/app/utils/featureClasses.test.ts +0 -101
  87. package/layers/core/app/utils/loading.test.ts +0 -48
  88. package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
  89. package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
  90. package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
  91. package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
  92. package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
  93. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
  94. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
  95. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
  96. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
  97. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
  98. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
  99. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
  100. package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
  101. package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
  102. package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
  103. package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
  104. package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
  105. package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
  106. package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
  107. package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
  108. package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
  109. package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
  110. package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
  111. package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
  112. package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
  113. package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
  114. package/layers/design-system/typography/app/composables/color.test.ts +0 -30
  115. package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
  116. package/layers/design-system/visual/app/components/HoverDimGroup.test.ts +0 -38
  117. package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
  118. package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
  119. package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
  120. package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
  121. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
  122. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
  123. package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
  124. package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
  125. package/layers/structure/grid/app/types/tracks.test.ts +0 -32
  126. package/layers/structure/grid/app/utils/placement.test.ts +0 -19
  127. package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
  128. package/layers/structure/navigation/app/utils/site.test.ts +0 -39
  129. package/layers/structure/routing/app/utils/resolveRoute.test.ts +0 -47
@@ -1,1850 +0,0 @@
1
- # Layout Layer Fluid Rebuild Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Replace `layers/structure/layout`'s breakpoint-based 6/12/18-column subgrid system with a CSS-only, `--fs`-derived fluid grid — no breakpoints, no numbered column placement, no JS grid math.
6
-
7
- **Architecture:** One root custom property (`--fs`) drives everything via CSS `calc()`/`round()`: `--unit` → `--edge-min`/`--lpf`/`--measure-min` → per-section `--lines`/`--fields`/`--slack`. The root grid uses `repeat(auto-fill, minmax(--measure-min, 1fr))` for organic column count. `LayoutSection` opts a full-width row into `subgrid` (inheriting the root's resolved columns) and runs its own field-rhythm derivation. Every derived custom property is registered via `@property` so a future bad edit fails loud (invalid declaration → visible layout break) instead of silently squashing cells into slivers — the exact failure mode this design already hit once in the `swiss-test` prototype.
8
-
9
- **Tech Stack:** Nuxt 4 layer (`layers/structure/layout`), Vue 3 `<script setup>`, plain CSS (`@property`, subgrid, container queries, `clamp()`/`round()`), Vitest + `@nuxt/test-utils` for the composable test.
10
-
11
- **Spec:** `docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md`
12
-
13
- ## Global Constraints
14
-
15
- - No numbered `colStart`/`colSpan`/`rowStart`/`rowSpan` placement API anywhere in the new component surface (spec §2, §5).
16
- - No mathematically-generated `--cols` — column count comes only from native `auto-fill` (spec §2, §7 Pitfall 1).
17
- - Every derived custom property (`--fs`, `--unit`, `--edge-min`, `--measure-min`, `--lpf`, `--lines`, `--fields`, `--avail-b`, `--type-b`, `--slack`) is registered via `@property` with a real `syntax` (spec §6).
18
- - Every fluid `clamp()` keeps a `rem` term; `svh`/`svi` never `dvh`/`vw`/`vh`; `container-type` never on `body`; `lh`/`rlh` never inside a media query condition (spec §8).
19
- - No TypeScript interface for the config→CSS-var bridge — `@property` is the only typing layer (spec §2, §9).
20
- - `HStack`/`VStack`/`ZStack`/`Spacer`, `LayoutPage`/`LayoutPageHeader`, and the z-index layer system (`useZIndex`, `GridLayers`) are untouched (spec §2).
21
- - No edits to `layers/content`, `layers/structure/navigation`, or the `starter` app in this plan — those are migration-guide territory, not this task (spec §2, §11).
22
-
23
- ---
24
-
25
- ## File Structure
26
-
27
- ```text
28
- layers/structure/layout/app/
29
- ├── assets/css/layout/grids.css # REWRITE — fluid engine + @property + rhythm utils (unchanged)
30
- ├── types/layouts.ts # REWRITE — GridMode/GridTunables/GridConfig, drop presets
31
- ├── app.config.ts # REWRITE — layoutLayer.ui.grid tunables, fix ui.page slots
32
- ├── composables/useGridConfig.ts # REWRITE — drop getPreset, add cssVars
33
- ├── components/Layout/
34
- │ ├── Main.vue # REWRITE — binds cssVars, class layout-grid
35
- │ ├── Section.vue # NEW — opt-in full-width subgrid row (replaces Section/index.vue)
36
- │ ├── Cell.vue # NEW — replaces Grid/Item.vue
37
- │ ├── Hero.vue # NEW — replaces the `hero` GridItem preset
38
- │ └── Grid/Debug.vue # REWRITE — --unit band overlay, no JS column tracking
39
- └── utils/gridPlacementStyle.ts # DELETE (+ .test.ts)
40
-
41
- layers/structure/layout/app/components/Layout/Section/ # DELETE whole dir (index.vue, Gallery.vue, Hero.vue, Split.vue)
42
- layers/structure/layout/app/components/Layout/Grid/Item.vue # DELETE
43
-
44
- tests/nuxt/use-grid-config.test.ts # REWRITE — mode/isEnabled/useZIndex/cssVars
45
-
46
- apps/playground/app/pages/layout.vue # REWRITE — demo against new components
47
-
48
- docs/LAYOUT.md # REWRITE
49
- layers/structure/layout/CLAUDE.md # REWRITE
50
- docs/LAYOUT-MIGRATION.md # NEW — old→new API mapping for later consumer migration
51
- ```
52
-
53
- ---
54
-
55
- ### Task 1: Types and config
56
-
57
- **Files:**
58
-
59
- - Modify: `layers/structure/layout/app/types/layouts.ts`
60
- - Modify: `layers/structure/layout/app/app.config.ts`
61
-
62
- **Interfaces:**
63
-
64
- - Produces: `GridMode = 'fluid' | 'disabled'`, `GridLayers` (unchanged shape), `GridTunables = { measureMin: string; edgeMin: { min: string; max: string }; lpf: { min: number; max: number } }`, `GridConfig = { mode?: GridMode; tunables: GridTunables; layers: GridLayers }` — every later task that imports from `layouts.ts` uses these exact names.
65
-
66
- - [ ] **Step 1: Rewrite `layouts.ts`**
67
-
68
- Replace the entire file with:
69
-
70
- ```ts
71
- export type GridLayers = {
72
- /** Component/content at rest — z-index 0 */
73
- back: number
74
- /** Standard interactive content — z-index 10 */
75
- mid: number
76
- /** Elevated content, sticky elements — z-index 20 */
77
- front: number
78
- /** Page-level overlays — z-index 30 */
79
- top: number
80
- /** Site header / navigation bar — z-index 100 */
81
- header: number
82
- /** Dropdown menus, popovers — z-index 200 */
83
- dropdown: number
84
- /** Overlay backdrops — z-index 300 */
85
- overlay: number
86
- /** Modal dialogs — z-index 400 */
87
- modal: number
88
- /** Toast notifications — z-index 500 */
89
- toast: number
90
- }
91
-
92
- export type GridMode = 'fluid' | 'disabled'
93
-
94
- export type GridTunables = {
95
- /** Narrowest column / repeat(auto-fill, minmax(...)) floor. CSS length, e.g. '22rem'. */
96
- measureMin: string
97
- /** clamp() bounds for the inline/block edge margin. CSS lengths. */
98
- edgeMin: { min: string; max: string }
99
- /** clamp() bounds for lines-per-field (vertical rhythm chunking). */
100
- lpf: { min: number; max: number }
101
- }
102
-
103
- export type GridConfig = {
104
- /**
105
- * Layout mode.
106
- * - `'fluid'` — the --fs-derived fluid grid (default)
107
- * - `'disabled'` — falls back to a plain, ungridded `<main>`
108
- */
109
- mode?: GridMode
110
- tunables: GridTunables
111
- layers: GridLayers
112
- }
113
- ```
114
-
115
- - [ ] **Step 2: Rewrite `app.config.ts`**
116
-
117
- Replace the `layoutLayer` block and the `ui.page` slots block. Keep `ui.header`, `ui.pageBody`, `ui.pageGrid`, `ui.pageColumns` unchanged — they only use `col-span-full`/`grid-cols-subgrid`, which stay meaningful with emergent columns. `ui.page`'s `left`/`center`/`right` split assumed a fixed 18-column grid (`col-start-5`, `col-start-15`) — that addressing is gone, so collapse all three slots to full width:
118
-
119
- ```ts
120
- import type { GridConfig } from './types/layouts'
121
-
122
- export default defineAppConfig({
123
- ui: {
124
- header: {
125
- slots: {
126
- container: 'max-w-full px-[clamp(1rem,2.5vw,2rem)]',
127
- },
128
- },
129
-
130
- // UPage: transparent subgrid participant. No addressable columns under
131
- // the fluid grid, so left/center/right all resolve to the same
132
- // full-width slot instead of a numbered sidebar split.
133
- page: {
134
- slots: {
135
- root: 'col-span-full grid grid-cols-subgrid',
136
- left: 'col-span-full',
137
- center: 'col-span-full',
138
- right: 'col-span-full',
139
- },
140
- },
141
-
142
- // UPageBody: no opinionated padding — the grid and sections own spacing
143
- pageBody: {
144
- base: '',
145
- },
146
-
147
- // UPageGrid: inherits subgrid column lines; gap matches --unit
148
- pageGrid: {
149
- base: 'col-span-full grid grid-cols-subgrid gap-[var(--unit)]',
150
- },
151
-
152
- // UPageColumns: inherits subgrid column lines
153
- pageColumns: {
154
- base: 'col-span-full grid grid-cols-subgrid',
155
- },
156
- },
157
-
158
- layoutLayer: {
159
- ui: {
160
- grid: {
161
- /**
162
- * Layout mode.
163
- * - 'fluid' — the --fs-derived fluid grid (default)
164
- * - 'disabled' — falls back to a plain, ungridded <main>
165
- */
166
- mode: 'fluid',
167
-
168
- // Tunables feed CSS custom properties via useGridConfig().cssVars,
169
- // bound on LayoutMain. See docs/LAYOUT.md for what each one does.
170
- tunables: {
171
- measureMin: '22rem',
172
- edgeMin: { min: 'var(--unit)', max: 'calc(var(--unit) * 3)' },
173
- lpf: { min: 4, max: 8 },
174
- },
175
-
176
- // Z-index layers — unchanged from the old system
177
- layers: {
178
- back: 0,
179
- mid: 10,
180
- front: 20,
181
- top: 30,
182
- header: 100,
183
- dropdown: 200,
184
- overlay: 300,
185
- modal: 400,
186
- toast: 500,
187
- },
188
- },
189
- },
190
- },
191
- })
192
-
193
- declare module '@nuxt/schema' {
194
- interface AppConfigInput {
195
- layoutLayer?: {
196
- name?: string
197
- ui?: {
198
- grid?: GridConfig
199
- }
200
- }
201
- }
202
- }
203
- ```
204
-
205
- - [ ] **Step 3: Typecheck**
206
-
207
- Run: `pnpm --filter kmcom-layer-layout typecheck`
208
- Expected: no errors from `layouts.ts`/`app.config.ts` (downstream files that still import `GridPresetsItem`/`getPreset` will fail here — that's expected, fixed in later tasks of this same plan, not a regression to chase down yet).
209
-
210
- - [ ] **Step 4: Commit**
211
-
212
- ```bash
213
- git add layers/structure/layout/app/types/layouts.ts layers/structure/layout/app/app.config.ts
214
- git commit -m "feat(layout): replace preset-based grid config with fluid tunables"
215
- ```
216
-
217
- ---
218
-
219
- ### Task 2: Fluid grid CSS engine
220
-
221
- **Files:**
222
-
223
- - Modify: `layers/structure/layout/app/assets/css/layout/grids.css`
224
-
225
- **Interfaces:**
226
-
227
- - Produces: CSS classes `.layout-grid`, `.layout-section`, `.layout-section[data-align='center']`, `.layout-section[data-align='split']`, `.layout-cell`, `.layout-cell--block`, `.layout-cell--plate`, `.layout-cell--span-2`, `.layout-cell--span-2-rows`, `.layout-cell--span-3-rows`, `.layout-hero`, and custom properties `--fs`/`--unit`/`--edge-min`/`--measure-min`/`--lpf` on `:root` — every component task below (3 onward) targets these exact class names.
228
- - Consumes: nothing (this is the base layer).
229
-
230
- - [ ] **Step 1: Replace the "Root grid" and "Section" sections of `grids.css`, keep the rhythm utilities**
231
-
232
- The file currently has three sections: `.grid-root`, `.basesection`, and the rhythm utilities (`.leading-rhythm-*`, `.space-rhythm-*`, `.prose-rhythm`). Replace only the first two; the rhythm utilities are unrelated to grid mechanics and stay as-is. Full new file:
233
-
234
- ```css
235
- /**
236
- * Fluid Grid System - CSS Utilities
237
- *
238
- * --fs-derived, breakpoint-free grid. One root unit (--unit, derived from
239
- * --fs) drives column floor, edge margins, and per-section vertical
240
- * rhythm. Column count comes from native `auto-fill` — never a
241
- * calc()/round()-derived integer fed into repeat() (see Pitfall 1 below).
242
- *
243
- * Full derivation and pitfalls:
244
- * docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md
245
- *
246
- * Hierarchy:
247
- * LayoutMain (.layout-grid) ← auto-fill columns, --fs owner
248
- * └── LayoutSection (.layout-section) ← opt-in, subgrid, own field rhythm
249
- * └── LayoutCell (.layout-cell) ← block/plate variant, span-N utility
250
- * LayoutHero (.layout-hero) ← deliberately outside the grid
251
- */
252
-
253
- /* Every derived custom property is registered so an invalid calc() chain
254
- fails loud (declaration invalidates, layout visibly breaks) instead of
255
- silently substituting as a token stream and squashing cells — the exact
256
- bug this design already hit once (Pitfall 1). */
257
- @property --fs {
258
- syntax: '<length>';
259
- inherits: true;
260
- initial-value: 1rem;
261
- }
262
-
263
- @property --unit {
264
- syntax: '<length>';
265
- inherits: true;
266
- initial-value: 1rem;
267
- }
268
-
269
- @property --edge-min {
270
- syntax: '<length>';
271
- inherits: true;
272
- initial-value: 1rem;
273
- }
274
-
275
- @property --measure-min {
276
- syntax: '<length>';
277
- inherits: true;
278
- initial-value: 22rem;
279
- }
280
-
281
- @property --lpf {
282
- syntax: '<integer>';
283
- inherits: true;
284
- initial-value: 8;
285
- }
286
-
287
- :root {
288
- /* Every clamp() below keeps a rem term (WCAG 1.4.4 — pure vw/cqi ignores
289
- browser zoom) and uses svi/svh, never dvh/vw/vh (dvh recomputes on
290
- mobile URL-bar collapse, reflowing mid-scroll). */
291
- --fs: clamp(1rem, 0.95rem + 0.25svi, 1.125rem);
292
- font-size: var(--fs);
293
- /* round(), not raw multiplication, so grid boundaries land on-pixel. */
294
- line-height: round(nearest, calc(var(--fs) * 1.5), 1px);
295
-
296
- /* rlh, not lh — lh resolves against the *current* element, so a heading
297
- with a different line-height would silently produce a different unit
298
- for anything nested inside it. rlh always resolves against root. */
299
- --unit: 1rlh;
300
- --measure-min: 22rem;
301
- /* Margin floor > 0 (never flush edge), steep svi slope so it shrinks
302
- fast on narrow screens. Feeds --avail-b in .layout-section, so it also
303
- trims top/bottom slack, not just inline edges. */
304
- --edge-min: clamp(var(--unit), 8svi, calc(var(--unit) * 3));
305
- /* Lines-per-field, clamped 4-8, scaled by viewport height so a fixed
306
- lpf=8 can't strand up to 8 line-heights as dead padding on a short
307
- viewport. */
308
- --lpf: clamp(4, round(down, calc(100svh / var(--unit) / 4.5), 1), 8);
309
- }
310
-
311
- /**
312
- * .layout-grid - Root fluid grid
313
- *
314
- * Continuous auto-fill grid — content not wrapped in .layout-section flows
315
- * and wraps normally, sized only by --measure-min/--unit. No viewport-
316
- * height chunking here; that's opt-in via .layout-section.
317
- */
318
- .layout-grid {
319
- display: grid;
320
- /* auto-fill computes column count natively (floor((avail+gutter) /
321
- (measure+gutter))) — no var()-derived integer needed. See Pitfall 1:
322
- repeat()'s track-count argument requires a literal <integer> at
323
- computed-value time; an unregistered calc()/round()-derived custom
324
- property there invalidates the whole declaration, falling back to
325
- implicit, content-sized auto-placement — cells silently squash to
326
- unreadable slivers. */
327
- grid-template-columns: repeat(auto-fill, minmax(var(--measure-min), 1fr));
328
- grid-auto-rows: minmax(calc(var(--lpf) * var(--unit)), max-content);
329
- /* Belt-and-braces: if a span ever outruns the explicit tracks, the
330
- forced implicit column is sized the same way, not auto/content
331
- (Pitfall 2). */
332
- grid-auto-columns: minmax(var(--measure-min), 1fr);
333
- gap: var(--unit);
334
- box-sizing: border-box;
335
- container-name: layout-viewport;
336
- /* Boundary for .layout-section's viewport-scoped span-2 gate below. */
337
- container-type: inline-size;
338
- padding-inline: var(--edge-min);
339
- width: 100%;
340
- max-width: 100%;
341
- /* clip without creating a scroll container (overflow-x:hidden would
342
- force overflow-y:auto) */
343
- overflow-x: clip;
344
- }
345
-
346
- /**
347
- * .layout-section - Opt-in full-width subgrid row
348
- *
349
- * Spans every column of the root's resolved auto-fill grid via subgrid —
350
- * legal because subgrid adopts however many explicit tracks the parent
351
- * resolved, whatever that count is; it only breaks when something tries
352
- * to *address* a column by number, which nothing here does. Runs its own
353
- * --lines/--fields/--slack derivation, scoped to itself, using the shared
354
- * global --unit/--edge-min/--lpf from :root.
355
- */
356
- .layout-section {
357
- display: grid;
358
- grid-template-columns: subgrid;
359
- grid-column: 1 / -1;
360
- box-sizing: border-box;
361
- container-name: layout-section;
362
- container-type: inline-size;
363
- }
364
-
365
- .layout-section[data-align='center'] {
366
- place-content: center;
367
- }
368
-
369
- .layout-section[data-align='split'] {
370
- --avail-b: calc(100svh - 2 * var(--edge-min));
371
- --lines: calc(round(down, var(--avail-b), var(--unit)) / var(--unit));
372
- --fields: max(1, round(down, calc((var(--lines) + 1) / (var(--lpf) + 1)), 1));
373
- --type-b: calc((var(--fields) * (var(--lpf) + 1) - 1) * var(--unit));
374
- /* max(0px, ...) keeps padding from going negative when the block-axis
375
- field system can't fit (short landscape viewport) — the section just
376
- scrolls instead of forcing an impossible field count. */
377
- --slack: calc(100svh - var(--type-b));
378
- padding-block: max(0px, calc(var(--slack) * 0.4)) max(0px, calc(var(--slack) * 0.6));
379
- }
380
-
381
- /**
382
- * .layout-cell - Positioned/styled grid child
383
- *
384
- * variant is applied via a modifier class (--block / --plate), span via
385
- * boolean utility classes. No numbered colStart/rowStart placement.
386
- */
387
- .layout-cell {
388
- container-name: layout-cell;
389
- container-type: inline-size;
390
- }
391
-
392
- .layout-cell--block {
393
- border: 1px solid currentColor;
394
- padding: calc(var(--unit) / 2);
395
- }
396
-
397
- .layout-cell--plate {
398
- background: var(--ui-bg-elevated, #16161a);
399
- padding: calc(var(--unit) / 2);
400
- color: var(--ui-text-highlighted, #fbfbf9);
401
- }
402
-
403
- /* Threshold mirrors --measure-min (22rem) — container query conditions
404
- can't reference custom properties, so keep this in sync by hand. */
405
- @container layout-cell (max-width: 22rem) {
406
- .layout-cell--block {
407
- padding-inline: calc(var(--unit) / 4);
408
- }
409
- }
410
-
411
- .layout-cell--span-2-rows {
412
- grid-row: span 2;
413
- }
414
-
415
- .layout-cell--span-3-rows {
416
- grid-row: span 3;
417
- }
418
-
419
- /* .layout-cell--span-2 only claims a 2nd column once the viewport
420
- container has genuinely resolved 2 — below this it stays a normal 1x1
421
- cell instead of forcing CSS Grid to fabricate a content-sized implicit
422
- column (Pitfall 2's squash/overflow bug). Threshold is a deliberately
423
- conservative 2 * --measure-min + gutter; hand-kept in sync with
424
- --measure-min since container conditions can't read custom properties. */
425
- @container layout-viewport (min-width: calc(2 * 22rem + 1rlh)) {
426
- .layout-cell--span-2 {
427
- grid-column: span 2;
428
- }
429
- }
430
-
431
- /**
432
- * .layout-hero - Deliberately outside the grid system
433
- *
434
- * No --fields, no column derivation — matches the doc's own .hero.
435
- */
436
- .layout-hero {
437
- display: flex;
438
- flex-direction: column;
439
- justify-content: center;
440
- align-items: center;
441
- gap: var(--unit);
442
- padding-inline: var(--edge-min);
443
- min-height: 100svh;
444
- text-align: center;
445
- }
446
-
447
- /**
448
- * Baseline Grid / Vertical Rhythm Utilities
449
- *
450
- * Maintains consistent vertical spacing based on a configurable baseline unit.
451
- * Default rhythm unit: 0.25rem (4px)
452
- */
453
-
454
- /* Line height utilities */
455
- .leading-rhythm-4 {
456
- --rhythm: 0.25rem;
457
-
458
- line-height: calc(var(--rhythm) * 4); /* 1rem */
459
- }
460
-
461
- .leading-rhythm-5 {
462
- --rhythm: 0.25rem;
463
-
464
- line-height: calc(var(--rhythm) * 5); /* 1.25rem */
465
- }
466
-
467
- .leading-rhythm-6 {
468
- --rhythm: 0.25rem;
469
-
470
- line-height: calc(var(--rhythm) * 6); /* 1.5rem */
471
- }
472
-
473
- .leading-rhythm-7 {
474
- --rhythm: 0.25rem;
475
-
476
- line-height: calc(var(--rhythm) * 7); /* 1.75rem */
477
- }
478
-
479
- .leading-rhythm-8 {
480
- --rhythm: 0.25rem;
481
-
482
- line-height: calc(var(--rhythm) * 8); /* 2rem */
483
- }
484
-
485
- /* Vertical spacing utilities */
486
- .space-rhythm-1 {
487
- --rhythm: 0.25rem;
488
-
489
- margin-block: var(--rhythm); /* 0.25rem */
490
- }
491
-
492
- .space-rhythm-2 {
493
- --rhythm: 0.25rem;
494
-
495
- margin-block: calc(var(--rhythm) * 2); /* 0.5rem */
496
- }
497
-
498
- .space-rhythm-4 {
499
- --rhythm: 0.25rem;
500
-
501
- margin-block: calc(var(--rhythm) * 4); /* 1rem */
502
- }
503
-
504
- .space-rhythm-6 {
505
- --rhythm: 0.25rem;
506
-
507
- margin-block: calc(var(--rhythm) * 6); /* 1.5rem */
508
- }
509
-
510
- .space-rhythm-8 {
511
- --rhythm: 0.25rem;
512
-
513
- margin-block: calc(var(--rhythm) * 8); /* 2rem */
514
- }
515
-
516
- /* Prose container - auto-applies rhythm to children */
517
- .prose-rhythm {
518
- --rhythm: 0.25rem;
519
-
520
- & > * + * {
521
- margin-block-start: calc(var(--rhythm) * 4); /* 1rem between elements */
522
- }
523
-
524
- & > h1,
525
- & > h2,
526
- & > h3,
527
- & > h4,
528
- & > h5,
529
- & > h6 {
530
- margin-block: calc(var(--rhythm) * 8) calc(var(--rhythm) * 4); /* 2rem before, 1rem after headings */
531
- line-height: calc(var(--rhythm) * 8);
532
- }
533
-
534
- & > p {
535
- line-height: calc(var(--rhythm) * 6); /* 1.5rem for paragraphs */
536
- }
537
- }
538
- ```
539
-
540
- - [ ] **Step 2: Lint the CSS**
541
-
542
- Run: `pnpm --filter kmcom-layer-layout lint`
543
- Expected: no stylelint errors. If `custom-property-pattern` flags `--_rs`-style leftovers from the old `.gi-placed` rules, those rules no longer exist in this file — ignore/fix only findings inside `grids.css`.
544
-
545
- - [ ] **Step 3: Commit**
546
-
547
- ```bash
548
- git add layers/structure/layout/app/assets/css/layout/grids.css
549
- git commit -m "feat(layout): replace breakpoint grid CSS with --fs-derived fluid engine"
550
- ```
551
-
552
- ---
553
-
554
- ### Task 3: `useGridConfig` composable
555
-
556
- **Files:**
557
-
558
- - Modify: `layers/structure/layout/app/composables/useGridConfig.ts`
559
- - Test: `tests/nuxt/use-grid-config.test.ts`
560
-
561
- **Interfaces:**
562
-
563
- - Consumes: `GridConfig`, `GridLayers`, `GridMode` from `layers/structure/layout/app/types/layouts.ts` (Task 1).
564
- - Produces: `useGridConfig(): { config: ComputedRef<GridConfig | undefined>; mode: ComputedRef<GridMode>; isEnabled: ComputedRef<boolean>; layers: ComputedRef<GridLayers | undefined>; useZIndex: (layer: keyof GridLayers) => number; cssVars: ComputedRef<Record<string, string>> }` — `LayoutMain` (Task 4) binds `cssVars` via `:style` and reads `mode`.
565
-
566
- - [ ] **Step 1: Rewrite the test file first**
567
-
568
- Replace `tests/nuxt/use-grid-config.test.ts` entirely:
569
-
570
- ```ts
571
- import { defineComponent } from 'vue'
572
- import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime'
573
- import { describe, expect, it } from 'vitest'
574
-
575
- import { useGridConfig } from '../../layers/structure/layout/app/composables/useGridConfig'
576
- import type { GridConfig, GridLayers } from '../../layers/structure/layout/app/types/layouts'
577
-
578
- type LayoutAppConfig = {
579
- layoutLayer?: {
580
- ui?: {
581
- grid?: GridConfig
582
- }
583
- }
584
- }
585
-
586
- const layers: GridLayers = {
587
- back: 0,
588
- mid: 10,
589
- front: 20,
590
- top: 30,
591
- header: 100,
592
- dropdown: 200,
593
- overlay: 300,
594
- modal: 400,
595
- toast: 500,
596
- }
597
-
598
- var appConfig: LayoutAppConfig = {
599
- layoutLayer: {
600
- ui: {
601
- grid: {
602
- mode: 'fluid',
603
- tunables: {
604
- measureMin: '22rem',
605
- edgeMin: { min: 'var(--unit)', max: 'calc(var(--unit) * 3)' },
606
- lpf: { min: 4, max: 8 },
607
- },
608
- layers,
609
- },
610
- },
611
- },
612
- }
613
-
614
- mockNuxtImport('useAppConfig', () => () => appConfig)
615
-
616
- async function mountGridConfig() {
617
- let resolved: ReturnType<typeof useGridConfig> | undefined
618
-
619
- await mountSuspended(
620
- defineComponent({
621
- setup() {
622
- resolved = useGridConfig()
623
- return () => null
624
- },
625
- })
626
- )
627
-
628
- if (!resolved) {
629
- throw new Error('grid composable did not resolve')
630
- }
631
-
632
- return resolved
633
- }
634
-
635
- describe('useGridConfig', () => {
636
- it('defaults to fluid mode when mode is omitted', async () => {
637
- appConfig = {
638
- layoutLayer: {
639
- ui: {
640
- grid: {
641
- tunables: {
642
- measureMin: '22rem',
643
- edgeMin: { min: 'var(--unit)', max: 'calc(var(--unit) * 3)' },
644
- lpf: { min: 4, max: 8 },
645
- },
646
- layers,
647
- },
648
- },
649
- },
650
- }
651
-
652
- const resolved = await mountGridConfig()
653
-
654
- expect(resolved.mode.value).toBe('fluid')
655
- expect(resolved.isEnabled.value).toBe(true)
656
- expect(resolved.useZIndex('modal')).toBe(400)
657
- expect(resolved.layers.value).toEqual(layers)
658
- })
659
-
660
- it('respects an explicit disabled mode', async () => {
661
- appConfig = {
662
- layoutLayer: {
663
- ui: {
664
- grid: {
665
- mode: 'disabled',
666
- tunables: {
667
- measureMin: '24rem',
668
- edgeMin: { min: 'var(--unit)', max: 'calc(var(--unit) * 3)' },
669
- lpf: { min: 4, max: 8 },
670
- },
671
- layers,
672
- },
673
- },
674
- },
675
- }
676
-
677
- const resolved = await mountGridConfig()
678
-
679
- expect(resolved.mode.value).toBe('disabled')
680
- expect(resolved.isEnabled.value).toBe(false)
681
- expect(resolved.useZIndex('toast')).toBe(500)
682
- })
683
-
684
- it('maps tunables to CSS custom properties', async () => {
685
- appConfig = {
686
- layoutLayer: {
687
- ui: {
688
- grid: {
689
- mode: 'fluid',
690
- tunables: {
691
- measureMin: '24rem',
692
- edgeMin: { min: 'var(--unit)', max: 'calc(var(--unit) * 4)' },
693
- lpf: { min: 5, max: 9 },
694
- },
695
- layers,
696
- },
697
- },
698
- },
699
- }
700
-
701
- const resolved = await mountGridConfig()
702
-
703
- expect(resolved.cssVars.value['--measure-min']).toBe('24rem')
704
- expect(resolved.cssVars.value['--edge-min']).toBe(
705
- 'clamp(var(--unit), 8svi, calc(var(--unit) * 4))'
706
- )
707
- expect(resolved.cssVars.value['--lpf']).toBe(
708
- 'clamp(5, round(down, calc(100svh / var(--unit) / 4.5), 1), 9)'
709
- )
710
- })
711
- })
712
- ```
713
-
714
- - [ ] **Step 2: Run the test to verify it fails**
715
-
716
- Run: `pnpm vitest run tests/nuxt/use-grid-config.test.ts`
717
- Expected: FAIL — `useGridConfig` still exports `getPreset`/references `enabled`/`presets`, `mode`/`cssVars` don't match yet.
718
-
719
- - [ ] **Step 3: Rewrite `useGridConfig.ts`**
720
-
721
- ```ts
722
- import type { GridConfig, GridLayers, GridMode } from '#layers/layout/app/types/layouts'
723
-
724
- type LayoutLayerConfig = {
725
- layoutLayer?: {
726
- name?: string
727
- ui?: {
728
- grid?: GridConfig
729
- }
730
- }
731
- }
732
-
733
- export function useGridConfig() {
734
- const appConfig = useAppConfig() as LayoutLayerConfig
735
- const gridConfig = computed(() => appConfig.layoutLayer?.ui?.grid)
736
-
737
- /** Resolved layout mode. Defaults to 'fluid' when omitted. */
738
- const mode = computed<GridMode>(() => gridConfig.value?.mode ?? 'fluid')
739
-
740
- /** True when the fluid grid is active. */
741
- const isEnabled = computed(() => mode.value !== 'disabled')
742
-
743
- /**
744
- * Returns the z-index value for a named stacking layer.
745
- *
746
- * @example
747
- * const zModal = useZIndex('modal') // → 400
748
- */
749
- const useZIndex = (layer: keyof GridLayers): number => {
750
- return gridConfig.value?.layers?.[layer] ?? 0
751
- }
752
-
753
- /**
754
- * Config tunables mapped to the CSS custom properties LayoutMain binds
755
- * via :style. This is the JS→CSS bridge — see spec §9 for why it carries
756
- * no dedicated TS type beyond GridConfig itself.
757
- */
758
- const cssVars = computed(() => {
759
- const tunables = gridConfig.value?.tunables
760
- if (!tunables) return {}
761
-
762
- return {
763
- '--measure-min': tunables.measureMin,
764
- '--edge-min': `clamp(var(--unit), 8svi, ${tunables.edgeMin.max})`,
765
- '--lpf': `clamp(${tunables.lpf.min}, round(down, calc(100svh / var(--unit) / 4.5), 1), ${tunables.lpf.max})`,
766
- }
767
- })
768
-
769
- return {
770
- config: gridConfig,
771
- layers: computed(() => gridConfig.value?.layers),
772
- isEnabled,
773
- mode,
774
- useZIndex,
775
- cssVars,
776
- }
777
- }
778
- ```
779
-
780
- - [ ] **Step 4: Run the test to verify it passes**
781
-
782
- Run: `pnpm vitest run tests/nuxt/use-grid-config.test.ts`
783
- Expected: PASS (3 tests)
784
-
785
- - [ ] **Step 5: Commit**
786
-
787
- ```bash
788
- git add layers/structure/layout/app/composables/useGridConfig.ts tests/nuxt/use-grid-config.test.ts
789
- git commit -m "feat(layout): rebuild useGridConfig around fluid tunables and cssVars"
790
- ```
791
-
792
- ---
793
-
794
- ### Task 4: `LayoutMain` rebuild
795
-
796
- **Files:**
797
-
798
- - Modify: `layers/structure/layout/app/components/Layout/Main.vue`
799
-
800
- **Interfaces:**
801
-
802
- - Consumes: `useGridConfig()` → `{ mode, cssVars }` (Task 3).
803
- - Produces: renders `.layout-grid` (Task 2's CSS) with `cssVars` bound as inline custom properties. `LayoutSection`/`LayoutCell` (Tasks 5-6) rely on being mounted inside this element for `--unit`/`--edge-min`/`--measure-min`/`--lpf` to resolve from the config-driven values rather than the `:root` defaults alone.
804
-
805
- - [ ] **Step 1: Rewrite `Main.vue`**
806
-
807
- ```vue
808
- <script setup lang="ts">
809
- /**
810
- * LayoutMain — root of the fluid grid.
811
- *
812
- * Renders `.layout-grid` and binds the config-driven tunables
813
- * (measureMin/edgeMin/lpf) as inline CSS custom properties, overriding
814
- * the :root defaults from grids.css for this subtree.
815
- *
816
- * When `mode` is `'disabled'`, falls back to a plain semantic `<main>`
817
- * so the page renders correctly without grid dependencies.
818
- *
819
- * @prop {string} tag — HTML element to render (default: 'main')
820
- *
821
- * @example
822
- * <LayoutMain>
823
- * <LayoutSection>…</LayoutSection>
824
- * </LayoutMain>
825
- */
826
-
827
- interface Props {
828
- tag?: string
829
- }
830
-
831
- const { tag = 'main' } = defineProps<Props>()
832
-
833
- const { mode, cssVars } = useGridConfig()
834
- </script>
835
-
836
- <template>
837
- <component
838
- :is="tag"
839
- :class="mode !== 'disabled' ? 'layout-grid' : undefined"
840
- :style="mode !== 'disabled' ? cssVars : undefined"
841
- >
842
- <slot />
843
- </component>
844
- </template>
845
- ```
846
-
847
- - [ ] **Step 2: Typecheck**
848
-
849
- Run: `pnpm --filter kmcom-layer-layout typecheck`
850
- Expected: no errors from `Main.vue`.
851
-
852
- - [ ] **Step 3: Commit**
853
-
854
- ```bash
855
- git add layers/structure/layout/app/components/Layout/Main.vue
856
- git commit -m "feat(layout): rebuild LayoutMain around the fluid grid"
857
- ```
858
-
859
- ---
860
-
861
- ### Task 5: `LayoutSection` (new) and deletion of the old Section directory
862
-
863
- **Files:**
864
-
865
- - Create: `layers/structure/layout/app/components/Layout/Section.vue`
866
- - Delete: `layers/structure/layout/app/components/Layout/Section/index.vue`
867
- - Delete: `layers/structure/layout/app/components/Layout/Section/Gallery.vue`
868
- - Delete: `layers/structure/layout/app/components/Layout/Section/Hero.vue`
869
- - Delete: `layers/structure/layout/app/components/Layout/Section/Split.vue`
870
-
871
- **Interfaces:**
872
-
873
- - Produces: `LayoutSection` — props `{ height?: string; align?: 'split' | 'center' }`, renders `.layout-section` (Task 2's CSS) with `data-align` and `block-size` bound.
874
-
875
- - [ ] **Step 1: Delete the old Section directory**
876
-
877
- ```bash
878
- git rm -r layers/structure/layout/app/components/Layout/Section
879
- ```
880
-
881
- - [ ] **Step 2: Create `Section.vue`**
882
-
883
- ```vue
884
- <script setup lang="ts">
885
- /**
886
- * LayoutSection — opt-in full-width subgrid row.
887
- *
888
- * Spans every column of the parent LayoutMain's resolved auto-fill grid
889
- * via CSS subgrid, so content inside stays aligned to the same gutters
890
- * as everything else on the page. Defaults to a 100svh block, running
891
- * its own --lines/--fields/--slack derivation scoped to itself.
892
- *
893
- * `align="split"` (default) uses the doc's asymmetric 40/60 optical
894
- * padding split. `align="center"` uses `place-content: center` for true
895
- * horizontal+vertical centering — pick this when you want a centered
896
- * hero-like section without fighting align-items: stretch defaults.
897
- *
898
- * @prop {string} height — block-size (default: '100svh')
899
- * @prop {'split' | 'center'} align — vertical rhythm mode (default: 'split')
900
- *
901
- * @example
902
- * <LayoutSection align="center">
903
- * <LayoutCell variant="plate">Centered content</LayoutCell>
904
- * </LayoutSection>
905
- */
906
-
907
- interface Props {
908
- height?: string
909
- align?: 'split' | 'center'
910
- }
911
-
912
- const { height = '100svh', align = 'split' } = defineProps<Props>()
913
- </script>
914
-
915
- <template>
916
- <section class="layout-section" :data-align="align" :style="{ blockSize: height }">
917
- <slot />
918
- </section>
919
- </template>
920
- ```
921
-
922
- - [ ] **Step 3: Typecheck**
923
-
924
- Run: `pnpm --filter kmcom-layer-layout typecheck`
925
- Expected: `Section.vue` itself introduces no errors. Remaining errors from files still importing the deleted `Section/index.vue` etc. are expected until Task 9 (playground) — do not fix them here.
926
-
927
- - [ ] **Step 4: Commit**
928
-
929
- ```bash
930
- git add layers/structure/layout/app/components/Layout/Section.vue
931
- git commit -m "feat(layout): add LayoutSection, drop the old subgrid Section directory"
932
- ```
933
-
934
- ---
935
-
936
- ### Task 6: `LayoutCell` (replaces `LayoutGridItem`)
937
-
938
- **Files:**
939
-
940
- - Create: `layers/structure/layout/app/components/Layout/Cell.vue`
941
- - Delete: `layers/structure/layout/app/components/Layout/Grid/Item.vue`
942
- - Delete: `layers/structure/layout/app/utils/gridPlacementStyle.ts`
943
- - Delete: `layers/structure/layout/app/utils/gridPlacementStyle.test.ts`
944
-
945
- **Interfaces:**
946
-
947
- - Produces: `LayoutCell` — props `{ as?: string; variant?: 'block' | 'plate'; span2?: boolean; span2Rows?: boolean; span3Rows?: boolean }`, renders the element with `layout-cell layout-cell--<variant>` plus the requested span modifier classes.
948
-
949
- - [ ] **Step 1: Delete the old GridItem component and placement utility**
950
-
951
- ```bash
952
- git rm layers/structure/layout/app/components/Layout/Grid/Item.vue
953
- git rm layers/structure/layout/app/utils/gridPlacementStyle.ts
954
- git rm layers/structure/layout/app/utils/gridPlacementStyle.test.ts
955
- ```
956
-
957
- - [ ] **Step 2: Create `Cell.vue`**
958
-
959
- ```vue
960
- <script setup lang="ts">
961
- /**
962
- * LayoutCell — a positioned/styled grid child.
963
- *
964
- * Replaces LayoutGridItem. No numbered colStart/rowStart placement —
965
- * emergent auto-fill columns have no addressable count to position
966
- * against. `variant` picks border-vs-filled styling; the span props are
967
- * boolean utility classes, not coordinates.
968
- *
969
- * @prop {string} as — HTML element to render (default: 'div')
970
- * @prop {'block' | 'plate'} variant — border vs. filled styling (default: 'block')
971
- * @prop {boolean} span2 — claim 2 columns once the viewport container has resolved 2 (see grids.css)
972
- * @prop {boolean} span2Rows — span 2 rows
973
- * @prop {boolean} span3Rows — span 3 rows
974
- *
975
- * @example
976
- * <LayoutCell variant="plate" span2>
977
- * <h2>Featured</h2>
978
- * </LayoutCell>
979
- */
980
-
981
- interface Props {
982
- as?: string
983
- variant?: 'block' | 'plate'
984
- span2?: boolean
985
- span2Rows?: boolean
986
- span3Rows?: boolean
987
- }
988
-
989
- const {
990
- as = 'div',
991
- variant = 'block',
992
- span2 = false,
993
- span2Rows = false,
994
- span3Rows = false,
995
- } = defineProps<Props>()
996
-
997
- const classes = computed(() => [
998
- 'layout-cell',
999
- `layout-cell--${variant}`,
1000
- span2 && 'layout-cell--span-2',
1001
- span2Rows && 'layout-cell--span-2-rows',
1002
- span3Rows && 'layout-cell--span-3-rows',
1003
- ])
1004
- </script>
1005
-
1006
- <template>
1007
- <component :is="as" :class="classes">
1008
- <slot />
1009
- </component>
1010
- </template>
1011
- ```
1012
-
1013
- - [ ] **Step 3: Typecheck**
1014
-
1015
- Run: `pnpm --filter kmcom-layer-layout typecheck`
1016
- Expected: `Cell.vue` introduces no errors.
1017
-
1018
- - [ ] **Step 4: Commit**
1019
-
1020
- ```bash
1021
- git add layers/structure/layout/app/components/Layout/Cell.vue
1022
- git commit -m "feat(layout): add LayoutCell, drop LayoutGridItem and gridPlacementStyle"
1023
- ```
1024
-
1025
- ---
1026
-
1027
- ### Task 7: `LayoutHero` (new)
1028
-
1029
- **Files:**
1030
-
1031
- - Create: `layers/structure/layout/app/components/Layout/Hero.vue`
1032
-
1033
- **Interfaces:**
1034
-
1035
- - Produces: `LayoutHero` — prop `{ as?: string }`, renders `.layout-hero` (Task 2's CSS).
1036
-
1037
- - [ ] **Step 1: Create `Hero.vue`**
1038
-
1039
- ```vue
1040
- <script setup lang="ts">
1041
- /**
1042
- * LayoutHero — full-viewport, flex-centered, deliberately outside the
1043
- * grid entirely. No --fields, no column derivation — replaces the old
1044
- * `hero` LayoutGridItem preset.
1045
- *
1046
- * @prop {string} as — HTML element to render (default: 'section')
1047
- *
1048
- * @example
1049
- * <LayoutHero>
1050
- * <h1>Welcome</h1>
1051
- * </LayoutHero>
1052
- */
1053
-
1054
- interface Props {
1055
- as?: string
1056
- }
1057
-
1058
- const { as = 'section' } = defineProps<Props>()
1059
- </script>
1060
-
1061
- <template>
1062
- <component :is="as" class="layout-hero">
1063
- <slot />
1064
- </component>
1065
- </template>
1066
- ```
1067
-
1068
- - [ ] **Step 2: Typecheck**
1069
-
1070
- Run: `pnpm --filter kmcom-layer-layout typecheck`
1071
- Expected: `Hero.vue` introduces no errors.
1072
-
1073
- - [ ] **Step 3: Commit**
1074
-
1075
- ```bash
1076
- git add layers/structure/layout/app/components/Layout/Hero.vue
1077
- git commit -m "feat(layout): add LayoutHero"
1078
- ```
1079
-
1080
- ---
1081
-
1082
- ### Task 8: `LayoutGridDebug` rebuild
1083
-
1084
- **Files:**
1085
-
1086
- - Modify: `layers/structure/layout/app/components/Layout/Grid/Debug.vue`
1087
-
1088
- **Interfaces:**
1089
-
1090
- - Produces: `LayoutGridDebug` — no props, `defineExpose({ toggle })`, same `⌘/Ctrl+G` shortcut as before.
1091
-
1092
- - [ ] **Step 1: Rewrite `Debug.vue`**
1093
-
1094
- Column count is now emergent (native `auto-fill`), so there's nothing left to track in JS — the old `cols`/`updateCols`/`BREAKPOINT_PX` machinery goes away entirely. The overlay instead visualizes `--unit` as horizontal bands, which is a pure CSS `repeating-linear-gradient` keyed to the same custom property the grid itself uses:
1095
-
1096
- ```vue
1097
- <script setup lang="ts">
1098
- /**
1099
- * LayoutGridDebug - Visual --unit overlay
1100
- *
1101
- * Displays a semi-transparent overlay showing horizontal bands at every
1102
- * --unit (1rlh) — the same value the fluid grid derives its row floor
1103
- * and field rhythm from. Toggle visibility with Ctrl+G (Windows/Linux)
1104
- * or Cmd+G (Mac).
1105
- *
1106
- * @prop {string} color — band overlay color (default: rgba(255, 0, 0, 0.08))
1107
- *
1108
- * @example
1109
- * <LayoutGridDebug color="rgba(0, 100, 255, 0.12)" />
1110
- */
1111
-
1112
- interface Props {
1113
- color?: string
1114
- }
1115
-
1116
- const { color = 'rgba(255, 0, 0, 0.08)' } = defineProps<Props>()
1117
-
1118
- const visible = ref(false)
1119
-
1120
- const toggle = () => {
1121
- visible.value = !visible.value
1122
- }
1123
-
1124
- const handleKeydown = (event: KeyboardEvent) => {
1125
- if ((event.ctrlKey || event.metaKey) && event.key === 'g') {
1126
- event.preventDefault()
1127
- toggle()
1128
- }
1129
- }
1130
-
1131
- onMounted(() => {
1132
- window.addEventListener('keydown', handleKeydown)
1133
- })
1134
-
1135
- onUnmounted(() => {
1136
- window.removeEventListener('keydown', handleKeydown)
1137
- })
1138
-
1139
- const style = computed(() => ({
1140
- backgroundImage: `repeating-linear-gradient(to bottom, ${color} 0, ${color} 1px, transparent 1px, transparent var(--unit, 1rlh))`,
1141
- pointerEvents: 'none' as const,
1142
- }))
1143
-
1144
- defineExpose({ toggle })
1145
- </script>
1146
-
1147
- <template>
1148
- <Teleport to="body">
1149
- <div v-if="visible" :style class="layout-grid-debug fixed inset-0 z-9999" aria-hidden="true" />
1150
- </Teleport>
1151
- </template>
1152
- ```
1153
-
1154
- - [ ] **Step 2: Typecheck**
1155
-
1156
- Run: `pnpm --filter kmcom-layer-layout typecheck`
1157
- Expected: no `BREAKPOINT_PX` import errors, no errors from `Debug.vue`.
1158
-
1159
- - [ ] **Step 3: Commit**
1160
-
1161
- ```bash
1162
- git add layers/structure/layout/app/components/Layout/Grid/Debug.vue
1163
- git commit -m "feat(layout): rebuild LayoutGridDebug as a --unit band overlay"
1164
- ```
1165
-
1166
- ---
1167
-
1168
- ### Task 9: Playground demo page rewrite
1169
-
1170
- **Files:**
1171
-
1172
- - Modify: `apps/playground/app/pages/layout.vue`
1173
-
1174
- **Interfaces:**
1175
-
1176
- - Consumes: `LayoutMain`, `LayoutSection`, `LayoutCell`, `LayoutHero`, `LayoutGridDebug`, `LayoutPage` (unchanged), `useGridConfig()` — all from Tasks 3-8.
1177
-
1178
- - [ ] **Step 1: Rewrite `apps/playground/app/pages/layout.vue`**
1179
-
1180
- The old page documented the 6/12/18 preset system in detail; that entire API is gone. Replace it with a page that demonstrates the fluid grid live — this is the manual verification surface for Task 12, not just documentation:
1181
-
1182
- ```vue
1183
- <script setup lang="ts">
1184
- definePageMeta({
1185
- layout: false,
1186
- })
1187
-
1188
- const { setPageAccent } = useAccentColor()
1189
- setPageAccent('amber')
1190
- onUnmounted(() => setPageAccent(null))
1191
-
1192
- const cells = Array.from({ length: 8 }, (_, i) => ({
1193
- id: i + 1,
1194
- variant: i % 3 === 0 ? ('plate' as const) : ('block' as const),
1195
- span2: i === 0,
1196
- }))
1197
- </script>
1198
-
1199
- <template>
1200
- <LayoutPage title="Layout Layer" description="Demonstrating the fluid, --fs-derived layout grid">
1201
- <div class="bg-default min-h-screen">
1202
- <LayoutGridDebug />
1203
- <DemoPageHero
1204
- name="LAYOUT"
1205
- description="Fluid grid — one root unit derives column floor, edge margins, and per-section field rhythm. No breakpoints, no numbered placement."
1206
- />
1207
- <UContainer class="space-y-12 py-8">
1208
- <div class="flex items-center gap-4">
1209
- <UButton to="/" variant="ghost" icon="i-lucide-arrow-left" />
1210
- <div>
1211
- <h1 class="text-highlighted text-3xl font-bold">Layout Layer</h1>
1212
- <p class="text-muted">Fluid grid, sections, and page components</p>
1213
- </div>
1214
- <div class="ml-auto">
1215
- <UBadge color="primary" variant="soft">Cmd+G to toggle the --unit overlay</UBadge>
1216
- </div>
1217
- </div>
1218
-
1219
- <LayoutMain tag="div">
1220
- <!-- Continuous auto-fill grid — no LayoutSection wrapper needed -->
1221
- <section class="space-y-4">
1222
- <h2 class="text-2xl font-bold">Continuous grid (LayoutCell)</h2>
1223
- <p class="text-muted">
1224
- Auto-fill columns floor at --measure-min (22rem). Resize the window and watch the
1225
- column count change continuously — no breakpoint jump.
1226
- </p>
1227
- <div class="grid grid-cols-[repeat(auto-fill,minmax(22rem,1fr))] gap-[var(--unit)]">
1228
- <LayoutCell
1229
- v-for="cell in cells"
1230
- :key="cell.id"
1231
- :variant="cell.variant"
1232
- :span2="cell.span2"
1233
- >
1234
- <h3 class="font-mono text-sm">Cell {{ cell.id }}</h3>
1235
- <p class="text-muted text-sm">variant: {{ cell.variant }}</p>
1236
- </LayoutCell>
1237
- </div>
1238
- </section>
1239
-
1240
- <LayoutSection align="split" class="mt-12">
1241
- <div class="flex items-center justify-center">
1242
- <div class="space-y-2 text-center">
1243
- <h2 class="text-2xl font-bold">LayoutSection align="split"</h2>
1244
- <p class="text-muted">
1245
- 100svh block, optical 40/60 padding split derived from --lines/--fields.
1246
- </p>
1247
- </div>
1248
- </div>
1249
- </LayoutSection>
1250
-
1251
- <LayoutSection align="center">
1252
- <div class="space-y-2 text-center">
1253
- <h2 class="text-2xl font-bold">LayoutSection align="center"</h2>
1254
- <p class="text-muted">
1255
- place-content: center — true horizontal+vertical centering, no fighting align-items:
1256
- stretch.
1257
- </p>
1258
- </div>
1259
- </LayoutSection>
1260
- </LayoutMain>
1261
-
1262
- <LayoutHero>
1263
- <h2 class="text-3xl font-bold">LayoutHero</h2>
1264
- <p class="text-muted max-w-lg">
1265
- Deliberately outside the grid — no --fields, no column derivation.
1266
- </p>
1267
- </LayoutHero>
1268
-
1269
- <section class="space-y-8">
1270
- <div>
1271
- <h2 class="mb-2 text-2xl font-bold">Z-Index System</h2>
1272
- <p class="text-muted">
1273
- Unchanged from the old system — read via
1274
- <code class="font-mono text-sm">useGridConfig().useZIndex(layer)</code>
1275
- </p>
1276
- </div>
1277
- <UCard>
1278
- <pre
1279
- class="bg-muted text-highlighted overflow-x-auto rounded-lg p-4 text-sm"
1280
- ><code>const { useZIndex } = useGridConfig()
1281
- const zModal = useZIndex('modal') // → 400</code></pre>
1282
- </UCard>
1283
- </section>
1284
-
1285
- <div class="flex justify-center gap-4 pt-4">
1286
- <UButton to="/ui" variant="outline" icon="i-lucide-arrow-left">UI Layer Demo</UButton>
1287
- <UButton to="/" icon="i-lucide-home">Back to Home</UButton>
1288
- <UButton to="/core" variant="outline" trailing-icon="i-lucide-arrow-right">
1289
- Core Layer Demo
1290
- </UButton>
1291
- </div>
1292
- </UContainer>
1293
- <DemoPageFooter
1294
- name="Layout Layer"
1295
- description="Fluid, --fs-derived layout grid"
1296
- :links="[
1297
- { label: 'Core', to: '/core', icon: 'i-lucide-box' },
1298
- { label: 'UI Layer', to: '/ui', icon: 'i-lucide-palette' },
1299
- ]"
1300
- />
1301
- </div>
1302
- </LayoutPage>
1303
- </template>
1304
- ```
1305
-
1306
- - [ ] **Step 2: Typecheck the playground app**
1307
-
1308
- Run: `pnpm --filter playground typecheck`
1309
- Expected: no errors referencing `LayoutGridItem`, `LayoutSectionHero`, `LayoutSectionSplit`, `LayoutSectionGallery`, or the old preset props — this file was the only playground consumer of the deleted API.
1310
-
1311
- - [ ] **Step 3: Commit**
1312
-
1313
- ```bash
1314
- git add apps/playground/app/pages/layout.vue
1315
- git commit -m "feat(layout): rebuild the playground layout demo against the fluid grid"
1316
- ```
1317
-
1318
- ---
1319
-
1320
- ### Task 10: Documentation rewrite
1321
-
1322
- **Files:**
1323
-
1324
- - Modify: `docs/LAYOUT.md`
1325
- - Modify: `layers/structure/layout/CLAUDE.md`
1326
-
1327
- **Interfaces:**
1328
-
1329
- - Consumes: nothing new — documents Tasks 1-9's final API surface.
1330
-
1331
- - [ ] **Step 1: Rewrite `docs/LAYOUT.md`**
1332
-
1333
- ````markdown
1334
- # Layout Layer
1335
-
1336
- A fluid, `--fs`-derived grid for Nuxt 4 applications. One root unit derives column floor, edge margins, and per-section vertical rhythm — no breakpoints, no numbered column placement.
1337
-
1338
- ---
1339
-
1340
- ## Contents
1341
-
1342
- - [How it works](#how-it-works)
1343
- - [Playground setup](#playground-setup)
1344
- - [Mode system](#mode-system)
1345
- - [Components](#components)
1346
- - [Composable](#composable)
1347
- - [CSS custom properties](#css-custom-properties)
1348
- - [Z-index system](#z-index-system)
1349
- - [Config reference](#config-reference)
1350
- - [Known pitfalls](#known-pitfalls)
1351
-
1352
- ---
1353
-
1354
- ## How it works
1355
-
1356
- One root custom property, `--fs`, drives everything else via CSS `calc()`/`round()`:
1357
-
1358
- ```text
1359
- --fs (fluid font-size)
1360
- → line-height (rounded to px)
1361
- → --unit: 1rlh
1362
- → --edge-min, --lpf, --measure-min
1363
- → --lines, --fields, --slack (per LayoutSection)
1364
- → padding-block, grid tracks, gap
1365
- ```
1366
- ````
1367
-
1368
- The root grid (`LayoutMain`, class `.layout-grid`) is a continuous `repeat(auto-fill, minmax(--measure-min, 1fr))` grid — column count is native and emergent, not a config value. `LayoutSection` is an opt-in full-width row that adopts the root's resolved columns via CSS `subgrid` and runs its own vertical-rhythm derivation, scoped to itself. `LayoutCell` is a positioned/styled grid child — variant (`block`/`plate`) plus span utility classes, no numbered `colStart`/`rowStart`. `LayoutHero` sits deliberately outside the grid.
1369
-
1370
- ```text
1371
- LayoutMain (.layout-grid) ← auto-fill columns, --fs owner
1372
- LayoutSection (.layout-section) ← opt-in, subgrid, own field rhythm
1373
- LayoutCell (.layout-cell) ← variant + span-N utility classes
1374
- LayoutHero (.layout-hero) ← deliberately outside the grid
1375
- ```
1376
-
1377
- ---
1378
-
1379
- ## Playground setup
1380
-
1381
- Same as before — the layout layer needs a dedicated Nuxt layout file and page-level layout declaration.
1382
-
1383
- **1. Create `layouts/grid.vue`** in your app:
1384
-
1385
- ```vue
1386
- <template>
1387
- <LayoutMain>
1388
- <slot />
1389
- <LayoutGridDebug />
1390
- </LayoutMain>
1391
- </template>
1392
- ```
1393
-
1394
- **2. Declare the layout in each page that uses the grid:**
1395
-
1396
- ```vue
1397
- <script setup lang="ts">
1398
- definePageMeta({ layout: 'grid' })
1399
- </script>
1400
- ```
1401
-
1402
- > `LayoutPage` is a fragment component — it provides no wrapper element. The grid lives on `LayoutMain` in the layout file. Do not add a second `LayoutMain` inside `LayoutPage`.
1403
-
1404
- ---
1405
-
1406
- ## Mode system
1407
-
1408
- ```ts
1409
- // app.config.ts
1410
- layoutLayer.ui.grid.mode: 'fluid' | 'disabled'
1411
- ```
1412
-
1413
- - `'fluid'` — default, the full `--fs`-derived grid
1414
- - `'disabled'` — plain `<main>`, no grid CSS
1415
-
1416
- ---
1417
-
1418
- ## Components
1419
-
1420
- ### `LayoutMain` — `<main class="layout-grid">`
1421
-
1422
- - **Props:** `tag` (default `'main'`)
1423
- - Binds the config-driven tunables (`--measure-min`/`--edge-min`/`--lpf`) as inline CSS custom properties
1424
- - Renders a plain element (no class/style) when `mode === 'disabled'`
1425
-
1426
- ### `LayoutSection` — opt-in full-width subgrid row
1427
-
1428
- - **Props:** `height` (default `'100svh'`), `align: 'split' | 'center'` (default `'split'`)
1429
- - `align="split"` — the doc's asymmetric 40/60 optical padding split
1430
- - `align="center"` — `place-content: center`, true horizontal+vertical centering
1431
-
1432
- ### `LayoutCell` — positioned/styled grid child
1433
-
1434
- - **Props:** `as` (default `'div'`), `variant: 'block' | 'plate'` (default `'block'`), `span2`, `span2Rows`, `span3Rows` (all boolean, default `false`)
1435
- - No `colStart`/`colSpan`/`rowStart`/`rowSpan` — see [Known pitfalls](#known-pitfalls) for why
1436
-
1437
- ### `LayoutHero` — full-viewport, outside the grid
1438
-
1439
- - **Props:** `as` (default `'section'`)
1440
- - No `--fields`, no column derivation
1441
-
1442
- ### `LayoutGridDebug` — `⌘/Ctrl+G` overlay
1443
-
1444
- - **Props:** `color` (default `'rgba(255, 0, 0, 0.08)'`)
1445
- - Visualizes `--unit` as horizontal bands — no JS column tracking (column count is native `auto-fill`, nothing to track)
1446
-
1447
- ### `AppContainer` (from `layers/core`) — width constraint
1448
-
1449
- - **Props:** `size` (`'content' | 'wide' | 'fluid' | 'full'`, default `'wide'`), `tag`
1450
- - Unchanged
1451
-
1452
- ---
1453
-
1454
- ## Composable
1455
-
1456
- ```ts
1457
- const { config, mode, isEnabled, layers, useZIndex, cssVars } = useGridConfig()
1458
- ```
1459
-
1460
- | Return | Description |
1461
- | ------------------ | --------------------------------------------------------------------------- |
1462
- | `config` | Raw `GridConfig` from app.config |
1463
- | `mode` | `'fluid' \| 'disabled'` |
1464
- | `isEnabled` | `true` when `mode !== 'disabled'` |
1465
- | `layers` | All z-index values |
1466
- | `useZIndex(layer)` | Get z-index value: `useZIndex('modal')` → `400` |
1467
- | `cssVars` | Tunables mapped to CSS custom properties — bound by `LayoutMain`'s `:style` |
1468
-
1469
- ---
1470
-
1471
- ## CSS custom properties
1472
-
1473
- Every derived property is registered via `@property` (browser-enforced typing — see [Known pitfalls](#known-pitfalls)):
1474
-
1475
- | Property | `syntax` | Meaning |
1476
- | ---------------------------------- | ----------- | -------------------------------------------------------------------------------- |
1477
- | `--fs` | `<length>` | Root fluid font-size clamp |
1478
- | `--unit` | `<length>` | `1rlh` — the one unit everything else derives from |
1479
- | `--edge-min` | `<length>` | Inline/block edge margin floor |
1480
- | `--measure-min` | `<length>` | Narrowest column / `auto-fill` floor |
1481
- | `--lpf` | `<integer>` | Lines-per-field (vertical rhythm chunk size) |
1482
- | `--avail-b`, `--type-b`, `--slack` | `<length>` | Per-`LayoutSection` derivation intermediates (only present with `align="split"`) |
1483
- | `--lines`, `--fields` | `<integer>` | Per-`LayoutSection` derivation intermediates (only present with `align="split"`) |
1484
-
1485
- ---
1486
-
1487
- ## Z-index system
1488
-
1489
- Unchanged from the old system — `layers` in config, `useZIndex(layer)` in code:
1490
-
1491
- ```ts
1492
- const { useZIndex } = useGridConfig()
1493
- const zModal = useZIndex('modal') // → 400
1494
- ```
1495
-
1496
- ---
1497
-
1498
- ## Config reference
1499
-
1500
- ```ts
1501
- // app.config.ts
1502
- layoutLayer: {
1503
- ui: {
1504
- grid: {
1505
- mode: 'fluid' | 'disabled',
1506
- tunables: {
1507
- measureMin: string // e.g. '22rem'
1508
- edgeMin: { min: string; max: string }
1509
- lpf: { min: number; max: number }
1510
- },
1511
- layers: {
1512
- back: number, mid: number, front: number, top: number,
1513
- header: number, dropdown: number, overlay: number, modal: number, toast: number,
1514
- },
1515
- },
1516
- },
1517
- }
1518
- ```
1519
-
1520
- ---
1521
-
1522
- ## Known pitfalls
1523
-
1524
- ### 1. `repeat(var(--n), ...)` silently invalidates
1525
-
1526
- `repeat()`'s track-count argument requires a literal `<integer>` at computed-value time. An unregistered custom property built from `calc()`/`round()` doesn't satisfy that — it substitutes as a token stream, not a resolved integer, invalidating the whole declaration. The grid falls back to implicit, content-sized tracks — cells silently squash to unreadable slivers. This is why column count comes only from native `repeat(auto-fill, minmax(...))`, never a computed integer.
1527
-
1528
- ### 2. Span utilities forcing malformed implicit tracks
1529
-
1530
- `.layout-cell--span-2` unconditionally applying `grid-column: span 2` can force CSS Grid to fabricate a content-sized implicit column when only 1 explicit column has resolved (narrow/medium viewport). Fixed two ways: `grid-auto-columns` matches the explicit track sizing (belt-and-braces), and `.layout-cell--span-2` is gated behind a `@container layout-viewport (min-width: ...)` query so it only activates once 2 columns have genuinely resolved.
1531
-
1532
- ### 3. Hand-synced container-query thresholds
1533
-
1534
- `@container` conditions can't reference custom properties. The `22rem` in `@container layout-cell (max-width: 22rem)` and the `2 * 22rem + 1rlh` in the span-2 gate both mirror `--measure-min`'s default — if you change `tunables.measureMin` in `app.config.ts`, these thresholds in `grids.css` need updating by hand.
1535
-
1536
- ````
1537
-
1538
- - [ ] **Step 2: Rewrite `layers/structure/layout/CLAUDE.md`**
1539
-
1540
- ```markdown
1541
- # Layout Layer
1542
-
1543
- Fluid, `--fs`-derived grid for Nuxt 4 applications. No breakpoints, no numbered column placement — one root unit derives column floor, edge margins, and per-section vertical rhythm.
1544
-
1545
- Full documentation: `docs/LAYOUT.md` in the monorepo root.
1546
-
1547
- ---
1548
-
1549
- ## Layer Structure
1550
-
1551
- ````
1552
-
1553
- layers/layout/
1554
- ├── app/
1555
- │ ├── assets/css/
1556
- │ │ └── layout/
1557
- │ │ └── grids.css # fluid grid engine + @property + rhythm utilities
1558
- │ ├── components/
1559
- │ │ ├── HStack.vue # HStack — flexbox row (unchanged)
1560
- │ │ ├── VStack.vue # VStack — flexbox column (unchanged)
1561
- │ │ ├── ZStack.vue # ZStack — single-area grid, layered children (unchanged)
1562
- │ │ └── Spacer.vue # Spacer — flexible remaining space (unchanged)
1563
- │ ├── components/Layout/
1564
- │ │ ├── Main.vue # LayoutMain — <main class="layout-grid"> wrapper, binds cssVars
1565
- │ │ ├── Section.vue # LayoutSection — opt-in full-width subgrid row
1566
- │ │ ├── Cell.vue # LayoutCell — variant + span-N utility classes
1567
- │ │ ├── Hero.vue # LayoutHero — deliberately outside the grid
1568
- │ │ ├── Grid/
1569
- │ │ │ └── Debug.vue # LayoutGridDebug — Cmd+G --unit band overlay
1570
- │ │ └── Page/
1571
- │ │ ├── index.vue # LayoutPage — fragment: SEO + optional header (unchanged)
1572
- │ │ └── Header.vue # LayoutPageHeader — title + description block (unchanged)
1573
- │ ├── composables/
1574
- │ │ └── useGridConfig.ts # config, isEnabled, mode, layers, useZIndex, cssVars
1575
- │ └── types/
1576
- │ └── layouts.ts # GridConfig, GridMode, GridLayers, GridTunables
1577
- │ └── app.config.ts # Config defaults + Nuxt UI component theming
1578
- ├── nuxt.config.ts # Layer meta, alias (#layers/layout), CSS import
1579
- └── CLAUDE.md # This file
1580
-
1581
- ```
1582
-
1583
- ---
1584
-
1585
- ## Core Concept
1586
-
1587
- The grid is a single `display: grid` on `<main>` (`.layout-grid`), sized only by `--measure-min`/`--unit` — no breakpoints. `LayoutSection` opts a full-width row into `subgrid`, inheriting the root's resolved `auto-fill` columns. No JS is involved in the grid mechanics themselves.
1588
-
1589
- ```
1590
-
1591
- LayoutMain (<main class="layout-grid">) ← continuous auto-fill grid
1592
- LayoutSection (<section class="layout-section">) ← opt-in, subgrid, own field rhythm
1593
- LayoutCell ← variant + span classes
1594
- LayoutHero ← deliberately outside the grid
1595
-
1596
- ````
1597
-
1598
- ---
1599
-
1600
- ## Playground Setup
1601
-
1602
- `layout` ships its own `default.vue` (`<LayoutMain><slot /></LayoutMain>`) — any consumer using the default layout gets the real grid root instead of a bare passthrough. For a page that also wants header/nav/footer chrome or the debug overlay, build a richer named layout, e.g.:
1603
-
1604
- `LayoutPage` is a fragment component — it provides no grid wrapper. You need:
1605
-
1606
- **`layouts/grid.vue`:**
1607
-
1608
- ```vue
1609
- <template>
1610
- <LayoutMain>
1611
- <slot />
1612
- <LayoutGridDebug />
1613
- </LayoutMain>
1614
- </template>
1615
- ````
1616
-
1617
- **In each page using the grid:**
1618
-
1619
- ```vue
1620
- <script setup lang="ts">
1621
- definePageMeta({ layout: 'grid' })
1622
- </script>
1623
- ```
1624
-
1625
- ---
1626
-
1627
- ## Mode System
1628
-
1629
- ```ts
1630
- // app.config.ts
1631
- layoutLayer.ui.grid.mode: 'fluid' | 'disabled'
1632
- ```
1633
-
1634
- - `'fluid'` — default, full `--fs`-derived grid
1635
- - `'disabled'` — plain `<main>`, no grid CSS
1636
-
1637
- ---
1638
-
1639
- ## Component Quick Reference
1640
-
1641
- ### `LayoutMain` — `<main class="layout-grid">`
1642
-
1643
- - **Props:** `tag` (default `'main'`)
1644
- - Binds config-driven tunables as inline CSS custom properties
1645
- - Renders plain element when `mode === 'disabled'`
1646
-
1647
- ### `AppContainer` (from `layers/core`) — width constraint
1648
-
1649
- - **Props:** `size` (`'content' | 'wide' | 'fluid' | 'full'`, default `'wide'`), `tag`
1650
- - Unchanged
1651
-
1652
- ### `LayoutPage` — fragment: SEO + optional header
1653
-
1654
- - **Props:** `title` (req), `description`, `showHeader`
1655
- - Fragment component — no wrapper element. Unchanged.
1656
-
1657
- ### `LayoutSection` — opt-in full-width subgrid row
1658
-
1659
- - **Props:** `height` (default `'100svh'`), `align: 'split' | 'center'` (default `'split'`)
1660
-
1661
- ### `LayoutCell` — positioned/styled grid child
1662
-
1663
- - **Props:** `as`, `variant: 'block' | 'plate'`, `span2`, `span2Rows`, `span3Rows`
1664
- - No numbered placement — see `docs/LAYOUT.md`'s "Known pitfalls" for why
1665
-
1666
- ### `LayoutHero` — full-viewport, outside the grid
1667
-
1668
- - **Props:** `as` (default `'section'`)
1669
-
1670
- ---
1671
-
1672
- ## useGridConfig()
1673
-
1674
- ```ts
1675
- const { config, isEnabled, mode, layers, useZIndex, cssVars } = useGridConfig()
1676
- ```
1677
-
1678
- | Return | Description |
1679
- | ------------------ | --------------------------------------------- |
1680
- | `config` | Raw `GridConfig` from app.config |
1681
- | `isEnabled` | `true` when `mode !== 'disabled'` |
1682
- | `mode` | `'fluid' \| 'disabled'` |
1683
- | `layers` | All z-index values |
1684
- | `useZIndex(layer)` | Get z-index value: `useZIndex('modal')` → 400 |
1685
- | `cssVars` | Tunables mapped to CSS custom properties |
1686
-
1687
- ---
1688
-
1689
- ## CSS Critical Notes
1690
-
1691
- 1. **Never use `@utility` in CSS files loaded via Nuxt `css: []`** — it bypasses the TW4 pipeline and the class never exists. Use plain `.class-name {}` selectors only.
1692
-
1693
- 2. **Use `overflow-x: clip`, never `overflow-x: hidden`** — `hidden` forces `overflow-y: auto` on the same element, creating an unintended scroll container and making `window.scrollY` always 0.
1694
-
1695
- 3. **Every derived custom property is registered via `@property`.** Adding a new one? Register it too — an unregistered `calc()`/`round()`-derived property silently invalidates whatever CSS it feeds (see `docs/LAYOUT.md`'s "Known pitfalls").
1696
-
1697
- ---
1698
-
1699
- ## Key Files
1700
-
1701
- | Task | File |
1702
- | ------------------------ | --------------------------------------------- |
1703
- | Grid CSS | `app/assets/css/layout/grids.css` |
1704
- | Config + Nuxt UI theming | `app/app.config.ts` |
1705
- | TypeScript types | `app/types/layouts.ts` |
1706
- | Config composable | `app/composables/useGridConfig.ts` |
1707
- | Grid wrapper component | `app/components/Layout/Main.vue` |
1708
- | Container component | `layers/core/app/components/AppContainer.vue` |
1709
- | Full reference docs | `../../docs/LAYOUT.md` |
1710
-
1711
- ````
1712
-
1713
- - [ ] **Step 3: Commit**
1714
-
1715
- ```bash
1716
- git add docs/LAYOUT.md layers/structure/layout/CLAUDE.md
1717
- git commit -m "docs(layout): rewrite LAYOUT.md and layer CLAUDE.md for the fluid grid"
1718
- ````
1719
-
1720
- ---
1721
-
1722
- ### Task 11: Migration guide
1723
-
1724
- **Files:**
1725
-
1726
- - Create: `docs/LAYOUT-MIGRATION.md`
1727
-
1728
- **Interfaces:**
1729
-
1730
- - Consumes: nothing — reference document only, for the later (out-of-scope) pass that updates `layers/content`, `layers/structure/navigation`, and `apps/starter`.
1731
-
1732
- - [ ] **Step 1: Create `docs/LAYOUT-MIGRATION.md`**
1733
-
1734
- ```markdown
1735
- # Layout Layer Migration Guide — Breakpoint Grid → Fluid Grid
1736
-
1737
- For the later pass that updates `layers/content`, `layers/structure/navigation`, and `apps/starter` to the fluid grid rebuilt in `docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md`. Not applied by that rebuild itself — those consumers still call the old API until this migration happens.
1738
-
1739
- ## Removed components
1740
-
1741
- | Old | New | Notes |
1742
- | --------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1743
- | `LayoutGridItem` | `LayoutCell` | No numbered placement. `preset` prop removed entirely. |
1744
- | `LayoutSection` (12-row, subgrid, no align control) | `LayoutSection` (same name, new props) | `fullHeight`/`fullWidth` props removed; use `height`/`align` instead. |
1745
- | `LayoutSectionHero` | `LayoutHero` | Slot-based (`#background`/`#default`/`#footer`) API dropped — `LayoutHero` is a single default slot. Rebuild background/footer composition with plain markup inside the slot, or a `HStack`/`VStack` from this same layer. |
1746
- | `LayoutSectionSplit` | _(no direct replacement)_ | Was a 9+9 two-column layout keyed to the 18-col grid. Rebuild with `HStack` (`layers/structure/layout`) or a plain CSS grid — there's no addressable "half the columns" concept left. |
1747
- | `LayoutSectionGallery` | _(no direct replacement)_ | Was an auto-placing grid with a `columns` prop. The fluid grid's own `auto-fill` already does this — drop the wrapper, use `LayoutCell` children directly inside a `LayoutMain` or a plain `<div class="layout-grid">`-styled container. |
1748
-
1749
- ## Removed props
1750
-
1751
- `LayoutGridItem`/`LayoutCell`:
1752
-
1753
- | Removed prop | Replacement |
1754
- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1755
- | `preset` | None — presets removed from config. Compose spans directly. |
1756
- | `colStart`, `colSpan`, `rowStart` | None — no addressable columns. If you need "half width," reconsider the layout as `LayoutCell span2` (claims 2 of however many columns resolved) or a plain flex/grid composition outside the fluid grid. |
1757
- | `rowSpan` | `span2Rows` / `span3Rows` boolean props (only 2/3-row spans are supported; add more span classes to `grids.css` if a wider range is needed). |
1758
- | `container` | Use `AppContainer` (from `layers/core`) directly inside the cell instead of a `container` prop on the cell itself. |
1759
- | `gap` | Not carried over — override `--unit` locally via inline style if a specific cell needs a different internal gap. |
1760
- | `density` | Not carried over — use the rhythm utility classes (`.leading-rhythm-*`, `.space-rhythm-*`, `.prose-rhythm`) directly, unchanged from before. |
1761
- | `layer` | Still supported conceptually via `useGridConfig().useZIndex(layer)` — apply the z-index via `:style="{ zIndex: useZIndex('front') }"` instead of a `layer` prop. |
1762
- | `bleed` | Not carried over — edge-to-edge bleed needs a manual `margin-inline: calc(-1 * var(--edge-min))` on the cell, mirroring the old `resolveBleedStyles` logic if needed. |
1763
-
1764
- `LayoutSection`:
1765
-
1766
- | Removed prop | Replacement |
1767
- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1768
- | `fullHeight` | `height` prop (default already `'100svh'` — pass a different value like `'auto'` for content-driven height). |
1769
- | `fullWidth` | No direct replacement — the old edge-to-edge "bleed" behavior needs manual `margin-inline: calc(-1 * var(--edge-min))`, same as `LayoutCell`'s removed `bleed` prop. |
1770
-
1771
- ## Config changes
1772
-
1773
- `app.config.ts`'s `layoutLayer.ui.grid`:
1774
-
1775
- | Old key | New key | Notes |
1776
- | ---------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1777
- | `mode: 'swiss'` | `mode: 'fluid'` | Rename only — same semantics (the enabled default). |
1778
- | `enabled: boolean` | _(removed)_ | Was a deprecated alias for `mode: 'disabled'`. Use `mode` directly. |
1779
- | `columns: ResponsiveValue<number>` | _(removed)_ | No configurable column count — `tunables.measureMin` controls the `auto-fill` floor instead. |
1780
- | `rowsPerSection: number` | _(removed)_ | No fixed row count per section — `tunables.lpf` (lines-per-field) plus the section's own derivation replace it. |
1781
- | `rhythm: string` | _(removed)_ | Was the base unit for the `.leading-rhythm-*`/`.space-rhythm-*` utilities, which are unchanged and still hardcode `0.25rem` in `grids.css` directly — not currently configurable. |
1782
- | `presets: GridPresets` | _(removed)_ | No preset system. Compose `LayoutCell`/`LayoutSection` props directly. |
1783
- | _(new)_ | `tunables: GridTunables` | `{ measureMin, edgeMin: {min,max}, lpf: {min,max} }` — see `docs/LAYOUT.md`'s config reference. |
1784
- | `layers: GridLayers` | `layers: GridLayers` | Unchanged. |
1785
-
1786
- `ui.page` slots (Nuxt UI theming, same file): `left`/`center`/`right` used to map to a numbered 18-column sidebar split (`col-start-5`, `col-start-15`). That addressing doesn't exist anymore — all three slots are now `col-span-full`. Any app relying on the sidebar split visually will need a different approach (e.g. `HStack` composing `UPageAside` + body directly, outside `UPage`'s slot system).
1787
-
1788
- ## Composable changes
1789
-
1790
- `useGridConfig()`:
1791
-
1792
- | Old | New |
1793
- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
1794
- | `getPreset(name)` | Removed — nothing to look up. |
1795
- | `mode.value === 'swiss'` | `mode.value === 'fluid'` |
1796
- | _(new)_ | `cssVars` — the tunables mapped to CSS custom properties, for anyone building a custom grid-root wrapper instead of using `LayoutMain`. |
1797
-
1798
- `useZIndex`, `layers`, `isEnabled`, `config` are unchanged.
1799
- ```
1800
-
1801
- - [ ] **Step 2: Commit**
1802
-
1803
- ```bash
1804
- git add docs/LAYOUT-MIGRATION.md
1805
- git commit -m "docs(layout): add migration guide for the fluid grid rebuild"
1806
- ```
1807
-
1808
- ---
1809
-
1810
- ### Task 12: Full verification pass
1811
-
1812
- **Files:** none (verification only)
1813
-
1814
- **Interfaces:** none
1815
-
1816
- - [ ] **Step 1: Full layer typecheck**
1817
-
1818
- Run: `pnpm --filter kmcom-layer-layout typecheck`
1819
- Expected: PASS, zero errors.
1820
-
1821
- - [ ] **Step 2: Full layer lint**
1822
-
1823
- Run: `pnpm --filter kmcom-layer-layout lint`
1824
- Expected: PASS, zero errors (stylelint on `grids.css`, eslint on the `.vue`/`.ts` files).
1825
-
1826
- - [ ] **Step 3: Playground typecheck and build**
1827
-
1828
- Run: `pnpm --filter playground typecheck && pnpm --filter playground build`
1829
- Expected: PASS. This is the only app in the monorepo whose pages consumed the deleted `LayoutGridItem`/`LayoutSectionHero`/`LayoutSectionSplit`/`LayoutSectionGallery` API (Task 9 already fixed its one usage) — a build failure here means something else in the playground still references the old API and needs fixing before this task is done.
1830
-
1831
- - [ ] **Step 4: Full test suite**
1832
-
1833
- Run: `pnpm vitest run tests/nuxt/use-grid-config.test.ts`
1834
- Expected: PASS (Task 3's 3 tests).
1835
-
1836
- - [ ] **Step 5: Manual browser verification**
1837
-
1838
- Start the playground dev server scoped to just this layer: `PLAYGROUND_LAYERS=core,layout pnpm dev`, open `/layout`, and check via real `getComputedStyle()` inspection (not just visual) at 500 / 768 / 900 / 1024 / 1440 / 1920px:
1839
-
1840
- - `.layout-grid`'s `gridTemplateColumns` resolves to explicit tracks (not `none` — a `none` value means Pitfall 1 regressed).
1841
- - Column count grows continuously with width, no jump at a fixed breakpoint.
1842
- - `LayoutCell`'s `span2` cell only spans 2 columns once the viewport container has resolved ≥2 columns (Pitfall 2's gate) — check it stays 1×1 below that width, with no visible squash or horizontal scroll on any sibling cell.
1843
- - `LayoutSection align="split"` sections are exactly `100svh` tall with visible asymmetric top/bottom padding (top thinner than bottom).
1844
- - `LayoutSection align="center"` content is visually centered both axes.
1845
- - `⌘/Ctrl+G` toggles the `--unit` band overlay on/off.
1846
- - No horizontal scrollbar at any tested width.
1847
-
1848
- - [ ] **Step 6: Commit (only if any fixes were needed in Step 1-4)**
1849
-
1850
- If Steps 1-4 required fixes, commit them now with a message describing what broke and why. If everything passed clean, there's nothing to commit for this task — the plan is complete.