kmcom-nuxt-layers 1.1.9 → 1.3.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.
- package/docs/LAYOUT.md +517 -0
- package/layers/core/app/composables/useScrollGuard.ts +2 -2
- package/layers/layout/app/assets/css/layout/grids.css +18 -18
- package/layers/layout/app/assets/css/layout/modes/fluid.css +100 -0
- package/layers/layout/app/assets/css/main.css +1 -0
- package/layers/layout/app/components/Layout/Container.vue +49 -0
- package/layers/layout/app/components/Layout/Grid/Debug.vue +18 -19
- package/layers/layout/app/components/Layout/Grid/Item.vue +43 -19
- package/layers/layout/app/components/Layout/Main.vue +36 -0
- package/layers/layout/app/components/Layout/Page/index.vue +69 -0
- package/layers/layout/app/components/Layout/Section/Grid.vue +49 -0
- package/layers/layout/app/components/Layout/Section/Sidebar.vue +72 -0
- package/layers/layout/app/components/Layout/Section/Stack.vue +63 -0
- package/layers/layout/app/composables/useGridConfig.ts +28 -1
- package/layers/layout/app/types/layouts.ts +42 -9
- package/layers/layout/app.config.ts +114 -20
- package/layers/motion/app/plugins/locomotive-scroll.client.ts +41 -21
- package/package.json +1 -1
package/docs/LAYOUT.md
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# Layout Layer
|
|
2
|
+
|
|
3
|
+
Swiss Grid System for Nuxt 4 applications. Provides a responsive 6/12/18-column CSS subgrid, page structure components, a mode system, and fluid layout utilities.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [How it works](#how-it-works)
|
|
10
|
+
- [Playground setup](#playground-setup)
|
|
11
|
+
- [Mode system](#mode-system)
|
|
12
|
+
- [Components](#components)
|
|
13
|
+
- [Composables](#composables)
|
|
14
|
+
- [CSS utilities](#css-utilities)
|
|
15
|
+
- [Z-index system](#z-index-system)
|
|
16
|
+
- [Config reference](#config-reference)
|
|
17
|
+
- [Known pitfalls](#known-pitfalls)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
The grid is a single CSS `display: grid` on `<main>` (the `.mastmain` class). Everything else — sections, items — participates via CSS `subgrid`, meaning they inherit the parent's column and row lines rather than defining their own. No JS is involved in the grid itself.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
<main class="mastmain"> ← defines the grid (6/12/18 cols, row height)
|
|
27
|
+
<section class="basesection"> ← subgrid: inherits cols + rows, spans 12 rows (= 100vh)
|
|
28
|
+
<div style="grid-column: ..."> ← positioned within the subgrid
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`LayoutMain` renders the `<main class="mastmain">`. `LayoutSection` renders `<section class="basesection">`. `LayoutGridItem` handles grid positioning via inline style or presets.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Playground setup
|
|
36
|
+
|
|
37
|
+
The layout layer requires a dedicated Nuxt layout file and page-level layout declaration. Without these, content appears edge-to-edge and the grid CSS never applies.
|
|
38
|
+
|
|
39
|
+
**1. Create `layouts/grid.vue`** in your app:
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<template>
|
|
43
|
+
<LayoutMain>
|
|
44
|
+
<slot />
|
|
45
|
+
<LayoutGridDebug />
|
|
46
|
+
</LayoutMain>
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**2. Declare the layout in each page that uses the grid:**
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
definePageMeta({ layout: 'grid' })
|
|
55
|
+
</script>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> `LayoutPage` is a fragment component — it intentionally provides no wrapper element. The `mastmain` grid lives on `LayoutMain` in the layout file, not in `LayoutPage` itself. Do not add a second `LayoutMain` inside `LayoutPage`.
|
|
59
|
+
|
|
60
|
+
**Grid debug overlay** (`Cmd+G` to toggle) is rendered by `LayoutGridDebug` inside the layout file, not per-page.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Mode system
|
|
65
|
+
|
|
66
|
+
The grid has three modes controlled via `app.config.ts`:
|
|
67
|
+
|
|
68
|
+
| Mode | Behaviour |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `'swiss'` | Default. Full Swiss grid with `mastmain`/`basesection` CSS subgrid. |
|
|
71
|
+
| `'fluid'` | Container-query-based auto-fit grid. `basesection` still gets `container-type`. |
|
|
72
|
+
| `'disabled'` | `LayoutMain` renders a plain `<main>` without grid CSS. |
|
|
73
|
+
|
|
74
|
+
Set in your `app.config.ts`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export default defineAppConfig({
|
|
78
|
+
layoutLayer: {
|
|
79
|
+
ui: {
|
|
80
|
+
grid: {
|
|
81
|
+
mode: 'swiss', // 'swiss' | 'fluid' | 'disabled'
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The old `enabled: boolean` flag is still supported (mapped to `'disabled'` / `'swiss'`) but `mode` takes precedence.
|
|
89
|
+
|
|
90
|
+
Read in code:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const { mode, isEnabled } = useGridConfig()
|
|
94
|
+
// mode.value === 'swiss' | 'fluid' | 'disabled'
|
|
95
|
+
// isEnabled.value === mode !== 'disabled'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Components
|
|
101
|
+
|
|
102
|
+
### `LayoutMain`
|
|
103
|
+
|
|
104
|
+
Renders `<main class="mastmain">`. When `mode === 'disabled'` it renders a plain `<main>` with no grid classes.
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<LayoutMain> <!-- default: <main> -->
|
|
108
|
+
<LayoutMain tag="div"> <!-- custom tag -->
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Props:**
|
|
112
|
+
|
|
113
|
+
| Prop | Type | Default |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `tag` | `string` | `'main'` |
|
|
116
|
+
|
|
117
|
+
Place this in your layout file (`layouts/grid.vue`), not in individual pages.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `LayoutContainer`
|
|
122
|
+
|
|
123
|
+
Constrains content width and centres it. Use inside sections or anywhere you need a contained block.
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<LayoutContainer> <!-- default: wide (90rem) -->
|
|
127
|
+
<LayoutContainer size="content"> <!-- 65ch prose width -->
|
|
128
|
+
<LayoutContainer size="wide"> <!-- 90rem -->
|
|
129
|
+
<LayoutContainer size="fluid"> <!-- 100% of parent -->
|
|
130
|
+
<LayoutContainer size="full"> <!-- 100vw, escapes grid padding -->
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Props:**
|
|
134
|
+
|
|
135
|
+
| Prop | Type | Default |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `size` | `'content' \| 'wide' \| 'fluid' \| 'full'` | `'wide'` |
|
|
138
|
+
| `tag` | `string` | `'div'` |
|
|
139
|
+
|
|
140
|
+
The `full` size uses negative `margin-inline` to escape the grid's `--grid-padding`, reaching the true viewport edge.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### `LayoutPage`
|
|
145
|
+
|
|
146
|
+
Fragment component (no wrapper element). Handles `useHead()` SEO and optionally renders a `LayoutPageHeader` section.
|
|
147
|
+
|
|
148
|
+
```vue
|
|
149
|
+
<template>
|
|
150
|
+
<LayoutPage title="About" description="About us">
|
|
151
|
+
<LayoutSection>
|
|
152
|
+
<LayoutGridItem preset="centered">
|
|
153
|
+
<!-- content -->
|
|
154
|
+
</LayoutGridItem>
|
|
155
|
+
</LayoutSection>
|
|
156
|
+
</LayoutPage>
|
|
157
|
+
</template>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Props:**
|
|
161
|
+
|
|
162
|
+
| Prop | Type | Default |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `title` | `string` | — (required) |
|
|
165
|
+
| `description` | `string` | `''` |
|
|
166
|
+
| `showHeader` | `boolean` | `false` |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `LayoutSection`
|
|
171
|
+
|
|
172
|
+
Full-viewport subgrid section. Spans 12 rows (= 100vh). Inherits the parent `.mastmain` grid columns via `subgrid`.
|
|
173
|
+
|
|
174
|
+
```vue
|
|
175
|
+
<LayoutSection>
|
|
176
|
+
<LayoutSection full-height> <!-- always 100vh, even on mobile -->
|
|
177
|
+
<LayoutSection tag="article">
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Props:**
|
|
181
|
+
|
|
182
|
+
| Prop | Type | Default |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `fullHeight` | `boolean` | `false` |
|
|
185
|
+
| `fullWidth` | `boolean` | `false` |
|
|
186
|
+
| `tag` | `string` | `'section'` |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### `LayoutGridItem`
|
|
191
|
+
|
|
192
|
+
Positioned child within a subgrid section. Use `preset` for common layouts or set column/row values directly.
|
|
193
|
+
|
|
194
|
+
```vue
|
|
195
|
+
<LayoutGridItem preset="centered" />
|
|
196
|
+
<LayoutGridItem col-start="3" col-span="12" row-start="2" row-span="8" />
|
|
197
|
+
<!-- Responsive values -->
|
|
198
|
+
<LayoutGridItem :col-span="{ default: 6, md: 10, lg: 12 }" />
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Props:**
|
|
202
|
+
|
|
203
|
+
| Prop | Type | Notes |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `preset` | `string` | Named preset from config |
|
|
206
|
+
| `colStart` | `number \| ResponsiveValue` | Grid column start |
|
|
207
|
+
| `colSpan` | `number \| ResponsiveValue` | Column span |
|
|
208
|
+
| `rowStart` | `number \| ResponsiveValue` | Grid row start |
|
|
209
|
+
| `rowSpan` | `number \| ResponsiveValue` | Row span |
|
|
210
|
+
| `layer` | `keyof GridLayers` | Z-index layer: `back`, `mid`, `front`, `top` |
|
|
211
|
+
| `align` | `string` | `align-self` value |
|
|
212
|
+
| `justify` | `string` | `justify-self` value |
|
|
213
|
+
| `bleed` | `boolean` | Negative margin to reach viewport edge |
|
|
214
|
+
| `as` | `string` | Tag override |
|
|
215
|
+
|
|
216
|
+
**Built-in presets:**
|
|
217
|
+
|
|
218
|
+
| Preset | Columns | Rows |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `hero` | full width | full height |
|
|
221
|
+
| `centered` | centre 10 cols | rows 2–10 |
|
|
222
|
+
| `fullWidth` | full width | auto |
|
|
223
|
+
| `sidebar` | cols 1–4 | full height |
|
|
224
|
+
| `content` | cols 5–14 | rows 2–10 |
|
|
225
|
+
| `splitLeft` | left half | full |
|
|
226
|
+
| `splitRight` | right half | full |
|
|
227
|
+
| `quarterLeft` | first quarter | full |
|
|
228
|
+
| `threeQuarterRight` | right 3/4 | full |
|
|
229
|
+
| `halfTop` | full width | top half |
|
|
230
|
+
| `halfBottom` | full width | bottom half |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### `LayoutSectionHero`
|
|
235
|
+
|
|
236
|
+
Section with `background`, `default`, and `footer` slot layers.
|
|
237
|
+
|
|
238
|
+
```vue
|
|
239
|
+
<LayoutSectionHero>
|
|
240
|
+
<template #background><img ... /></template>
|
|
241
|
+
<h1>Title</h1>
|
|
242
|
+
<template #footer><p>Subtitle</p></template>
|
|
243
|
+
</LayoutSectionHero>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### `LayoutSectionSplit`
|
|
249
|
+
|
|
250
|
+
Two-column 50/50 layout.
|
|
251
|
+
|
|
252
|
+
```vue
|
|
253
|
+
<LayoutSectionSplit>
|
|
254
|
+
<template #left>Left content</template>
|
|
255
|
+
<template #right>Right content</template>
|
|
256
|
+
</LayoutSectionSplit>
|
|
257
|
+
|
|
258
|
+
<LayoutSectionSplit reverse /> <!-- right renders first visually -->
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### `LayoutSectionGallery`
|
|
264
|
+
|
|
265
|
+
Auto-placing collection grid.
|
|
266
|
+
|
|
267
|
+
```vue
|
|
268
|
+
<LayoutSectionGallery :items="items" :columns="3" :item-row-span="4">
|
|
269
|
+
<template #item="{ item, index }">
|
|
270
|
+
<img :src="item.src" />
|
|
271
|
+
</template>
|
|
272
|
+
</LayoutSectionGallery>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Props:** `items` (required), `columns` (2/3/4/6, default 3), `itemRowSpan` (default 4)
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### `LayoutGridDebug`
|
|
280
|
+
|
|
281
|
+
Column overlay toggled with `Cmd+G`. Place once in your grid layout file, not in pages.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Composables
|
|
286
|
+
|
|
287
|
+
### `useGridConfig()`
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const { config, getPreset, isEnabled, mode, layers, useZIndex } = useGridConfig()
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
| Return | Type | Description |
|
|
294
|
+
|---|---|---|
|
|
295
|
+
| `config` | `Ref<GridConfig>` | Raw config from `app.config` |
|
|
296
|
+
| `isEnabled` | `ComputedRef<boolean>` | `true` when `mode !== 'disabled'` |
|
|
297
|
+
| `mode` | `ComputedRef<GridMode>` | `'swiss' \| 'fluid' \| 'disabled'` |
|
|
298
|
+
| `layers` | `ComputedRef<GridLayers>` | All z-index values |
|
|
299
|
+
| `getPreset(name)` | `GridPresetsItem \| undefined` | Look up a preset by name |
|
|
300
|
+
| `useZIndex(layer)` | `number` | Get a z-index value by layer name |
|
|
301
|
+
|
|
302
|
+
**`useZIndex` example:**
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
const { useZIndex } = useGridConfig()
|
|
306
|
+
const zModal = useZIndex('modal') // 400
|
|
307
|
+
const zHeader = useZIndex('header') // 100
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## CSS utilities
|
|
313
|
+
|
|
314
|
+
All grid CSS is in `layers/layout/app/assets/css/layout/`.
|
|
315
|
+
|
|
316
|
+
> **Important:** All classes are plain CSS (not `@utility` directives). Do not convert them to `@utility` — they are loaded via Nuxt's `css: []` array which bypasses the Tailwind CSS 4 pipeline, so `@utility` would produce no output.
|
|
317
|
+
|
|
318
|
+
### `grids.css` — Core grid classes
|
|
319
|
+
|
|
320
|
+
**`.mastmain`** — Root grid container on `<main>`.
|
|
321
|
+
|
|
322
|
+
```css
|
|
323
|
+
/* Responsive column counts */
|
|
324
|
+
/* Mobile: 6 columns (< 768px) */
|
|
325
|
+
/* Tablet: 12 columns (≥ 768px) */
|
|
326
|
+
/* Desktop: 18 columns (≥ 1280px) */
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
CSS custom properties exposed:
|
|
330
|
+
|
|
331
|
+
| Variable | Value | Purpose |
|
|
332
|
+
|---|---|---|
|
|
333
|
+
| `--grid-cols` | 6 / 12 / 18 | Active column count |
|
|
334
|
+
| `--grid-gap` | `clamp(0.75rem, 1.5vw, 1.5rem)` | Gap between columns and rows |
|
|
335
|
+
| `--grid-padding` | `clamp(1rem, 2.5vw, 2rem)` | Outer left/right gutters |
|
|
336
|
+
| `--section-height` | `100vh` | Height of one section |
|
|
337
|
+
| `--grid-row-height` | computed | Height of one grid row |
|
|
338
|
+
|
|
339
|
+
**`.basesection`** — Subgrid section (12 rows = 100vh).
|
|
340
|
+
|
|
341
|
+
**Vertical rhythm utilities** (`.leading-rhythm-*`, `.space-rhythm-*`, `.prose-rhythm`)
|
|
342
|
+
|
|
343
|
+
### `modes/fluid.css` — Fluid grid classes
|
|
344
|
+
|
|
345
|
+
Container-query-based auto-fit grids for components that should respond to their container width rather than the viewport.
|
|
346
|
+
|
|
347
|
+
```vue
|
|
348
|
+
<div class="fluid-grid"> <!-- auto-fit, min col 16rem -->
|
|
349
|
+
<div class="fluid-grid-2"> <!-- 2-column at container ≥ 30rem -->
|
|
350
|
+
<div class="fluid-grid-3"> <!-- 3-column at container ≥ 44rem -->
|
|
351
|
+
<div class="fluid-grid-4"> <!-- 4-column at container ≥ 52rem -->
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Tune minimum column width per-instance:
|
|
355
|
+
|
|
356
|
+
```vue
|
|
357
|
+
<div class="fluid-grid" style="--fluid-col-min: 20rem">
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Container size classes** (used by `LayoutContainer`):
|
|
361
|
+
|
|
362
|
+
| Class | Effect |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `.layout-container-content` | `max-width: 65ch`, centred |
|
|
365
|
+
| `.layout-container-wide` | `max-width: 90rem`, centred |
|
|
366
|
+
| `.layout-container-fluid` | `max-width: 100%` |
|
|
367
|
+
| `.layout-container-full` | `width: 100vw`, escapes grid padding |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Z-index system
|
|
372
|
+
|
|
373
|
+
The grid manages z-index through named layers. Use `useZIndex()` or the `layer` prop on `LayoutGridItem` — never raw numbers.
|
|
374
|
+
|
|
375
|
+
| Layer | Value | Use |
|
|
376
|
+
|---|---|---|
|
|
377
|
+
| `back` | 0 | Background elements |
|
|
378
|
+
| `mid` | 10 | Content |
|
|
379
|
+
| `front` | 20 | Floating content |
|
|
380
|
+
| `top` | 30 | Pinned / sticky |
|
|
381
|
+
| `header` | 100 | Site header / nav |
|
|
382
|
+
| `dropdown` | 200 | Dropdown menus, popups |
|
|
383
|
+
| `overlay` | 300 | Drawers, sidebars |
|
|
384
|
+
| `modal` | 400 | Modal dialogs |
|
|
385
|
+
| `toast` | 500 | Toasts, notifications |
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
// In a component
|
|
389
|
+
const { useZIndex } = useGridConfig()
|
|
390
|
+
</script>
|
|
391
|
+
<template>
|
|
392
|
+
<div :style="{ zIndex: useZIndex('modal') }">
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```vue
|
|
396
|
+
<!-- On a grid item -->
|
|
397
|
+
<LayoutGridItem layer="front">
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Override values in `app.config.ts`:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
layoutLayer: {
|
|
404
|
+
ui: {
|
|
405
|
+
grid: {
|
|
406
|
+
layers: {
|
|
407
|
+
toast: 9999, // override individual values
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Config reference
|
|
417
|
+
|
|
418
|
+
Full type from `layers/layout/app/types/layouts.ts`:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
interface GridConfig {
|
|
422
|
+
mode?: 'swiss' | 'fluid' | 'disabled'
|
|
423
|
+
/** @deprecated use mode: 'disabled' */
|
|
424
|
+
enabled?: boolean
|
|
425
|
+
layers?: Partial<GridLayers>
|
|
426
|
+
presets?: Record<string, GridPresetsItem>
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
interface GridPresetsItem {
|
|
430
|
+
colStart?: number | ResponsiveValue
|
|
431
|
+
colSpan?: number | ResponsiveValue
|
|
432
|
+
rowStart?: number | ResponsiveValue
|
|
433
|
+
rowSpan?: number | ResponsiveValue
|
|
434
|
+
container?: 'content' | 'wide' | 'fluid' | 'full'
|
|
435
|
+
gap?: string
|
|
436
|
+
density?: 'compact' | 'normal' | 'spacious'
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
App config namespace: `layoutLayer.ui.grid`.
|
|
441
|
+
|
|
442
|
+
**Custom preset example:**
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
export default defineAppConfig({
|
|
446
|
+
layoutLayer: {
|
|
447
|
+
ui: {
|
|
448
|
+
grid: {
|
|
449
|
+
presets: {
|
|
450
|
+
spotlight: {
|
|
451
|
+
colStart: 4,
|
|
452
|
+
colSpan: 12,
|
|
453
|
+
rowStart: 3,
|
|
454
|
+
rowSpan: 6,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Known pitfalls
|
|
466
|
+
|
|
467
|
+
### `@utility` doesn't work in layer CSS files
|
|
468
|
+
|
|
469
|
+
`@utility` is a Tailwind CSS 4 directive that only works inside files processed by TW4's own pipeline (files containing `@import "tailwindcss"`). CSS loaded via Nuxt's `css: []` array bypasses this pipeline — `@utility` blocks are silently ignored and the class is never generated.
|
|
470
|
+
|
|
471
|
+
**Rule: always use plain CSS class selectors in layer CSS files.**
|
|
472
|
+
|
|
473
|
+
```css
|
|
474
|
+
/* Wrong — class will not exist in output */
|
|
475
|
+
@utility mastmain {
|
|
476
|
+
display: grid;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* Correct */
|
|
480
|
+
.mastmain {
|
|
481
|
+
display: grid;
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### `overflow-x: hidden` creates unintended scroll containers
|
|
486
|
+
|
|
487
|
+
The CSS overflow interaction rule: setting `overflow-x: hidden` on any element forces `overflow-y` from `visible` to `auto`, turning that element into a scroll container. When the element is `html` or `body`, `window.scrollY` stays 0 even when content overflows the viewport.
|
|
488
|
+
|
|
489
|
+
**Use `overflow-x: clip` instead.** `clip` cuts off overflow without creating a scroll container and is not subject to the forced `overflow-y` conversion. This is why `.mastmain`, `html`, and `body` all use `overflow-x: clip` in this layer.
|
|
490
|
+
|
|
491
|
+
```css
|
|
492
|
+
/* Wrong */
|
|
493
|
+
.mastmain { overflow-x: hidden; } /* becomes a scroll container */
|
|
494
|
+
|
|
495
|
+
/* Correct */
|
|
496
|
+
.mastmain { overflow-x: clip; } /* clips without scroll container */
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Locomotive Scroll intercepts wheel events globally
|
|
500
|
+
|
|
501
|
+
Locomotive Scroll v5 (Lenis-based) with `smoothWheel: true` calls `preventDefault()` on every `wheel` event. If initialised globally in a Nuxt plugin, it intercepts scrolling on every page — even pages where the window has no scroll height, silently swallowing events.
|
|
502
|
+
|
|
503
|
+
**Scope Locomotive Scroll to the specific route** using `addRouteMiddleware`:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
// plugins/locomotive-scroll.client.ts
|
|
507
|
+
addRouteMiddleware((to, from) => {
|
|
508
|
+
if (to.path === '/the-route') nextTick(init)
|
|
509
|
+
else if (from?.path === '/the-route') destroy()
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### `LayoutPage` is a fragment — it provides no grid wrapper
|
|
514
|
+
|
|
515
|
+
`LayoutPage` renders no element of its own. The `mastmain` grid lives on `LayoutMain` inside a layout file. If you use `<LayoutPage>` on a page that uses the `default` layout (which doesn't include `LayoutMain`), the grid CSS never applies.
|
|
516
|
+
|
|
517
|
+
Always pair `LayoutPage` usage with `definePageMeta({ layout: 'grid' })` and ensure `layouts/grid.vue` exists with `<LayoutMain>`.
|
|
@@ -96,8 +96,8 @@ function guard() {
|
|
|
96
96
|
const html = document.documentElement
|
|
97
97
|
const body = document.body
|
|
98
98
|
|
|
99
|
-
html.style.overflowX = '
|
|
100
|
-
body.style.overflowX = '
|
|
99
|
+
html.style.overflowX = 'clip'
|
|
100
|
+
body.style.overflowX = 'clip'
|
|
101
101
|
body.style.maxWidth = '100vw'
|
|
102
102
|
|
|
103
103
|
if (!opts.strict) return
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* Uses CSS subgrid for precise alignment across layout hierarchy.
|
|
6
6
|
*
|
|
7
7
|
* Hierarchy:
|
|
8
|
-
* MastMain (mastmain
|
|
9
|
-
* └── BaseSection (basesection
|
|
8
|
+
* MastMain (.mastmain)
|
|
9
|
+
* └── BaseSection (.basesection, uses subgrid)
|
|
10
10
|
* └── BaseGridItem (positioned via inline grid-column/grid-row)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* mastmain - Root grid container with responsive density
|
|
14
|
+
* .mastmain - Root grid container with responsive density
|
|
15
15
|
*
|
|
16
16
|
* Creates responsive grid:
|
|
17
17
|
* - Mobile: 6 columns (default)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Each row height is calculated to fit 12 rows per viewport.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
.mastmain {
|
|
24
24
|
--grid-cols: 6;
|
|
25
25
|
--grid-rows-per-section: 12;
|
|
26
26
|
--grid-gap: clamp(0.75rem, 1.5vw, 1.5rem);
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
grid-auto-rows: var(--grid-row-height);
|
|
37
37
|
gap: var(--grid-gap);
|
|
38
38
|
padding-inline: var(--grid-padding); /* Outer gutters on left/right */
|
|
39
|
-
overflow-x: hidden
|
|
39
|
+
overflow-x: clip; /* clip without creating a scroll container (overflow-x:hidden would force overflow-y:auto) */
|
|
40
40
|
width: 100%;
|
|
41
41
|
max-width: 100%;
|
|
42
42
|
box-sizing: border-box;
|
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* basesection - Full-viewport section using subgrid
|
|
61
|
+
* .basesection - Full-viewport section using subgrid
|
|
62
62
|
*
|
|
63
63
|
* Inherits parent's grid lines via subgrid.
|
|
64
64
|
* Spans 12 rows (1 viewport height).
|
|
65
65
|
*/
|
|
66
|
-
|
|
66
|
+
.basesection {
|
|
67
67
|
container-type: inline-size;
|
|
68
68
|
container-name: section;
|
|
69
69
|
grid-column: 1 / -1;
|
|
@@ -100,59 +100,59 @@
|
|
|
100
100
|
*/
|
|
101
101
|
|
|
102
102
|
/* Line height utilities */
|
|
103
|
-
|
|
103
|
+
.leading-rhythm-4 {
|
|
104
104
|
--rhythm: 0.25rem;
|
|
105
105
|
line-height: calc(var(--rhythm) * 4); /* 1rem */
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
.leading-rhythm-5 {
|
|
109
109
|
--rhythm: 0.25rem;
|
|
110
110
|
line-height: calc(var(--rhythm) * 5); /* 1.25rem */
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
.leading-rhythm-6 {
|
|
114
114
|
--rhythm: 0.25rem;
|
|
115
115
|
line-height: calc(var(--rhythm) * 6); /* 1.5rem */
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
.leading-rhythm-7 {
|
|
119
119
|
--rhythm: 0.25rem;
|
|
120
120
|
line-height: calc(var(--rhythm) * 7); /* 1.75rem */
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
.leading-rhythm-8 {
|
|
124
124
|
--rhythm: 0.25rem;
|
|
125
125
|
line-height: calc(var(--rhythm) * 8); /* 2rem */
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/* Vertical spacing utilities */
|
|
129
|
-
|
|
129
|
+
.space-rhythm-1 {
|
|
130
130
|
--rhythm: 0.25rem;
|
|
131
131
|
margin-block: var(--rhythm); /* 0.25rem */
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
.space-rhythm-2 {
|
|
135
135
|
--rhythm: 0.25rem;
|
|
136
136
|
margin-block: calc(var(--rhythm) * 2); /* 0.5rem */
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
.space-rhythm-4 {
|
|
140
140
|
--rhythm: 0.25rem;
|
|
141
141
|
margin-block: calc(var(--rhythm) * 4); /* 1rem */
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
.space-rhythm-6 {
|
|
145
145
|
--rhythm: 0.25rem;
|
|
146
146
|
margin-block: calc(var(--rhythm) * 6); /* 1.5rem */
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
.space-rhythm-8 {
|
|
150
150
|
--rhythm: 0.25rem;
|
|
151
151
|
margin-block: calc(var(--rhythm) * 8); /* 2rem */
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/* Prose container - auto-applies rhythm to children */
|
|
155
|
-
|
|
155
|
+
.prose-rhythm {
|
|
156
156
|
--rhythm: 0.25rem;
|
|
157
157
|
|
|
158
158
|
& > * + * {
|