@typed-grid-areas/solid 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -1344
- package/REFERENCE.md +1406 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,1029 +1,66 @@
|
|
|
1
1
|
# typed-grid-areas
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
[](https://michaelbuss.github.io/typed-grid-areas/)
|
|
4
|
+
[](https://michaelbuss.github.io/typed-grid-areas/try-it/)
|
|
5
|
+
[](https://www.npmjs.com/package/typed-grid-areas)
|
|
6
|
+
[](https://bundlejs.com/?q=typed-grid-areas)
|
|
7
|
+
[](https://github.com/MichaelBuss/typed-grid-areas/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
is a package of its own. The React adapter, `@typed-grid-areas/react`,
|
|
11
|
-
renders a definition as typed slot components (`<Card>`, `<Card.icon>`).
|
|
9
|
+
**Describe a grid once. Every combination of slots lays itself out.**
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
[Try it in your browser](https://michaelbuss.github.io/typed-grid-areas/try-it/)
|
|
11
|
+
**Zero-dependency core** · **a few kB gzipped** · **built on web standards**: CSS grid, `:has()`, container queries and cascade layers
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```sh
|
|
19
|
-
npm i typed-grid-areas @typed-grid-areas/react
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`@typed-grid-areas/react` needs `react` 19 or later. The core on its own
|
|
23
|
-
(`npm i typed-grid-areas`) has no framework dependency.
|
|
24
|
-
|
|
25
|
-
Moving from `typed-grid-areas/react` (0.1)? Install `@typed-grid-areas/react`
|
|
26
|
-
and import `createGridComponent` from it instead; nothing else changes.
|
|
27
|
-
|
|
28
|
-
### Agent skill
|
|
29
|
-
|
|
30
|
-
The `typed-grid-areas` package ships an agent skill (in its
|
|
31
|
-
`skills/typed-grid-areas/`) that walks a coding agent through writing,
|
|
32
|
-
converting and debugging layouts, versioned with each release. Enable it in your project with
|
|
33
|
-
[TanStack Intent](https://tanstack.com/intent):
|
|
34
|
-
|
|
35
|
-
```sh
|
|
36
|
-
npx @tanstack/intent@latest install
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
The first run asks which packages' skills to enable, so it needs an
|
|
40
|
-
interactive terminal, and Intent needs Node.js 20.12 or newer.
|
|
41
|
-
|
|
42
|
-
## At a glance
|
|
43
|
-
|
|
44
|
-
```tsx
|
|
45
|
-
import { defineLayout } from "typed-grid-areas";
|
|
46
|
-
import { createGridComponent } from "@typed-grid-areas/react";
|
|
47
|
-
|
|
48
|
-
const Card = createGridComponent(
|
|
49
|
-
defineLayout({
|
|
50
|
-
areas: ["icon?", "information", "cardTag?"],
|
|
51
|
-
layouts: [
|
|
52
|
-
{
|
|
53
|
-
template: "icon information cardTag",
|
|
54
|
-
columns: "min-content minmax(0, 1fr) auto",
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
}),
|
|
58
|
-
);
|
|
13
|
+

|
|
59
14
|
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<Card className="gap-2">
|
|
63
|
-
<Card.icon>★</Card.icon>
|
|
64
|
-
<Card.information>Weekly digest</Card.information>
|
|
65
|
-
{tag && <Card.cardTag>{tag}</Card.cardTag>}
|
|
66
|
-
</Card>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
```
|
|
15
|
+
## Grid template areas, in one picture
|
|
70
16
|
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
17
|
+
CSS can draw a layout as text. With
|
|
18
|
+
[`grid-template-areas`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-areas)
|
|
19
|
+
you name a grid's regions and write the names down the way the regions sit
|
|
20
|
+
on screen; each child then takes its region by name. The template is a
|
|
21
|
+
picture of the component:
|
|
74
22
|
|
|
75
|
-
|
|
23
|
+

|
|
76
24
|
|
|
77
|
-
|
|
78
|
-
text templates. Everything that names an area is checked against `areas` at
|
|
79
|
-
the type level.
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
import { defineLayout } from "typed-grid-areas";
|
|
83
|
-
|
|
84
|
-
const cardGrid = defineLayout({
|
|
85
|
-
areas: ["icon?", "information", "cardTag?"],
|
|
86
|
-
layouts: [
|
|
87
|
-
{
|
|
88
|
-
template: `
|
|
89
|
-
icon cardTag
|
|
90
|
-
information information`,
|
|
91
|
-
columns: "min-content minmax(0, 1fr)",
|
|
92
|
-
align: { cardTag: { justify: "end" } },
|
|
93
|
-
breakpoints: {
|
|
94
|
-
"@sm": {
|
|
95
|
-
template: "icon information cardTag",
|
|
96
|
-
columns: "min-content minmax(0, 1fr) auto",
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
{ unless: ["cardTag"], template: "icon information" },
|
|
101
|
-
],
|
|
102
|
-
});
|
|
103
|
-
```
|
|
25
|
+
## Simple on paper, hard in practice
|
|
104
26
|
|
|
105
|
-
- **
|
|
106
|
-
the
|
|
107
|
-
- **
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`"min-content minmax(0, 1fr)"` is two tracks.
|
|
112
|
-
|
|
113
|
-
Compile-time checks, each reported on the offending property:
|
|
114
|
-
|
|
115
|
-
- an unknown area in a cell (also inside `a+b`): `{ _unknownArea: "iconn" }`
|
|
116
|
-
on `template`
|
|
117
|
-
- rows with different cell counts:
|
|
118
|
-
`{ _raggedRow: { row: 2; cells: 1; expected: 2 } }` on `template`
|
|
119
|
-
- a track count that doesn't match the template:
|
|
120
|
-
`{ _columnCountError: { expected: 2; got: 3 } }` on `columns` (and
|
|
121
|
-
`_rowCountError` on `rows`); skipped for `repeat(` and non-literal strings
|
|
122
|
-
- unknown `align` keys and unknown `when`/`unless` values
|
|
123
|
-
|
|
124
|
-
There is no autocomplete inside a template string, and each template reports
|
|
125
|
-
one problem at a time.
|
|
126
|
-
|
|
127
|
-
In development, `defineLayout` also runs `validateGridConfig()` once per
|
|
128
|
-
definition and logs what types can't check: areas and layers that aren't
|
|
129
|
-
rectangles, and malformed cells. `validateGridConfig()` returns those
|
|
130
|
-
warnings as strings, so it is also useful for definitions assembled
|
|
131
|
-
dynamically.
|
|
132
|
-
|
|
133
|
-
## Optional areas and derived layouts
|
|
134
|
-
|
|
135
|
-
Mark an area optional with a `?` suffix in `areas`; everywhere else it is
|
|
136
|
-
named without the `?`. Every other area is required: its slot is always
|
|
137
|
-
expected to be rendered. Draw each layout with every area present, and the
|
|
138
|
-
library derives a layout for every combination of absent optional areas that
|
|
139
|
-
the layout uses and its condition doesn't mention:
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
defineLayout({
|
|
143
|
-
areas: ["icon?", "information", "cardTag?", "children?"],
|
|
144
|
-
layouts: [
|
|
145
|
-
{
|
|
146
|
-
template: `
|
|
147
|
-
icon cardTag
|
|
148
|
-
information information
|
|
149
|
-
children children`,
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
});
|
|
153
|
-
// without children: icon cardTag / information information
|
|
154
|
-
// without cardTag: icon / information / children
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
A derived layout is the authored one (and each of its breakpoint
|
|
158
|
-
arrangements) with the absent areas taken out:
|
|
159
|
-
|
|
160
|
-
1. A row or column is dropped, with its track size, when it held an absent
|
|
161
|
-
area and everything else in it is also absent or continues into a
|
|
162
|
-
neighbouring track (a spanning area just gets one track shorter).
|
|
163
|
-
2. A hole that can't be dropped is filled by a neighbouring area (left, then
|
|
164
|
-
right, up, down) if that area stays a rectangle; otherwise it stays empty.
|
|
165
|
-
3. Cells written as `"."` are never dropped or filled.
|
|
166
|
-
|
|
167
|
-
The generated CSS grows with the number of combinations (2ⁿ for n optional
|
|
168
|
-
areas in one grid), so it is deduplicated, and nesting grids keeps n small
|
|
169
|
-
per grid: the two-grid PreferenceCard below stays under 10 KB.
|
|
170
|
-
|
|
171
|
-
## Overrides: `when` and `unless`
|
|
172
|
-
|
|
173
|
-
Where a derived layout isn't what you want, write an **override**: a layout
|
|
174
|
-
with a **condition**, declared after the layout it replaces. A condition is
|
|
175
|
-
`when` (areas that must be present) plus `unless` (areas that must be
|
|
176
|
-
absent); areas named in neither don't affect it. A layout without a
|
|
177
|
-
condition is a **base** layout, which always applies.
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
defineLayout({
|
|
181
|
-
areas: ["icon?", "information", "cardTag?", "children?"],
|
|
182
|
-
layouts: [
|
|
183
|
-
{
|
|
184
|
-
template: `
|
|
185
|
-
icon cardTag
|
|
186
|
-
information information
|
|
187
|
-
children children`,
|
|
188
|
-
columns: "min-content minmax(0, 1fr)",
|
|
189
|
-
},
|
|
190
|
-
// The derived layout without a tag would stack icon and information in
|
|
191
|
-
// the min-content column; put them side by side instead.
|
|
192
|
-
{ unless: ["cardTag"], template: "icon information / children children" },
|
|
193
|
-
],
|
|
194
|
-
});
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Conditions are checked with `:has()` / `:not(:has())` on the grid's direct
|
|
198
|
-
children, so layouts react to which slots are actually rendered, without any
|
|
199
|
-
layout JavaScript. When several layouts match, the one declared **last**
|
|
200
|
-
wins, however many areas each one mentions. An override gets derived layouts
|
|
201
|
-
of its own for the optional areas its condition doesn't mention.
|
|
202
|
-
|
|
203
|
-
Every layout is self-contained: while it applies it sets the tracks, the
|
|
204
|
-
placement and the alignment (`justify` / `align`: `start`, `center`, `end`,
|
|
205
|
-
`stretch`, `baseline`) of every area, so nothing leaks in from another
|
|
206
|
-
matching layout:
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
defineLayout({
|
|
210
|
-
areas: ["icon", "info", "tag"],
|
|
211
|
-
layouts: [
|
|
212
|
-
{ template: "info tag", align: { tag: { justify: "end" } } },
|
|
213
|
-
{
|
|
214
|
-
when: ["icon"],
|
|
215
|
-
unless: ["tag"],
|
|
216
|
-
template: "icon info",
|
|
217
|
-
align: { icon: { justify: "center", align: "start" } },
|
|
218
|
-
},
|
|
219
|
-
],
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
An area a layout's template leaves out is **hidden** (`display: none
|
|
224
|
-
!important`) while that layout applies.
|
|
225
|
-
|
|
226
|
-
All generated CSS sits in the `typed-grid-areas` cascade layer, so your own
|
|
227
|
-
unlayered classes on the grid or a slot (say `justify-self`) always win. The
|
|
228
|
-
one exception is hiding: it is `!important`, so a `display` class on a slot
|
|
229
|
-
can't bring back an area the current layout leaves out
|
|
230
|
-
([ADR 0003](https://github.com/MichaelBuss/typed-grid-areas/blob/main/docs/adr/0003-hidden-areas-are-important.md)).
|
|
231
|
-
|
|
232
|
-
## Breakpoints
|
|
233
|
-
|
|
234
|
-
`breakpoints` give a layout different arrangements once the grid is given
|
|
235
|
-
at least a certain width. Keys are Tailwind's default `@container` sizes
|
|
236
|
-
(`@xs` through `@7xl`) or an arbitrary length in Tailwind's arbitrary-value
|
|
237
|
-
syntax (`"@[40rem]"`):
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
defineLayout({
|
|
241
|
-
areas: ["icon", "info"],
|
|
242
|
-
layouts: [
|
|
243
|
-
{
|
|
244
|
-
template: "info",
|
|
245
|
-
breakpoints: {
|
|
246
|
-
"@sm": { template: "icon info" },
|
|
247
|
-
"@[40rem]": { template: "info icon", columns: "1fr auto" },
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
});
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
A breakpoint arrangement beats its own layout's base arrangement, but a
|
|
255
|
-
later layout still beats an earlier layout's breakpoints. Breakpoint
|
|
256
|
-
arrangements take their own `rows`, `columns` and `align`; nothing is
|
|
257
|
-
inherited from the layout. Derived layouts apply to each breakpoint
|
|
258
|
-
arrangement too.
|
|
259
|
-
|
|
260
|
-
**A breakpoint measures the space the grid itself is given**, never the
|
|
261
|
-
viewport or a container somewhere above it, so a grid behaves the same
|
|
262
|
-
wherever it's placed. A container query can't measure the element it
|
|
263
|
-
styles, so a grid with breakpoints renders inside its own size container: a
|
|
264
|
-
wrapper `div` with `container-type: inline-size` and a container name unique
|
|
265
|
-
to the definition, which its `@container` queries target by name (see
|
|
266
|
-
[ADR 0002](https://github.com/MichaelBuss/typed-grid-areas/blob/main/docs/adr/0002-grid-owns-its-size-container.md)). Grids without
|
|
267
|
-
breakpoints get no wrapper and are not size containers.
|
|
268
|
-
|
|
269
|
-
Inline-size containment has a cost: **a grid with breakpoints can't size to
|
|
270
|
-
its content.** That's fine in a `1fr`, `minmax(0, 1fr)` or stretched track,
|
|
271
|
-
but in an `auto` or `min-content` track of a parent grid it collapses to zero
|
|
272
|
-
width. A nested grid with breakpoints also switches at a wider outer width
|
|
273
|
-
than a flat one would, since it measures only its own area.
|
|
274
|
-
|
|
275
|
-
## Layers: `base+layer`
|
|
276
|
-
|
|
277
|
-
A cell written `base+layer` (or `base+layer+layer…`) is owned by `base` as
|
|
278
|
-
usual, and also has `layer` drawn on top of it. A layer covers the bounding
|
|
279
|
-
rectangle of the cells it appears in and is drawn above its base, whatever
|
|
280
|
-
their order in the DOM. Several layers can share a host, each in its own
|
|
281
|
-
cells, and an area can be a layer in one layout and own cells in another:
|
|
282
|
-
|
|
283
|
-
```ts
|
|
284
|
-
defineLayout({
|
|
285
|
-
areas: ["photo", "identity", "details", "logo?", "qr?", "badge?"],
|
|
286
|
-
layouts: [
|
|
287
|
-
{
|
|
288
|
-
template: `
|
|
289
|
-
photo+logo photo photo+badge
|
|
290
|
-
photo photo photo
|
|
291
|
-
photo+identity photo+identity photo+identity
|
|
292
|
-
details details qr`,
|
|
293
|
-
rows: "40px 80px 40px 60px",
|
|
294
|
-
columns: "60px 1fr 60px",
|
|
295
|
-
breakpoints: {
|
|
296
|
-
"@md": {
|
|
297
|
-
template: `
|
|
298
|
-
photo photo+badge identity logo
|
|
299
|
-
photo photo details qr`,
|
|
300
|
-
rows: "40px 120px",
|
|
301
|
-
columns: "100px 40px 1fr 80px",
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
],
|
|
306
|
-
});
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
On narrow cards `logo`, `badge` and `identity` sit in the photo's corners;
|
|
310
|
-
from `@md` the photo is a column of its own and only `badge` stays on it.
|
|
311
|
-
Use `align` to pin a layer within its cells.
|
|
312
|
-
|
|
313
|
-
In derived layouts, a missing optional layer just disappears from its cells:
|
|
314
|
-
it never leaves a hole for a neighbour to fill. A row or column is only
|
|
315
|
-
dropped if every layer in it also continues into a neighbouring track.
|
|
316
|
-
|
|
317
|
-
**Track sizing.** Layers sit in grid tracks, so their content takes part in
|
|
318
|
-
sizing those tracks just like the base's does. Give the host area's tracks
|
|
319
|
-
fixed or `minmax()` sizes, or a large layer can stretch the area it sits on.
|
|
320
|
-
|
|
321
|
-
### Restyling layered content: `--tga-over`
|
|
322
|
-
|
|
323
|
-
Every layered slot gets the custom property `--tga-over`, set to the name of
|
|
324
|
-
the area underneath it (the one owning its top-left cell); every other slot
|
|
325
|
-
gets `none`. Content inside a slot can restyle itself with a container style
|
|
326
|
-
query, for example white text while it sits over a photo:
|
|
27
|
+
- **The names are unchecked strings.** A typo, in the template or on a
|
|
28
|
+
child, quietly puts the child somewhere else. Nothing reports it.
|
|
29
|
+
- **One picture fits one set of children.** Leave out the tag and its cells
|
|
30
|
+
stay behind as an empty column. Every combination of optional slots needs
|
|
31
|
+
its own picture behind a `:has()` rule, and every breakpoint multiplies
|
|
32
|
+
them. For a card with an optional icon and tag:
|
|
327
33
|
|
|
328
34
|
```css
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
35
|
+
.card {
|
|
36
|
+
display: grid;
|
|
37
|
+
grid-template-areas: "icon information cardTag";
|
|
38
|
+
grid-template-columns: min-content minmax(0, 1fr) auto;
|
|
333
39
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
ancestor's value: put the rule on the slot's content, not the slot itself.
|
|
338
|
-
|
|
339
|
-
Browser support for style queries on custom properties
|
|
340
|
-
([MDN browser-compat-data](https://github.com/mdn/browser-compat-data/blob/main/css/at-rules/container.json)
|
|
341
|
-
`css.at-rules.container.style_queries_for_custom_properties`, and
|
|
342
|
-
[caniuse](https://caniuse.com/css-container-queries-style), checked
|
|
343
|
-
2026-10-31):
|
|
344
|
-
|
|
345
|
-
| Browser | Supported since |
|
|
346
|
-
| ------- | --------------- |
|
|
347
|
-
| Chrome | 111 |
|
|
348
|
-
| Safari | 18 |
|
|
349
|
-
| Firefox | 151 |
|
|
350
|
-
|
|
351
|
-
All on by default. Only custom-property queries are supported (which is all
|
|
352
|
-
`--tga-over` needs); the range syntax (`style(--x > 1)`) isn't in Safari.
|
|
353
|
-
The library's own tests only run in Chromium. In older browsers the query
|
|
354
|
-
simply never matches, so write the unlayered look as the default and the
|
|
355
|
-
layered look inside the query.
|
|
356
|
-
|
|
357
|
-
## Rendering with React: grid components
|
|
358
|
-
|
|
359
|
-
`createGridComponent(definition)` turns a definition into the component it
|
|
360
|
-
renders as. Create it once, at module level:
|
|
361
|
-
|
|
362
|
-
```tsx
|
|
363
|
-
import { defineLayout } from "typed-grid-areas";
|
|
364
|
-
import { createGridComponent } from "@typed-grid-areas/react";
|
|
365
|
-
|
|
366
|
-
const Card = createGridComponent(
|
|
367
|
-
defineLayout({
|
|
368
|
-
areas: ["icon", "info"],
|
|
369
|
-
layouts: [{ template: "icon info", columns: "48px 1fr" }],
|
|
370
|
-
}),
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
function Example() {
|
|
374
|
-
return (
|
|
375
|
-
<Card className="gap-4" aria-label="Card">
|
|
376
|
-
<Card.icon>★</Card.icon>
|
|
377
|
-
<Card.info className="truncate">Information</Card.info>
|
|
378
|
-
</Card>
|
|
379
|
-
);
|
|
40
|
+
.card:not(:has(> .icon)) {
|
|
41
|
+
grid-template-areas: "information cardTag";
|
|
42
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
380
43
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
-
|
|
384
|
-
`data-grid-id`, which the generated CSS gives `display: grid` (layered, so
|
|
385
|
-
your own class can change it), taking `className`, `style`, `ref`,
|
|
386
|
-
`children` and any other `div` props. When the definition has breakpoints
|
|
387
|
-
the grid sits inside its own size container (a wrapper `div`); your props
|
|
388
|
-
still go on the grid itself.
|
|
389
|
-
- **`<Card.icon>`** is the slot for the `icon` area: a static component per
|
|
390
|
-
area, named exactly like the area, so `<Card.iconn>` is a type error. It
|
|
391
|
-
takes the same props and renders a `div` marked `data-area="icon"`.
|
|
392
|
-
Placement lives in the generated CSS, not on the slot.
|
|
393
|
-
- **`asChild`** (on slots and on the grid) renders your own single child
|
|
394
|
-
element instead of a `div`, with the props merged onto it (Radix `Slot`),
|
|
395
|
-
so it needs no extra wrapper:
|
|
396
|
-
|
|
397
|
-
```tsx
|
|
398
|
-
import { defineLayout } from "typed-grid-areas";
|
|
399
|
-
import { createGridComponent } from "@typed-grid-areas/react";
|
|
400
|
-
|
|
401
|
-
const Row = createGridComponent(
|
|
402
|
-
defineLayout({ areas: ["input", "content"], layouts: [{ template: "input content" }] }),
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
function Example() {
|
|
406
|
-
return (
|
|
407
|
-
<Row>
|
|
408
|
-
<Row.input asChild>
|
|
409
|
-
<label htmlFor="notify">
|
|
410
|
-
<input id="notify" type="checkbox" />
|
|
411
|
-
</label>
|
|
412
|
-
</Row.input>
|
|
413
|
-
<Row.content>Notify me</Row.content>
|
|
414
|
-
</Row>
|
|
415
|
-
);
|
|
44
|
+
.card:not(:has(> .cardTag)) {
|
|
45
|
+
grid-template-areas: "icon information";
|
|
46
|
+
grid-template-columns: min-content minmax(0, 1fr);
|
|
416
47
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
are fine). Render an area's slot even while the current layout hides it;
|
|
421
|
-
leave it out only to make an optional area absent.
|
|
422
|
-
|
|
423
|
-
Grid components work with React Compiler and in Strict Mode, and the
|
|
424
|
-
adapter's tests run under both.
|
|
425
|
-
|
|
426
|
-
### Nesting: `nest`
|
|
427
|
-
|
|
428
|
-
A grid can fill another grid's area. `nest` makes the area's slot the root
|
|
429
|
-
of the nested grid, and reaches that grid's slots through it:
|
|
430
|
-
|
|
431
|
-
```tsx
|
|
432
|
-
import { defineLayout } from "typed-grid-areas";
|
|
433
|
-
import { createGridComponent } from "@typed-grid-areas/react";
|
|
434
|
-
|
|
435
|
-
const Content = createGridComponent(
|
|
436
|
-
defineLayout({ areas: ["icon?", "title"], layouts: [{ template: "icon title" }] }),
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
const Card = createGridComponent(
|
|
440
|
-
defineLayout({ areas: ["input?", "content"], layouts: [{ template: "input content" }] }),
|
|
441
|
-
{ nest: { content: Content } },
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
function Example() {
|
|
445
|
-
return (
|
|
446
|
-
<Card>
|
|
447
|
-
<Card.content className="p-4">
|
|
448
|
-
<Card.content.icon>★</Card.content.icon>
|
|
449
|
-
<Card.content.title>Title</Card.content.title>
|
|
450
|
-
</Card.content>
|
|
451
|
-
</Card>
|
|
452
|
-
);
|
|
48
|
+
.card:not(:has(> .icon, > .cardTag)) {
|
|
49
|
+
grid-template-areas: "information";
|
|
50
|
+
grid-template-columns: minmax(0, 1fr);
|
|
453
51
|
}
|
|
52
|
+
/* …and a grid-area rule for each slot */
|
|
454
53
|
```
|
|
455
54
|
|
|
456
|
-
|
|
457
|
-
`Content`. If the nested grid has breakpoints, the outer grid's area goes on
|
|
458
|
-
its size container, the outermost element. Nest where the design has panels
|
|
459
|
-
of their own (padding, background, border): it also keeps each grid's
|
|
460
|
-
optional areas, and so its CSS, small.
|
|
461
|
-
|
|
462
|
-
### CSS and development warnings
|
|
55
|
+
## With typed-grid-areas
|
|
463
56
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
In development (`process.env.NODE_ENV === "development"`), a grid warns on
|
|
468
|
-
the console when:
|
|
469
|
-
|
|
470
|
-
- a required area's slot isn't rendered, or
|
|
471
|
-
- it has a direct child that isn't one of its slots.
|
|
472
|
-
|
|
473
|
-
`defineLayout` also warns, once per definition, about areas and layers that
|
|
474
|
-
aren't rectangles.
|
|
475
|
-
|
|
476
|
-
## Examples: PreferenceCard and ID card
|
|
477
|
-
|
|
478
|
-
**PreferenceCard:** an optional input panel (a checkbox, as a `<label>`)
|
|
479
|
-
beside a content panel with its own padding. The content panel is a nested
|
|
480
|
-
grid of an optional icon, the information, an optional tag and optional
|
|
481
|
-
children. With both an icon and a tag, the two share the top row above
|
|
482
|
-
the information on narrow cards, and from `@sm` all three share one row.
|
|
483
|
-
With only one of them, it sits beside the information at every width (the
|
|
484
|
-
second override: the derived layout without an icon would stack the tag
|
|
485
|
-
above the information). Children always take a full-width last row. The
|
|
486
|
-
library's browser acceptance tests render this design in every combination.
|
|
57
|
+
Draw the picture once, with every slot present. The library derives the
|
|
58
|
+
other three, checks every area name at compile time, and writes the CSS:
|
|
487
59
|
|
|
488
60
|
```tsx
|
|
489
|
-
import type { ReactNode } from "react";
|
|
490
61
|
import { defineLayout } from "typed-grid-areas";
|
|
491
62
|
import { createGridComponent } from "@typed-grid-areas/react";
|
|
492
63
|
|
|
493
|
-
const PreferenceContent = createGridComponent(
|
|
494
|
-
defineLayout({
|
|
495
|
-
areas: ["icon?", "information", "cardTag?", "children?"],
|
|
496
|
-
layouts: [
|
|
497
|
-
{
|
|
498
|
-
template: `
|
|
499
|
-
icon cardTag
|
|
500
|
-
information information
|
|
501
|
-
children children`,
|
|
502
|
-
columns: "min-content minmax(0, 1fr)",
|
|
503
|
-
align: { cardTag: { justify: "end" } },
|
|
504
|
-
breakpoints: {
|
|
505
|
-
"@sm": {
|
|
506
|
-
template: "icon information cardTag / children children children",
|
|
507
|
-
columns: "min-content minmax(0, 1fr) auto",
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
},
|
|
511
|
-
{ unless: ["cardTag"], template: "icon information / children children" },
|
|
512
|
-
{
|
|
513
|
-
when: ["cardTag"],
|
|
514
|
-
unless: ["icon"],
|
|
515
|
-
template: "information cardTag / children children",
|
|
516
|
-
columns: "minmax(0, 1fr) auto",
|
|
517
|
-
},
|
|
518
|
-
],
|
|
519
|
-
}),
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
const Card = createGridComponent(
|
|
523
|
-
defineLayout({
|
|
524
|
-
areas: ["input?", "content"],
|
|
525
|
-
// `content` has breakpoints, so it needs a `minmax(0, 1fr)` track, not `auto`.
|
|
526
|
-
layouts: [{ template: "input content", columns: "auto minmax(0, 1fr)" }],
|
|
527
|
-
}),
|
|
528
|
-
{ nest: { content: PreferenceContent } },
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
type PreferenceCardProps = {
|
|
532
|
-
input?: ReactNode;
|
|
533
|
-
icon?: ReactNode;
|
|
534
|
-
title: ReactNode;
|
|
535
|
-
tag?: ReactNode;
|
|
536
|
-
children?: ReactNode;
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
export function PreferenceCard({ input, icon, title, tag, children }: PreferenceCardProps) {
|
|
540
|
-
return (
|
|
541
|
-
<Card className="rounded-lg border">
|
|
542
|
-
{input && (
|
|
543
|
-
<Card.input asChild className="border-r p-4">
|
|
544
|
-
<label>{input}</label>
|
|
545
|
-
</Card.input>
|
|
546
|
-
)}
|
|
547
|
-
<Card.content className="gap-2 p-4">
|
|
548
|
-
{icon && <Card.content.icon>{icon}</Card.content.icon>}
|
|
549
|
-
<Card.content.information>{title}</Card.content.information>
|
|
550
|
-
{tag && <Card.content.cardTag>{tag}</Card.content.cardTag>}
|
|
551
|
-
{children && <Card.content.children>{children}</Card.content.children>}
|
|
552
|
-
</Card.content>
|
|
553
|
-
</Card>
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
**ID card:** the layered card from [Layers](#layers-baselayer). On narrow
|
|
559
|
-
cards the logo, badge and identity sit in the photo's corners, and the name
|
|
560
|
-
turns white over the photo; from `@md` the photo is a column of its own and
|
|
561
|
-
everything else moves beside it.
|
|
562
|
-
|
|
563
|
-
```tsx
|
|
564
|
-
import { defineLayout } from "typed-grid-areas";
|
|
565
|
-
import { createGridComponent } from "@typed-grid-areas/react";
|
|
566
|
-
|
|
567
|
-
const Card = createGridComponent(
|
|
568
|
-
defineLayout({
|
|
569
|
-
areas: ["photo", "identity", "details", "logo?", "qr?", "badge?"],
|
|
570
|
-
layouts: [
|
|
571
|
-
{
|
|
572
|
-
template: `
|
|
573
|
-
photo+logo photo photo+badge
|
|
574
|
-
photo photo photo
|
|
575
|
-
photo+identity photo+identity photo+identity
|
|
576
|
-
details details qr`,
|
|
577
|
-
rows: "40px 80px 40px 60px",
|
|
578
|
-
columns: "60px 1fr 60px",
|
|
579
|
-
breakpoints: {
|
|
580
|
-
"@md": {
|
|
581
|
-
template: `
|
|
582
|
-
photo photo+badge identity logo
|
|
583
|
-
photo photo details qr`,
|
|
584
|
-
rows: "40px 120px",
|
|
585
|
-
columns: "100px 40px 1fr 80px",
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
},
|
|
589
|
-
],
|
|
590
|
-
}),
|
|
591
|
-
);
|
|
592
|
-
|
|
593
|
-
type Person = { name: string; role: string; photoUrl: string; verified?: boolean };
|
|
594
|
-
|
|
595
|
-
export function IdCard({ person, qrUrl }: { person: Person; qrUrl?: string }) {
|
|
596
|
-
return (
|
|
597
|
-
<Card className="overflow-hidden rounded-xl">
|
|
598
|
-
<Card.photo asChild>
|
|
599
|
-
<img src={person.photoUrl} alt="" className="h-full w-full object-cover" />
|
|
600
|
-
</Card.photo>
|
|
601
|
-
<Card.logo>ACME</Card.logo>
|
|
602
|
-
{person.verified && <Card.badge>✓</Card.badge>}
|
|
603
|
-
<Card.identity>
|
|
604
|
-
{/* White over the photo, through `@container style(--tga-over: photo)`. */}
|
|
605
|
-
<span className="identity-name">{person.name}</span>
|
|
606
|
-
</Card.identity>
|
|
607
|
-
<Card.details>{person.role}</Card.details>
|
|
608
|
-
{qrUrl && (
|
|
609
|
-
<Card.qr asChild>
|
|
610
|
-
<img src={qrUrl} alt="QR code" />
|
|
611
|
-
</Card.qr>
|
|
612
|
-
)}
|
|
613
|
-
</Card>
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
## Rendering with Angular: grid directives
|
|
619
|
-
|
|
620
|
-
```sh
|
|
621
|
-
npm i typed-grid-areas @typed-grid-areas/angular
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
`@typed-grid-areas/angular` supports Angular 22 (`@angular/core`
|
|
625
|
-
`^22.0.0`). Instead of components, it gives you three standalone attribute
|
|
626
|
-
directives that go on your own elements, and a typed reference per
|
|
627
|
-
definition that the template names grids and areas through:
|
|
628
|
-
|
|
629
|
-
```ts angular
|
|
630
|
-
import { Component, input } from "@angular/core";
|
|
631
|
-
import { defineLayout } from "typed-grid-areas";
|
|
632
|
-
import { createGrid, TgaArea, TgaGrid, TgaSizeContainer } from "@typed-grid-areas/angular";
|
|
633
|
-
|
|
634
|
-
const Content = createGrid(
|
|
635
|
-
defineLayout({
|
|
636
|
-
areas: ["icon?", "title"],
|
|
637
|
-
layouts: [{ template: "title", breakpoints: { "@sm": { template: "icon title" } } }],
|
|
638
|
-
}),
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
const Card = createGrid(
|
|
642
|
-
defineLayout({ areas: ["input?", "content"], layouts: [{ template: "input content" }] }),
|
|
643
|
-
{ nest: { content: Content } },
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
@Component({
|
|
647
|
-
selector: "app-card",
|
|
648
|
-
imports: [TgaGrid, TgaArea, TgaSizeContainer],
|
|
649
|
-
template: `
|
|
650
|
-
<div [tgaGrid]="Card" class="gap-2">
|
|
651
|
-
@if (selectable()) {
|
|
652
|
-
<label [tgaArea]="Card.input"><input type="checkbox" /></label>
|
|
653
|
-
}
|
|
654
|
-
<div tgaSizeContainer>
|
|
655
|
-
<div [tgaGrid]="Card.content" class="p-4">
|
|
656
|
-
<span [tgaArea]="Card.content.icon">★</span>
|
|
657
|
-
<h2 [tgaArea]="Card.content.title">Title</h2>
|
|
658
|
-
</div>
|
|
659
|
-
</div>
|
|
660
|
-
</div>
|
|
661
|
-
`,
|
|
662
|
-
})
|
|
663
|
-
export class CardComponent {
|
|
664
|
-
protected readonly Card = Card;
|
|
665
|
-
readonly selectable = input(false);
|
|
666
|
-
}
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
- **`createGrid(definition, { nest })`** returns the definition's reference:
|
|
670
|
-
the grid itself, with one property per area (`Card.input`). An area nested
|
|
671
|
-
through `nest` is the nested grid's reference (`Card.content`), whose
|
|
672
|
-
properties are its areas (`Card.content.icon`). Create references at module
|
|
673
|
-
level and expose them to the template from the component
|
|
674
|
-
(`protected readonly Card = Card`).
|
|
675
|
-
- **`[tgaGrid]="Card"`** makes the element it's on the grid. It takes a grid
|
|
676
|
-
reference, so with `strictTemplates`, `[tgaGrid]="Card.input"` doesn't
|
|
677
|
-
compile.
|
|
678
|
-
- **`[tgaArea]="Card.input"`** makes the element it's on the slot that fills
|
|
679
|
-
an area. It takes an area reference, not a string, so a misspelled area
|
|
680
|
-
(`Card.inputt`, `Card.content.iconn`), a plain string or a grid reference
|
|
681
|
-
doesn't compile. Slots must be direct children of their grid's element;
|
|
682
|
-
make an optional area absent with `@if`.
|
|
683
|
-
- **`tgaSizeContainer`** marks the element around a grid with breakpoints as
|
|
684
|
-
its size container (a directive can't wrap its own element). It gets the
|
|
685
|
-
container styles, and when the grid is nested, the outer grid's
|
|
686
|
-
`data-area`. Leave it out for grids without breakpoints.
|
|
687
|
-
|
|
688
|
-
**Your own element as a slot** needs nothing extra: put `[tgaArea]` (or
|
|
689
|
-
`[tgaGrid]`) on any element, including a component's host element
|
|
690
|
-
(`<app-avatar [tgaArea]="Card.content.icon" />`). Your classes and
|
|
691
|
-
attributes stay where you wrote them.
|
|
692
|
-
|
|
693
|
-
**CSS.** Each definition's CSS goes into a
|
|
694
|
-
`<style data-typed-grid-areas="…">` in the `<head>` of Angular's `DOCUMENT`,
|
|
695
|
-
once, however many instances render, carrying the `CSP_NONCE` value when
|
|
696
|
-
your app provides one. Without server rendering it's inserted when the
|
|
697
|
-
first grid of a definition renders. With server rendering (`@angular/ssr`)
|
|
698
|
-
it's in the rendered `<head>`, and client hydration reuses it rather than
|
|
699
|
-
adding a second copy. Nothing to set up either way.
|
|
700
|
-
|
|
701
|
-
**Development warnings.** In development mode (`isDevMode()`), in the
|
|
702
|
-
browser, a grid warns on the console after every render when a required
|
|
703
|
-
area's slot is missing, when it has a direct child that isn't one of its
|
|
704
|
-
slots, or when it has breakpoints but its parent isn't its size container.
|
|
705
|
-
Production builds and server rendering log nothing.
|
|
706
|
-
|
|
707
|
-
## Rendering with Vue: `@typed-grid-areas/vue`
|
|
708
|
-
|
|
709
|
-
```sh
|
|
710
|
-
npm i typed-grid-areas @typed-grid-areas/vue
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
`@typed-grid-areas/vue` needs `vue` 3.5 or later. It has the React
|
|
714
|
-
adapter's shape: `createGridComponent(definition, { nest })` returns the
|
|
715
|
-
grid's component, with a member component per area that templates use as a
|
|
716
|
-
tag (`<Card.input>`):
|
|
717
|
-
|
|
718
|
-
```vue
|
|
719
|
-
<script setup lang="ts">
|
|
720
|
-
import { defineLayout } from "typed-grid-areas";
|
|
721
|
-
import { createGridComponent } from "@typed-grid-areas/vue";
|
|
722
|
-
|
|
723
|
-
defineProps<{ selectable: boolean }>();
|
|
724
|
-
|
|
725
|
-
const Content = createGridComponent(
|
|
726
|
-
defineLayout({ areas: ["icon?", "title"], layouts: [{ template: "icon title" }] }),
|
|
727
|
-
);
|
|
728
|
-
const Card = createGridComponent(
|
|
729
|
-
defineLayout({ areas: ["input?", "content"], layouts: [{ template: "input content" }] }),
|
|
730
|
-
{ nest: { content: Content } },
|
|
731
|
-
);
|
|
732
|
-
</script>
|
|
733
|
-
|
|
734
|
-
<template>
|
|
735
|
-
<Card class="gap-4" aria-label="Card">
|
|
736
|
-
<Card.input v-if="selectable" as-child>
|
|
737
|
-
<label><input type="checkbox" /></label>
|
|
738
|
-
</Card.input>
|
|
739
|
-
<Card.content class="p-4">
|
|
740
|
-
<Card.content.icon>★</Card.content.icon>
|
|
741
|
-
<Card.content.title>Title</Card.content.title>
|
|
742
|
-
</Card.content>
|
|
743
|
-
</Card>
|
|
744
|
-
</template>
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
- **`<Card>`** renders the grid, a `div` taking `class`, `style` and any other
|
|
748
|
-
attribute (inside its own size container when the definition has
|
|
749
|
-
breakpoints; your attributes still go on the grid). **`<Card.input>`** is
|
|
750
|
-
the slot for the `input` area, a `div` marked `data-area="input"`.
|
|
751
|
-
**`<Card.content>`** is both the `content` slot and the nested grid, so
|
|
752
|
-
`<Card.content.icon>` is one of its slots. Leave an optional area out with
|
|
753
|
-
`v-if`; slots must be direct children of their grid.
|
|
754
|
-
- **Bind grid components in `<script setup>`** (or return them from
|
|
755
|
-
`setup()`): Vue resolves member tags like `<Card.input>` only from there.
|
|
756
|
-
Registering them under `components:` (Options API) and in-DOM templates
|
|
757
|
-
aren't supported. Run `vue-tsc` to type-check templates: `<Card.inputt>`
|
|
758
|
-
and `<Card.content.iconn>` are type errors.
|
|
759
|
-
- **`as-child`** (on slots and on the grid) renders your own single child
|
|
760
|
-
element instead of a `div`, the way Reka UI's `as-child` does: the slot's
|
|
761
|
-
attributes are merged into the child's (the child's own win, classes and
|
|
762
|
-
styles combine, event handlers both run), it always gets the slot's
|
|
763
|
-
`data-area` (or the grid's `data-grid-id`), and its own `ref` still
|
|
764
|
-
reaches it.
|
|
765
|
-
- **Area names Vue reserves** on a component (`length`, `props`, `emits`,
|
|
766
|
-
`slots`, `inheritAttrs`, `displayName`, `compatConfig`, and any starting
|
|
767
|
-
with `__`) can't be members: `createGridComponent` rejects them, as a type
|
|
768
|
-
error and at runtime.
|
|
769
|
-
|
|
770
|
-
**CSS.** In the browser, each grid puts its definition's CSS into `<head>`
|
|
771
|
-
once (a `<style data-typed-grid-areas>`), however many instances render, and
|
|
772
|
-
leaves it there. Vue has no API for `<head>` on the server, so when you
|
|
773
|
-
server-render, add the grids' style tags to your page's `<head>` in your
|
|
774
|
-
server entry with `renderGridStyles`:
|
|
775
|
-
|
|
776
|
-
```js
|
|
777
|
-
import { renderToString } from "vue/server-renderer";
|
|
778
|
-
import { renderGridStyles } from "@typed-grid-areas/vue";
|
|
779
|
-
|
|
780
|
-
/** @param {import("vue").App} app the app from `createSSRApp` */
|
|
781
|
-
export async function render(app) {
|
|
782
|
-
const ctx = {};
|
|
783
|
-
const html = await renderToString(app, ctx);
|
|
784
|
-
return { html, head: renderGridStyles(ctx) };
|
|
785
|
-
}
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
It returns one tag per definition rendered with that `ctx`, and hydration
|
|
789
|
-
reuses them instead of adding a second. Without it, server-rendered grids
|
|
790
|
-
still work: their CSS is inserted on hydration, so the page lays out once
|
|
791
|
-
the app's script runs.
|
|
792
|
-
|
|
793
|
-
**Development warnings.** In development (`process.env.NODE_ENV ===
|
|
794
|
-
"development"`), a grid warns on the console after it mounts and after each
|
|
795
|
-
update when a required area's slot isn't rendered, or when it has a direct
|
|
796
|
-
child that isn't one of its slots. Production builds and server rendering
|
|
797
|
-
log nothing.
|
|
798
|
-
|
|
799
|
-
## Rendering with Solid: `@typed-grid-areas/solid`
|
|
800
|
-
|
|
801
|
-
```sh
|
|
802
|
-
npm i typed-grid-areas @typed-grid-areas/solid
|
|
803
|
-
```
|
|
804
|
-
|
|
805
|
-
`@typed-grid-areas/solid` supports Solid 1.x: it needs `solid-js` 1.9 or
|
|
806
|
-
later within 1.x. Solid 2 will be supported by a new major of the adapter.
|
|
807
|
-
|
|
808
|
-
The API is the React adapter's: `createGridComponent(definition, { nest })`
|
|
809
|
-
returns the grid component, with a slot component per area (`<Card.icon>`),
|
|
810
|
-
and a nested grid's slots reached through its area (`<Card.content.icon>`).
|
|
811
|
-
Create it once, at module level. Slots and the grid take `class`, `style`,
|
|
812
|
-
`ref`, `children` and any other attribute of the element they render.
|
|
813
|
-
|
|
814
|
-
```tsx solid
|
|
815
|
-
import { createSignal, Show } from "solid-js";
|
|
816
|
-
import { defineLayout } from "typed-grid-areas";
|
|
817
|
-
import { createGridComponent } from "@typed-grid-areas/solid";
|
|
818
|
-
|
|
819
|
-
const Content = createGridComponent(
|
|
820
|
-
defineLayout({ areas: ["icon?", "title", "tag?"], layouts: [{ template: "icon title tag" }] }),
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
const Card = createGridComponent(
|
|
824
|
-
defineLayout({ areas: ["input?", "content"], layouts: [{ template: "input content" }] }),
|
|
825
|
-
{ nest: { content: Content } },
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
function Example(props: { tag?: string }) {
|
|
829
|
-
const [hasIcon, setHasIcon] = createSignal(true);
|
|
830
|
-
return (
|
|
831
|
-
<Card class="gap-4" aria-label="Card">
|
|
832
|
-
<Card.input as="label" for="notify">
|
|
833
|
-
<input id="notify" type="checkbox" onChange={() => setHasIcon(!hasIcon())} />
|
|
834
|
-
</Card.input>
|
|
835
|
-
<Card.content class="p-4">
|
|
836
|
-
<Show when={hasIcon()}>
|
|
837
|
-
<Card.content.icon>★</Card.content.icon>
|
|
838
|
-
</Show>
|
|
839
|
-
<Card.content.title>Weekly digest</Card.content.title>
|
|
840
|
-
<Show when={props.tag}>
|
|
841
|
-
<Card.content.tag>{props.tag}</Card.content.tag>
|
|
842
|
-
</Show>
|
|
843
|
-
</Card.content>
|
|
844
|
-
</Card>
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
- **Optional areas** are absent when their slot isn't rendered: use Solid's
|
|
850
|
-
own `<Show>` (or any conditional). Slots stay reactive, through a nested
|
|
851
|
-
grid's root too, and so does the layout.
|
|
852
|
-
- **Your own element as a slot: `as`.** Solid's JSX creates real DOM nodes,
|
|
853
|
-
so there's no `asChild`; instead `as` (on slots and the grid) renders the
|
|
854
|
-
slot as a tag name, a component or a render function, with no wrapper.
|
|
855
|
-
The other props are typed from `as`: with `as="label"`, `for` type-checks
|
|
856
|
-
and `ref` gets an `HTMLLabelElement`, and an attribute a label doesn't take
|
|
857
|
-
is an error. A component (or render function) gets the slot's props,
|
|
858
|
-
including its `data-area`, and must spread them onto its root element.
|
|
859
|
-
Annotate a render function's `props` for the slot to take that element's
|
|
860
|
-
attributes:
|
|
861
|
-
|
|
862
|
-
```tsx solid
|
|
863
|
-
import type { JSX } from "solid-js";
|
|
864
|
-
import { defineLayout } from "typed-grid-areas";
|
|
865
|
-
import { createGridComponent } from "@typed-grid-areas/solid";
|
|
866
|
-
|
|
867
|
-
const Row = createGridComponent(
|
|
868
|
-
defineLayout({ areas: ["icon", "input"], layouts: [{ template: "icon input" }] }),
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
function Icon(props: JSX.SvgSVGAttributes<SVGSVGElement> & { size: number }) {
|
|
872
|
-
return <svg {...props} width={props.size} height={props.size} />;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function Example() {
|
|
876
|
-
return (
|
|
877
|
-
<Row as="section">
|
|
878
|
-
<Row.icon as={Icon} size={16} />
|
|
879
|
-
<Row.input
|
|
880
|
-
as={(props: JSX.LabelHTMLAttributes<HTMLLabelElement>) => <label {...props} />}
|
|
881
|
-
for="notify"
|
|
882
|
-
>
|
|
883
|
-
<input id="notify" type="checkbox" />
|
|
884
|
-
</Row.input>
|
|
885
|
-
</Row>
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
```
|
|
889
|
-
|
|
890
|
-
- **CSS.** Each definition's CSS goes on the page once, however many
|
|
891
|
-
instances render. In the browser, the grid puts its style tag in `<head>`
|
|
892
|
-
as it's created, reusing one the server rendered, so hydration adds no
|
|
893
|
-
second copy. On the server, the grid registers its style tag with Solid's
|
|
894
|
-
`useAssets`, once per definition per render, and `renderToString`,
|
|
895
|
-
`renderToStringAsync` and `renderToStream` put it before `</head>` when the
|
|
896
|
-
app renders the whole document (as SolidStart does). If your app renders
|
|
897
|
-
only part of the page, put `getAssets()` from `solid-js/web` in the
|
|
898
|
-
`<head>` you render around it. Nothing else is needed: no `@solidjs/meta`.
|
|
899
|
-
- **Development warnings** are React's: in development
|
|
900
|
-
(`process.env.NODE_ENV === "development"`), after mount and after every
|
|
901
|
-
change to its slots, a grid warns about a missing required slot or a
|
|
902
|
-
direct child that isn't a slot, once per distinct set of problems. Never on
|
|
903
|
-
the server, never in production.
|
|
904
|
-
- **Builds.** The package's `solid` export is Solid JSX for your bundler's
|
|
905
|
-
Solid plugin (`vite-plugin-solid` and the like) to compile, for the browser
|
|
906
|
-
or the server; its `import` export is compiled for the browser.
|
|
907
|
-
- **Server rendering needs a Solid-aware bundler.** Only the `solid` export
|
|
908
|
-
renders on the server, so the server build must resolve the `solid`
|
|
909
|
-
export condition, as `vite-plugin-solid` (and SolidStart) does. Without
|
|
910
|
-
that condition, plain Node loads the browser build, and importing it
|
|
911
|
-
throws Solid's "Client-only API called on the server side".
|
|
912
|
-
|
|
913
|
-
## Rendering with Svelte: `@typed-grid-areas/svelte`
|
|
914
|
-
|
|
915
|
-
```sh
|
|
916
|
-
npm i typed-grid-areas @typed-grid-areas/svelte
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
Needs Svelte 5.57.0 or later, in runes mode. The package ships `.svelte`
|
|
920
|
-
files under the `svelte` export condition, so build with a Svelte-aware
|
|
921
|
-
bundler (Vite with `@sveltejs/vite-plugin-svelte`, or SvelteKit).
|
|
922
|
-
|
|
923
|
-
`createGridComponent(definition, { nest })` turns a definition into a
|
|
924
|
-
component with one member per area, like the React adapter. A `.svelte`
|
|
925
|
-
file can't be a generic factory, so create grid components in a module: a
|
|
926
|
-
`.ts` file, or a component's `<script module>`:
|
|
927
|
-
|
|
928
|
-
```svelte
|
|
929
|
-
<script module lang="ts">
|
|
930
|
-
import { defineLayout } from "typed-grid-areas";
|
|
931
|
-
import { createGridComponent } from "@typed-grid-areas/svelte";
|
|
932
|
-
|
|
933
|
-
const Content = createGridComponent(
|
|
934
|
-
defineLayout({
|
|
935
|
-
areas: ["icon?", "information", "cardTag?"],
|
|
936
|
-
layouts: [
|
|
937
|
-
{
|
|
938
|
-
template: "icon information cardTag",
|
|
939
|
-
columns: "min-content minmax(0, 1fr) auto",
|
|
940
|
-
},
|
|
941
|
-
],
|
|
942
|
-
}),
|
|
943
|
-
);
|
|
944
|
-
|
|
945
|
-
const Card = createGridComponent(
|
|
946
|
-
defineLayout({
|
|
947
|
-
areas: ["input?", "content"],
|
|
948
|
-
layouts: [{ template: "input content", columns: "auto minmax(0, 1fr)" }],
|
|
949
|
-
}),
|
|
950
|
-
{ nest: { content: Content } },
|
|
951
|
-
);
|
|
952
|
-
</script>
|
|
953
|
-
|
|
954
|
-
<script lang="ts">
|
|
955
|
-
let { selectable = false, tag }: { selectable?: boolean; tag?: string } = $props();
|
|
956
|
-
</script>
|
|
957
|
-
|
|
958
|
-
<Card class="gap-2" aria-label="Card">
|
|
959
|
-
{#if selectable}
|
|
960
|
-
<Card.input>
|
|
961
|
-
{#snippet child({ props })}
|
|
962
|
-
<label {...props}><input type="checkbox" /></label>
|
|
963
|
-
{/snippet}
|
|
964
|
-
</Card.input>
|
|
965
|
-
{/if}
|
|
966
|
-
<Card.content class="p-4">
|
|
967
|
-
<Card.content.icon>★</Card.content.icon>
|
|
968
|
-
<Card.content.information>Weekly digest</Card.content.information>
|
|
969
|
-
{#if tag}
|
|
970
|
-
<Card.content.cardTag>{tag}</Card.content.cardTag>
|
|
971
|
-
{/if}
|
|
972
|
-
</Card.content>
|
|
973
|
-
</Card>
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
- **`<Card>`** renders the grid, and **`<Card.icon>`** the slot for the
|
|
977
|
-
`icon` area, exactly as in React: the same DOM, the same size container
|
|
978
|
-
for a definition with breakpoints (your attributes still go on the grid),
|
|
979
|
-
and `<Card.iconn>` is an error in `svelte-check`. Both take any attribute
|
|
980
|
-
a `div` takes (`class`, `style`, `aria-*`, event handlers) and children.
|
|
981
|
-
- **Optional areas:** leave a slot out with an ordinary `{#if}`; the
|
|
982
|
-
derived layout for its absence applies. Slots must be direct children of
|
|
983
|
-
their grid (`{#if}`, `{#each}` and snippets rendered in place are fine).
|
|
984
|
-
- **Your own element as a slot (or the grid):** pass a `child` snippet
|
|
985
|
-
instead of children, and spread its `props` onto your element, Bits UI's
|
|
986
|
-
convention: `{#snippet child({ props })}<label {...props}>…</label>{/snippet}`.
|
|
987
|
-
`props` carries the slot's `data-area` (or the grid's attributes) and
|
|
988
|
-
every attribute you gave the component.
|
|
989
|
-
- **Nesting:** `nest: { content: Content }` makes `<Card.content>` both the
|
|
990
|
-
`content` slot and the `Content` grid, reaching its slots as
|
|
991
|
-
`<Card.content.icon>`.
|
|
992
|
-
|
|
993
|
-
**Where the CSS goes.** Each definition's CSS is a
|
|
994
|
-
`<style data-typed-grid-areas="<grid id>">` in `<svelte:head>`, rendered by
|
|
995
|
-
exactly one instance of the definition, however many render:
|
|
996
|
-
|
|
997
|
-
- **With server rendering**, `render()` from `svelte/server` puts it in
|
|
998
|
-
`head` once per render; SvelteKit puts `head` into the page for you, and
|
|
999
|
-
with plain `svelte/server` you put it in your HTML's `<head>`. Hydration
|
|
1000
|
-
reuses the server's tag.
|
|
1001
|
-
- **Without server rendering**, the first mounted instance adds it to
|
|
1002
|
-
`document.head`; when that instance unmounts another one takes it over,
|
|
1003
|
-
and it goes away with the last instance.
|
|
1004
|
-
|
|
1005
|
-
**Development warnings.** Under the `development` export condition (Vite's
|
|
1006
|
-
dev server, Vitest), a grid warns on the console when a required area's
|
|
1007
|
-
slot isn't rendered or it has a direct child that isn't one of its slots,
|
|
1008
|
-
once per distinct set of problems, after mount and after each update.
|
|
1009
|
-
Production builds and server rendering log nothing.
|
|
1010
|
-
|
|
1011
|
-
## Rendering with Preact: `@typed-grid-areas/preact`
|
|
1012
|
-
|
|
1013
|
-
```sh
|
|
1014
|
-
npm i typed-grid-areas @typed-grid-areas/preact
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
`@typed-grid-areas/preact` is a native Preact adapter (no `preact/compat`)
|
|
1018
|
-
for Preact 11 (`preact` `^11.0.0`; checked with 11.1). Its API is the React
|
|
1019
|
-
adapter's: `createGridComponent(definition, { nest })` returns `<Card>`,
|
|
1020
|
-
with a slot component per area (`<Card.icon>`), each taking anything a
|
|
1021
|
-
`div` takes (`class` or `className`, `style`, `ref`, …):
|
|
1022
|
-
|
|
1023
|
-
```tsx preact
|
|
1024
|
-
import { defineLayout } from "typed-grid-areas";
|
|
1025
|
-
import { createGridComponent } from "@typed-grid-areas/preact";
|
|
1026
|
-
|
|
1027
64
|
const Card = createGridComponent(
|
|
1028
65
|
defineLayout({
|
|
1029
66
|
areas: ["icon?", "information", "cardTag?"],
|
|
@@ -1038,7 +75,7 @@ const Card = createGridComponent(
|
|
|
1038
75
|
|
|
1039
76
|
function Example({ tag }: { tag?: string }) {
|
|
1040
77
|
return (
|
|
1041
|
-
<Card
|
|
78
|
+
<Card className="gap-2">
|
|
1042
79
|
<Card.icon>★</Card.icon>
|
|
1043
80
|
<Card.information>Weekly digest</Card.information>
|
|
1044
81
|
{tag && <Card.cardTag>{tag}</Card.cardTag>}
|
|
@@ -1047,356 +84,66 @@ function Example({ tag }: { tag?: string }) {
|
|
|
1047
84
|
}
|
|
1048
85
|
```
|
|
1049
86
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
collects it with `createGridStyleSheet()`, a new one per request, and puts
|
|
1087
|
-
it in `<head>` itself:
|
|
1088
|
-
|
|
1089
|
-
```tsx preact
|
|
1090
|
-
import { renderToString } from "preact-render-to-string";
|
|
1091
|
-
import { createGridStyleSheet } from "@typed-grid-areas/preact";
|
|
1092
|
-
|
|
1093
|
-
function App() {
|
|
1094
|
-
return <main>…</main>;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const sheet = createGridStyleSheet();
|
|
1098
|
-
const body = renderToString(sheet.collect(<App />));
|
|
1099
|
-
const html = `<!doctype html><html><head>${sheet}</head><body>${body}</body></html>`;
|
|
1100
|
-
```
|
|
1101
|
-
|
|
1102
|
-
When the page hydrates, the grids reuse the server's style tags. In
|
|
1103
|
-
development, a grid rendered on the server outside a sheet warns that its
|
|
1104
|
-
CSS is missing.
|
|
1105
|
-
|
|
1106
|
-
## Rendering with Lit: grid template functions
|
|
1107
|
-
|
|
1108
|
-
```sh
|
|
1109
|
-
npm i typed-grid-areas @typed-grid-areas/lit
|
|
1110
|
-
```
|
|
1111
|
-
|
|
1112
|
-
`@typed-grid-areas/lit` needs `lit` 3. `createGrid(definition)` turns a
|
|
1113
|
-
definition into a template function; each area is a member of it, typed as
|
|
1114
|
-
the area's name. Create grids at module level, once per definition:
|
|
1115
|
-
|
|
1116
|
-
```ts lit
|
|
1117
|
-
import { css, html, LitElement, nothing } from "lit";
|
|
1118
|
-
import { defineLayout } from "typed-grid-areas";
|
|
1119
|
-
import { createGrid, gridStyles } from "@typed-grid-areas/lit";
|
|
1120
|
-
|
|
1121
|
-
const Content = createGrid(
|
|
1122
|
-
defineLayout({
|
|
1123
|
-
areas: ["icon?", "title"],
|
|
1124
|
-
layouts: [{ template: "icon title", columns: "min-content minmax(0, 1fr)" }],
|
|
1125
|
-
}),
|
|
1126
|
-
);
|
|
1127
|
-
|
|
1128
|
-
const Card = createGrid(
|
|
1129
|
-
defineLayout({
|
|
1130
|
-
areas: ["input?", "content"],
|
|
1131
|
-
layouts: [{ template: "input content", columns: "auto minmax(0, 1fr)" }],
|
|
1132
|
-
}),
|
|
1133
|
-
{ nest: { content: Content } },
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
class PreferenceCard extends LitElement {
|
|
1137
|
-
static styles = [
|
|
1138
|
-
gridStyles(Card),
|
|
1139
|
-
css`
|
|
1140
|
-
.gap {
|
|
1141
|
-
gap: 1rem;
|
|
1142
|
-
}
|
|
1143
|
-
`,
|
|
1144
|
-
];
|
|
1145
|
-
static properties = { selectable: { type: Boolean } };
|
|
1146
|
-
declare selectable: boolean;
|
|
1147
|
-
|
|
1148
|
-
constructor() {
|
|
1149
|
-
super();
|
|
1150
|
-
this.selectable = false;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
render() {
|
|
1154
|
-
return Card(
|
|
1155
|
-
html`
|
|
1156
|
-
${
|
|
1157
|
-
this.selectable
|
|
1158
|
-
? html`<label data-area=${Card.input}><input type="checkbox" /></label>`
|
|
1159
|
-
: nothing
|
|
1160
|
-
}
|
|
1161
|
-
${Card.content(
|
|
1162
|
-
html`
|
|
1163
|
-
<span data-area=${Card.content.icon}>★</span>
|
|
1164
|
-
<div data-area=${Card.content.title}>Weekly digest</div>
|
|
1165
|
-
`,
|
|
1166
|
-
{ class: "p-4" },
|
|
1167
|
-
)}
|
|
1168
|
-
`,
|
|
1169
|
-
{ class: "gap" },
|
|
1170
|
-
);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
customElements.define("preference-card", PreferenceCard);
|
|
1175
|
-
```
|
|
1176
|
-
|
|
1177
|
-
- **`Card(content, { class, style, ref })`** renders the grid: a `div` with
|
|
1178
|
-
the definition's `data-grid-id`, with `content` as its slots. When the
|
|
1179
|
-
definition has breakpoints the grid sits inside its own size container;
|
|
1180
|
-
the options still go on the grid. Lit can't spread arbitrary attributes,
|
|
1181
|
-
so the grid takes only `class`, `style` (text, or an object as `styleMap`
|
|
1182
|
-
takes it) and `ref` (as Lit's `ref` directive takes it): for other
|
|
1183
|
-
attributes or event listeners on the grid, use `ref`.
|
|
1184
|
-
- **`Card.input`** is the area's name, `"input"`, typed as that literal.
|
|
1185
|
-
A slot is any element with `data-area=${Card.input}`: your own `<label>`,
|
|
1186
|
-
`<img>` or `<button>` is the slot itself, with nothing to opt into and no
|
|
1187
|
-
wrapper. `Card.inptu` is a type error. A slot here is an element with
|
|
1188
|
-
`data-area`, never a `<slot>`.
|
|
1189
|
-
- **`nest`** makes the area's member the nested grid's template function:
|
|
1190
|
-
`Card.content(…)` renders `Content` as the `content` slot of `Card`, and
|
|
1191
|
-
`Card.content.icon` reaches its areas.
|
|
1192
|
-
|
|
1193
|
-
Slots must be **direct children** of their grid, in the grid's own DOM:
|
|
1194
|
-
grids render in light DOM, wherever your template is (usually your
|
|
1195
|
-
component's shadow root), since the generated CSS's child selectors can't see
|
|
1196
|
-
through a shadow boundary into `<slot>`ted children. Leave an optional
|
|
1197
|
-
area's slot out with an ordinary conditional (`nothing`) to make it absent.
|
|
1198
|
-
|
|
1199
|
-
### CSS, server rendering and development warnings
|
|
1200
|
-
|
|
1201
|
-
Each grid puts its definition's CSS in the root it renders in, after each
|
|
1202
|
-
update:
|
|
1203
|
-
|
|
1204
|
-
- **In a shadow root**, one shared constructable style sheet per definition
|
|
1205
|
-
is adopted into it, once per shadow root rather than once per page. Your
|
|
1206
|
-
component's own `static styles` still beat the grid's layered CSS, and an
|
|
1207
|
-
area its layout leaves out stays hidden whatever `display` its slot sets.
|
|
1208
|
-
- **In the document**, a `<style data-typed-grid-areas>` goes into `<head>`,
|
|
1209
|
-
once per definition.
|
|
1210
|
-
|
|
1211
|
-
`gridStyles(Card)` returns the CSS of `Card` and the grids nested in it, as a
|
|
1212
|
-
`CSSResult` for `static styles`. In the browser it's optional (a grid already
|
|
1213
|
-
there isn't adopted again). **Server rendering needs it**: with
|
|
1214
|
-
`@lit-labs/ssr`, Lit's server renderer, the CSS reaches each component's
|
|
1215
|
-
declarative shadow root only through `static styles`, and hydration with
|
|
1216
|
-
`@lit-labs/ssr-client` takes over from there. Grids rendered on the server
|
|
1217
|
-
outside any shadow root need their style tags in `<head>`: see
|
|
1218
|
-
`gridStyleTags` [below](#without-react-typed-grid-areasadapter).
|
|
1219
|
-
|
|
1220
|
-
In development (`process.env.NODE_ENV === "development"`), a grid warns on
|
|
1221
|
-
the console after an update when a required area's slot isn't rendered, or
|
|
1222
|
-
when it has a direct child that isn't one of its slots, once per distinct
|
|
1223
|
-
set of problems.
|
|
87
|
+
Without a tag, the card lays out as `icon information`; nothing in your code
|
|
88
|
+
handles that case. A misspelled area or a ragged row is a compile error.
|
|
89
|
+
|
|
90
|
+
## Why one grid, not nested flexboxes
|
|
91
|
+
|
|
92
|
+
- **One flat grid.** Every slot is a direct child, placed by name: no
|
|
93
|
+
wrapper divs that only exist to group things for one layout. A new
|
|
94
|
+
arrangement is a new picture, not new markup.
|
|
95
|
+
- **It follows what's actually rendered.** `:has()` sees which slots are in
|
|
96
|
+
the page. No `hasIcon` or `compact` props to keep your state and your
|
|
97
|
+
classes in step.
|
|
98
|
+
- **Hard to break by accident.** Every area name is type-checked, every
|
|
99
|
+
combination of slots has a layout, even the one nobody tried, and the
|
|
100
|
+
generated CSS has zero specificity, so your own classes always win.
|
|
101
|
+
- **Breakpoints follow the grid's own width**, not the viewport's, so a grid
|
|
102
|
+
behaves the same wherever it's placed.
|
|
103
|
+
- **Easy to read, for people and agents.** The template is the layout. Type
|
|
104
|
+
errors name the problem where you wrote it, and an agent skill ships with
|
|
105
|
+
the package.
|
|
106
|
+
- **Pure CSS at runtime, in any framework.** No JavaScript lays anything
|
|
107
|
+
out. Adapters for React, Vue, Svelte, Solid, Preact, Angular, Lit and
|
|
108
|
+
Octane, and a framework-free core.
|
|
109
|
+
|
|
110
|
+
## Frameworks
|
|
111
|
+
|
|
112
|
+
| Framework | Install | Reference |
|
|
113
|
+
| --------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| React | `npm i typed-grid-areas @typed-grid-areas/react` | [Rendering with React](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-react-grid-components) |
|
|
115
|
+
| Vue | `npm i typed-grid-areas @typed-grid-areas/vue` | [Rendering with Vue](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-vue-typed-grid-areasvue) |
|
|
116
|
+
| Svelte | `npm i typed-grid-areas @typed-grid-areas/svelte` | [Rendering with Svelte](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-svelte-typed-grid-areassvelte) |
|
|
117
|
+
| Solid | `npm i typed-grid-areas @typed-grid-areas/solid` | [Rendering with Solid](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-solid-typed-grid-areassolid) |
|
|
118
|
+
| Preact | `npm i typed-grid-areas @typed-grid-areas/preact` | [Rendering with Preact](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-preact-typed-grid-areaspreact) |
|
|
119
|
+
| Angular | `npm i typed-grid-areas @typed-grid-areas/angular` | [Rendering with Angular](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-angular-grid-directives) |
|
|
120
|
+
| Lit | `npm i typed-grid-areas @typed-grid-areas/lit` | [Rendering with Lit](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-lit-grid-template-functions) |
|
|
121
|
+
| Octane (experimental) | `npm i typed-grid-areas @typed-grid-areas/octane` | [Rendering with Octane](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#rendering-with-octane-typed-grid-areasoctane-experimental) |
|
|
122
|
+
| No framework | `npm i typed-grid-areas` | [Without React](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md#without-react-typed-grid-areasadapter) |
|
|
1224
123
|
|
|
1225
|
-
|
|
1226
|
-
Lit team) for server rendering.
|
|
1227
|
-
|
|
1228
|
-
## Rendering with Octane: `@typed-grid-areas/octane` (experimental)
|
|
124
|
+
## Install
|
|
1229
125
|
|
|
1230
126
|
```sh
|
|
1231
|
-
npm i typed-grid-areas @typed-grid-areas/
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
`@typed-grid-areas/octane` renders a definition as typed slot components in
|
|
1235
|
-
[Octane](https://octanejs.dev), natively (not through Octane's React
|
|
1236
|
-
compatibility layer). It is **experimental until Octane 1.0**: it supports
|
|
1237
|
-
the current Octane minor only, **Octane 0.16** (`octane@^0.16.0`), and each
|
|
1238
|
-
new Octane minor needs a release of the adapter.
|
|
1239
|
-
|
|
1240
|
-
The package ships its source (`.tsx`), not a build: your app's Octane
|
|
1241
|
-
bundler plugin (`@octanejs/vite-plugin`, Rspack, Rsbuild) compiles it like
|
|
1242
|
-
your own components, and plain `tsc` type-checks it.
|
|
1243
|
-
|
|
1244
|
-
```tsrx
|
|
1245
|
-
import { defineLayout } from "typed-grid-areas";
|
|
1246
|
-
import { createGridComponent } from "@typed-grid-areas/octane";
|
|
1247
|
-
|
|
1248
|
-
const Content = createGridComponent(
|
|
1249
|
-
defineLayout({ areas: ["icon?", "title"], layouts: [{ template: "icon title" }] }),
|
|
1250
|
-
);
|
|
1251
|
-
|
|
1252
|
-
const Card = createGridComponent(
|
|
1253
|
-
defineLayout({ areas: ["input?", "content"], layouts: [{ template: "input content" }] }),
|
|
1254
|
-
{ nest: { content: Content } },
|
|
1255
|
-
);
|
|
1256
|
-
|
|
1257
|
-
export function Example(props: { selectable: boolean }) @{
|
|
1258
|
-
<Card class="gap-4" render={<section />}>
|
|
1259
|
-
@if (props.selectable) {
|
|
1260
|
-
<Card.input render={<label for="notify" />}><input id="notify" type="checkbox" /></Card.input>
|
|
1261
|
-
}
|
|
1262
|
-
<Card.content>
|
|
1263
|
-
<Card.content.icon>{'★'}</Card.content.icon>
|
|
1264
|
-
<Card.content.title>{'Notify me'}</Card.content.title>
|
|
1265
|
-
</Card.content>
|
|
1266
|
-
</Card>
|
|
1267
|
-
}
|
|
1268
|
-
```
|
|
1269
|
-
|
|
1270
|
-
- **The API is React's** (see [Rendering with React](#rendering-with-react-grid-components)):
|
|
1271
|
-
`createGridComponent(definition, { nest })`, `<Card>` for the grid, a
|
|
1272
|
-
static slot component per area (`<Card.input>`, and `<Card.content.icon>`
|
|
1273
|
-
through `nest`), each taking `class`, `style`, `ref`, `children` and any
|
|
1274
|
-
other `div` props. Member tags work in `.tsx` and `.tsrx`, including
|
|
1275
|
-
inside `@if`; a misspelled area is a type error (`tsrx-tsc` checks
|
|
1276
|
-
`.tsrx`).
|
|
1277
|
-
- **Your own element as a slot: `render={<el />}`**, on slots and on the
|
|
1278
|
-
grid, instead of React's `asChild` (Octane's `.tsrx` compiles children to
|
|
1279
|
-
functions, which can't be cloned). The element is rendered with the
|
|
1280
|
-
slot's props merged in: the adapter's `data-area` / `data-grid-id` always
|
|
1281
|
-
win; otherwise the element's own props win; classes compose, styles merge,
|
|
1282
|
-
event handlers chain (the element's first), both refs attach; the slot's
|
|
1283
|
-
children become the element's children.
|
|
1284
|
-
- **Optional areas:** leave a slot out with Octane's ordinary conditional
|
|
1285
|
-
(`@if` in `.tsrx`, `{cond && …}` in `.tsx`); the derived layout applies.
|
|
1286
|
-
- **CSS:** each definition's CSS goes on the page once, through Octane's
|
|
1287
|
-
`injectStyle`: one `<style data-octane>` tag per definition in `<head>`,
|
|
1288
|
-
however many instances render. Without server rendering there is nothing
|
|
1289
|
-
to do. With it, `renderToString` (and `prerender`) return the CSS in
|
|
1290
|
-
`css`, separate from `html`: put `result.css` before your markup (in
|
|
1291
|
-
`<head>`), as Octane's scoped styles require. A stream
|
|
1292
|
-
(`renderToReadableStream`, `renderToPipeableStream`) leads its shell with
|
|
1293
|
-
it. Hydration adopts the server's tags instead of adding second copies.
|
|
1294
|
-
- **Development warnings** are React's: a missing required slot or a stray
|
|
1295
|
-
direct child warns once (after mount, and after an update that introduces
|
|
1296
|
-
it); production and server rendering log nothing.
|
|
1297
|
-
|
|
1298
|
-
```ts octane
|
|
1299
|
-
// entry-server.ts
|
|
1300
|
-
import { renderToString } from "octane/server";
|
|
1301
|
-
import { App } from "./App.tsrx";
|
|
1302
|
-
|
|
1303
|
-
const { html, css } = renderToString(App);
|
|
1304
|
-
const page = `<!doctype html><html><head>${css}</head><body><div id="root">${html}</div></body></html>`;
|
|
1305
|
-
```
|
|
1306
|
-
|
|
1307
|
-
## Without React: `typed-grid-areas/adapter`
|
|
1308
|
-
|
|
1309
|
-
The core has no React dependency. To render grids in another framework, build
|
|
1310
|
-
on `typed-grid-areas/adapter`: it says exactly what to render for a
|
|
1311
|
-
definition, so your adapter renders the same DOM the React one does.
|
|
1312
|
-
|
|
1313
|
-
```ts
|
|
1314
|
-
import { defineLayout } from "typed-grid-areas";
|
|
1315
|
-
import {
|
|
1316
|
-
createSlotCheck,
|
|
1317
|
-
gridMarkup,
|
|
1318
|
-
gridStyleTags,
|
|
1319
|
-
insertGridStyle,
|
|
1320
|
-
} from "typed-grid-areas/adapter";
|
|
1321
|
-
|
|
1322
|
-
const cardGrid = defineLayout({
|
|
1323
|
-
areas: ["icon?", "info"],
|
|
1324
|
-
layouts: [{ template: "icon info", breakpoints: { "@sm": { template: "info icon" } } }],
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
// Once per definition.
|
|
1328
|
-
const markup = gridMarkup(cardGrid);
|
|
1329
|
-
|
|
1330
|
-
// Per instance. Pass the outer grid's area when this grid fills one.
|
|
1331
|
-
const { sizeContainer, grid } = markup.elements("content");
|
|
1332
|
-
// sizeContainer: undefined without breakpoints; otherwise the element that
|
|
1333
|
-
// wraps the grid: { attributes: { "data-area": "content" }, style, styleText }
|
|
1334
|
-
// grid.attributes: { "data-grid-id": markup.id }, plus the outer "data-area"
|
|
1335
|
-
// when there's no size container
|
|
1336
|
-
|
|
1337
|
-
markup.slot("info"); // { "data-area": "info" }, for each direct child of the grid
|
|
1338
|
-
|
|
1339
|
-
// The CSS, once per page: in the browser...
|
|
1340
|
-
insertGridStyle(document, cardGrid);
|
|
1341
|
-
// ...or in server-rendered <head>.
|
|
1342
|
-
const head = gridStyleTags([cardGrid]);
|
|
1343
|
-
|
|
1344
|
-
// In development, after each render: what to warn about the grid's slots.
|
|
1345
|
-
try {
|
|
1346
|
-
if (process.env.NODE_ENV === "development") {
|
|
1347
|
-
const checkSlots = createSlotCheck(cardGrid, {
|
|
1348
|
-
strayChildRemedy: "make it a slot with asChild",
|
|
1349
|
-
});
|
|
1350
|
-
const gridElement = document.querySelector(`[data-grid-id="${markup.id}"]`);
|
|
1351
|
-
if (gridElement !== null) {
|
|
1352
|
-
checkSlots(gridElement).forEach(warning => console.warn(warning));
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
} catch (error) {
|
|
1356
|
-
// Without a bundler, `process` may not exist: then this isn't development.
|
|
1357
|
-
if (!(error instanceof ReferenceError)) throw error;
|
|
1358
|
-
}
|
|
127
|
+
npm i typed-grid-areas @typed-grid-areas/react
|
|
1359
128
|
```
|
|
1360
129
|
|
|
1361
|
-
|
|
1362
|
-
attributes and style, the grid inside it with its attributes, and each slot
|
|
1363
|
-
as a direct child of the grid with its attributes. The CSS makes the grid a
|
|
1364
|
-
grid and places the slots (see [Breakpoints](#breakpoints) for why a grid
|
|
1365
|
-
with breakpoints has its own size container).
|
|
1366
|
-
- **CSS.** `markup.css` belongs on the page once per definition, before its
|
|
1367
|
-
grids lay out. `insertGridStyle` puts a `<style data-typed-grid-areas="<grid
|
|
1368
|
-
id>">` in `<head>` (ahead of the page's other styles, reusing a tag the
|
|
1369
|
-
server rendered), and `gridStyleTags` renders those tags as an HTML string.
|
|
1370
|
-
A framework with its own way to emit a style once can key it by
|
|
1371
|
-
`markup.cssKey` instead.
|
|
1372
|
-
- **Slot check.** One check per grid instance; it reports a set of problems
|
|
1373
|
-
once. `strayChildRemedy` says how your framework makes an element a slot.
|
|
1374
|
-
Gate it on your framework's development flag, or read
|
|
1375
|
-
`process.env.NODE_ENV` as above: bundlers replace it, so production builds
|
|
1376
|
-
drop the check. Behind a call, such as `isDevelopment()`, they keep it.
|
|
130
|
+
Swap `react` for your framework's package from the table above.
|
|
1377
131
|
|
|
1378
|
-
|
|
1379
|
-
rendering by hand.
|
|
132
|
+
## Agent skill
|
|
1380
133
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
An npm-workspaces monorepo: one folder per package under `packages/`
|
|
1384
|
-
(`core` is `typed-grid-areas`, `react` is `@typed-grid-areas/react`, and the
|
|
1385
|
-
private `acceptance` holds the browser acceptance tests every adapter runs).
|
|
1386
|
-
`npm install` at the root sets them all up.
|
|
134
|
+
The core ships a skill that walks coding agents through writing, converting
|
|
135
|
+
and debugging layouts. Enable it with [TanStack Intent](https://tanstack.com/intent):
|
|
1387
136
|
|
|
1388
137
|
```sh
|
|
1389
|
-
|
|
1390
|
-
just test-browser # in-browser layout tests (Chromium via Playwright)
|
|
1391
|
-
just typecheck # tsc --noEmit, including the type tests
|
|
1392
|
-
just build # compile each package's src/ into its dist/, as npm pack does
|
|
138
|
+
npx @tanstack/intent@latest install
|
|
1393
139
|
```
|
|
1394
140
|
|
|
1395
|
-
|
|
1396
|
-
`npm test`, `npm run typecheck`, `npm run build`). Each package owns its
|
|
1397
|
-
toolchain, down to its TypeScript version; see
|
|
1398
|
-
[docs/agents/workspaces.md](https://github.com/MichaelBuss/typed-grid-areas/blob/main/docs/agents/workspaces.md) for adding an adapter
|
|
1399
|
-
and pinning its own TypeScript.
|
|
141
|
+
## Learn more
|
|
1400
142
|
|
|
1401
|
-
|
|
1402
|
-
|
|
143
|
+
- [Docs](https://michaelbuss.github.io/typed-grid-areas/): how it works,
|
|
144
|
+
the concepts, and live examples.
|
|
145
|
+
- [Try it in your browser](https://michaelbuss.github.io/typed-grid-areas/try-it/):
|
|
146
|
+
a playground, and an example app per framework.
|
|
147
|
+
- [The full API reference](https://github.com/MichaelBuss/typed-grid-areas/blob/main/REFERENCE.md):
|
|
148
|
+
every option and every adapter. It also ships in every package, matching
|
|
149
|
+
the version you installed: `node_modules/typed-grid-areas/REFERENCE.md`.
|