uniweb 0.8.7 → 0.8.9
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/package.json +7 -5
- package/partials/agents.md +512 -552
- package/partials/ai-assistance.hbs +5 -3
- package/partials/learn-more.hbs +3 -2
- package/src/commands/inspect.js +358 -0
- package/src/index.js +8 -0
- package/src/utils/scaffold.js +32 -8
- package/starter/foundation/src/sections/Section/index.jsx +13 -39
- package/starter/foundation/src/sections/Section/meta.js +2 -17
- package/starter/site/pages/home/1-welcome.md.hbs +1 -1
- package/templates/foundation/src/foundation.js.hbs +23 -1
- package/templates/site/site.yml.hbs +49 -0
- package/templates/site/theme.yml +3 -3
- package/templates/workspace/README.md.hbs +12 -14
package/partials/agents.md
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## The Architecture in One Sentence
|
|
4
|
+
|
|
5
|
+
A Uniweb project separates **what the site says** from **how it's built**. Content authors write markdown — choosing section types, setting params, composing layouts. Component developers build reusable section types that receive pre-parsed content and render it. Neither touches the other's files. Neither can break the other's work.
|
|
6
|
+
|
|
7
|
+
Every pattern in this guide serves that separation: markdown for content, frontmatter for configuration, `meta.js` for the contract between the two roles, semantic tokens for context adaptation, and a runtime that handles section wrapping, backgrounds, theming, and token resolution so components don't have to.
|
|
8
|
+
|
|
9
|
+
Once the runtime parses content and hands it to your component as `{ content, params }`, **it's standard React.** Standard Tailwind. Standard anything — import any library, use any pattern, build any UI. The `{ content, params }` interface is only for section types (components that content authors select in markdown). Everything else in your foundation is ordinary React with ordinary props. The framework handles the content pipeline and the boilerplate; you handle the design and interaction.
|
|
10
|
+
|
|
11
|
+
### What this replaces
|
|
12
|
+
|
|
13
|
+
In conventional React, content lives in JSX or ad-hoc data files. Theming means conditional logic in every component. Dark mode means `isDark ? 'text-white' : 'text-gray-900'` scattered everywhere. Each component handles its own background, its own null checks, its own i18n wrapping. A "simple" marketing page becomes hundreds of lines of undifferentiated boilerplate — and when a non-developer needs to change a headline, they open a pull request into code they don't understand.
|
|
14
|
+
|
|
15
|
+
Uniweb eliminates these categories of work. The runtime handles theming, backgrounds, and context adaptation. Components receive guaranteed content shapes — empty strings and arrays, never null. You build a *system* of section types, not individual pages. Authors compose pages from your system. That's what makes i18n, theming, and multi-site tractable: they're properties of the system, not things bolted onto individual components.
|
|
16
|
+
|
|
17
|
+
### Before you start: what the runtime already does
|
|
18
|
+
|
|
19
|
+
The most common mistake is reimplementing what the framework provides for free. Check this before writing any component logic:
|
|
20
|
+
|
|
21
|
+
| The runtime handles | So components should NOT contain |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Section backgrounds (image, video, gradient, color, overlay) from `background:` | Background rendering code, `bg-white`/`bg-gray-900` on wrapper |
|
|
24
|
+
| Context classes (`context-light`/`medium`/`dark`) on every section | Theme maps: `const themes = { light: {...}, dark: {...} }` |
|
|
25
|
+
| Token resolution — `text-heading` adapts automatically | Conditionals: `isDark ? 'text-white' : 'text-gray-900'` |
|
|
26
|
+
| Content parsing with guaranteed shape | Defensive null checks on content fields |
|
|
27
|
+
| Section wrapping in `<section>` with context class | Outer `<section>` with background/theme classes |
|
|
28
|
+
| i18n via locale-specific content directories | String wrapping with `t()` or `<Trans>` |
|
|
29
|
+
|
|
30
|
+
Components *should* contain: layout (`grid`, `flex`, `max-w-7xl`), spacing (`p-6`, `gap-8`), typography scale (`text-3xl`, `font-bold`), animations, border-radius — anything that stays the same regardless of theme context.
|
|
31
|
+
|
|
32
|
+
---
|
|
4
33
|
|
|
5
34
|
## Documentation
|
|
6
35
|
|
|
7
|
-
This project was created with [Uniweb](https://github.com/uniweb/cli). Full documentation (markdown, fetchable): https://github.com/uniweb/docs
|
|
36
|
+
This project was created with [Uniweb CLI](https://github.com/uniweb/cli). Full documentation (markdown, fetchable): https://github.com/uniweb/docs
|
|
8
37
|
|
|
9
38
|
**To read a specific page:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
|
|
10
39
|
|
|
@@ -27,15 +56,15 @@ This project was created with [Uniweb](https://github.com/uniweb/cli). Full docu
|
|
|
27
56
|
|
|
28
57
|
```
|
|
29
58
|
project/
|
|
30
|
-
├── foundation/ #
|
|
31
|
-
├── site/ # Content
|
|
59
|
+
├── foundation/ # Component developer's domain
|
|
60
|
+
├── site/ # Content author's domain
|
|
32
61
|
└── pnpm-workspace.yaml
|
|
33
62
|
```
|
|
34
63
|
|
|
35
64
|
Multi-site variant uses `foundations/` and `sites/` (plural) folders.
|
|
36
65
|
|
|
37
|
-
- **Foundation
|
|
38
|
-
- **Site
|
|
66
|
+
- **Foundation** (developer): React components. Those with `meta.js` are *section types* — selectable by content authors via `type:` in frontmatter. Everything else is ordinary React.
|
|
67
|
+
- **Site** (content author): Markdown content + configuration. Each section file references a section type. Authors work here without touching foundation code.
|
|
39
68
|
|
|
40
69
|
## Project Setup
|
|
41
70
|
|
|
@@ -48,7 +77,7 @@ pnpm create uniweb my-project
|
|
|
48
77
|
cd my-project && pnpm install
|
|
49
78
|
```
|
|
50
79
|
|
|
51
|
-
This creates a workspace with foundation + site + starter content — two commands to a dev server. Use `--template <
|
|
80
|
+
This creates a workspace with foundation + site + starter content — two commands to a dev server. Use `--template <n>` for an official template (`marketing`, `docs`, `academic`, etc.), `--template none` for foundation + site with no content, or `--blank` for an empty workspace.
|
|
52
81
|
|
|
53
82
|
### Adding a co-located project
|
|
54
83
|
|
|
@@ -68,7 +97,7 @@ pnpm uniweb add site # First site → ./site/
|
|
|
68
97
|
pnpm uniweb add site blog # Named → ./blog/
|
|
69
98
|
```
|
|
70
99
|
|
|
71
|
-
The name is both the directory name and the package name. Use `--project <
|
|
100
|
+
The name is both the directory name and the package name. Use `--project <n>` to co-locate under a project directory (e.g., `--project docs` → `docs/foundation/`).
|
|
72
101
|
|
|
73
102
|
### Adding section types
|
|
74
103
|
|
|
@@ -102,8 +131,14 @@ pnpm preview # Preview production build (SSG + SPA)
|
|
|
102
131
|
|
|
103
132
|
> **npm works too.** Projects include both `pnpm-workspace.yaml` and npm workspaces. Replace `pnpm` with `npm` in any command above.
|
|
104
133
|
|
|
134
|
+
---
|
|
135
|
+
|
|
105
136
|
## Content Authoring
|
|
106
137
|
|
|
138
|
+
The decision rule: **would a content author need to change this?** Yes → it belongs in markdown, frontmatter, or a tagged data block. No → it belongs in component code.
|
|
139
|
+
|
|
140
|
+
Start with the content, not the component. Write the markdown a content author would naturally write, check what content shape the parser produces, *then* build the component to receive it.
|
|
141
|
+
|
|
107
142
|
### Section Format
|
|
108
143
|
|
|
109
144
|
Each `.md` file is a section. Frontmatter on top, content below:
|
|
@@ -135,9 +170,9 @@ The semantic parser extracts markdown into a flat, guaranteed structure. No null
|
|
|
135
170
|
|
|
136
171
|
```js
|
|
137
172
|
content = {
|
|
138
|
-
title: '', // Main heading
|
|
173
|
+
title: '', // Main heading (string or string[] for multi-line)
|
|
139
174
|
pretitle: '', // Heading before main title (auto-detected)
|
|
140
|
-
subtitle: '', // Heading after title
|
|
175
|
+
subtitle: '', // Heading after title (string or string[] for multi-line)
|
|
141
176
|
subtitle2: '', // Third-level heading
|
|
142
177
|
paragraphs: [], // Text blocks
|
|
143
178
|
links: [], // { href, label, role } — standalone links become buttons
|
|
@@ -170,7 +205,28 @@ Lightning quick. ← items[0].paragraphs[0]
|
|
|
170
205
|
Enterprise-grade. ← items[1].paragraphs[0]
|
|
171
206
|
```
|
|
172
207
|
|
|
173
|
-
|
|
208
|
+
**Items have the full content shape** — this is the most commonly overlooked feature. Each item has `title`, `pretitle`, `subtitle`, `paragraphs`, `links`, `icons`, `lists`, and even `data` (tagged blocks). You don't need workarounds for structured content within items:
|
|
209
|
+
|
|
210
|
+
```markdown
|
|
211
|
+
### The Problem ← items[0].pretitle
|
|
212
|
+
## Content gets trapped ← items[0].title
|
|
213
|
+
Body text here. ← items[0].paragraphs[0]
|
|
214
|
+
|
|
215
|
+
### The Solution ← items[1].pretitle
|
|
216
|
+
## Separate content from code ← items[1].title
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
If you need an eyebrow label above an item's title, that's `pretitle` — the same heading hierarchy as the top level. Heading hierarchy within items follows the same rules — `####` within a `###` item becomes `items[0].subtitle`. If you need metadata per item, use a tagged block inside the item:
|
|
220
|
+
|
|
221
|
+
````markdown
|
|
222
|
+
### Starter ← items[0].title
|
|
223
|
+
$9/month ← items[0].paragraphs[0]
|
|
224
|
+
|
|
225
|
+
```yaml:details
|
|
226
|
+
trial: 14 days
|
|
227
|
+
seats: 1
|
|
228
|
+
``` ← items[0].data.details = { trial: "14 days", seats: 1 }
|
|
229
|
+
````
|
|
174
230
|
|
|
175
231
|
**Complete example — markdown and resulting content shape side by side:**
|
|
176
232
|
|
|
@@ -194,63 +250,71 @@ Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise
|
|
|
194
250
|
|
|
195
251
|
Headings before the main title become `pretitle`. Headings after the main title at a lower importance become `subtitle`. Headings that appear after body content (paragraphs, links, images) start the `items` array.
|
|
196
252
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```markdown
|
|
200
|
-
# Features ← title
|
|
253
|
+
### Choosing how to model content
|
|
201
254
|
|
|
202
|
-
|
|
203
|
-
- **Hot** reload ← lists[0][1].paragraphs[0] (HTML: "<strong>Hot</strong> reload")
|
|
204
|
-
```
|
|
255
|
+
You have three layers. Most of the design skill is choosing between them:
|
|
205
256
|
|
|
206
|
-
|
|
257
|
+
**Pure markdown** — headings, paragraphs, links, images, lists, items. This is the default. If the content reads naturally as markdown and the parser's semantic structure captures it, stop here. Most sections live entirely in this layer.
|
|
207
258
|
|
|
208
|
-
|
|
209
|
-
### Starter ← items[0].title
|
|
210
|
-
$9/month ← items[0].paragraphs[0]
|
|
259
|
+
**Frontmatter params** — `columns: 3`, `variant: centered`, `theme: dark`. Configuration that an author might change but that isn't *content*. Would changing this value change the section's *meaning*, or just its *presentation*? Presentation → param. Meaning → content.
|
|
211
260
|
|
|
212
|
-
-
|
|
213
|
-
- Feature B ← items[0].lists[0][1].paragraphs[0]
|
|
214
|
-
```
|
|
261
|
+
**Tagged data blocks** — for content that doesn't fit markdown patterns. Products with SKUs, team members with roles, event schedules, pricing metadata, form definitions. When the information is genuinely structured data that a content author still owns, a well-named tagged block (`yaml:pricing`, `yaml:speakers`, `yaml:config`) is clearer than contorting markdown into a data format.
|
|
215
262
|
|
|
216
|
-
|
|
263
|
+
Read the markdown out loud. If a content author would understand what every line does and how to edit it, you've chosen the right layer. The moment markdown feels like it's encoding data rather than expressing content, step up to a tagged block — that's fine. A well-documented `yaml:pricing` block is better than a markdown structure that puzzles the author.
|
|
217
264
|
|
|
218
|
-
|
|
219
|
-
import { Span } from '@uniweb/kit'
|
|
265
|
+
**You are designing these, not choosing from a menu.** The examples in this guide illustrate patterns, not exhaustive inventories. Any param name works in `meta.js`. Any tag name works for data blocks. Any section type name works. The framework has fixed mechanisms (the content shape, the context modes, the token system); nearly everything else is yours to define.
|
|
220
266
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
267
|
+
```js
|
|
268
|
+
// You design this — it's not a fixed schema
|
|
269
|
+
export default {
|
|
270
|
+
params: {
|
|
271
|
+
columns: { type: 'number', default: 3 },
|
|
272
|
+
cardStyle: { type: 'select', options: ['minimal', 'bordered', 'elevated'], default: 'minimal' },
|
|
273
|
+
showIcon: { type: 'boolean', default: true },
|
|
274
|
+
maxItems: { type: 'number', default: 6 },
|
|
275
|
+
}
|
|
276
|
+
}
|
|
224
277
|
```
|
|
225
278
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
279
|
+
````markdown
|
|
280
|
+
<!-- You invent the tag name — the framework parses it -->
|
|
281
|
+
```yaml:speakers
|
|
282
|
+
- name: Ada Lovelace
|
|
283
|
+
role: Keynote
|
|
284
|
+
topic: The Future of Computing
|
|
285
|
+
```
|
|
286
|
+
````
|
|
287
|
+
Access: `content.data?.speakers` — an array of objects. You defined this. The framework parsed it.
|
|
229
288
|
|
|
230
|
-
|
|
289
|
+
**Parameter naming matters.** Would an author understand the param without reading code? `columns: 3` yes. `gridCols: 3` no. `variant: centered` yes. `renderMode: flex-center` no. `align: left` yes. `contentAlignment: flex-start` no.
|
|
231
290
|
|
|
232
|
-
###
|
|
291
|
+
### Multi-Line Headings
|
|
233
292
|
|
|
234
|
-
|
|
293
|
+
Consecutive headings at the same level merge into a title array — a single heading split across visual lines:
|
|
235
294
|
|
|
236
295
|
```markdown
|
|
237
|
-
|
|
238
|
-
|
|
296
|
+
# Build the future │ content.title = ["Build the future", "with confidence"]
|
|
297
|
+
# with confidence │
|
|
239
298
|
```
|
|
240
299
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
- `{params}` — configuration attributes passed as `block.properties`
|
|
300
|
+
Kit's `<H1>`, `<H2>`, etc. render arrays as a single tag with line breaks. This is how you create dramatic multi-line hero headlines.
|
|
301
|
+
|
|
302
|
+
**Works with accent styling:**
|
|
245
303
|
|
|
246
304
|
```markdown
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
305
|
+
# Build the future │ content.title = [
|
|
306
|
+
# [with confidence]{accent} │ "Build the future",
|
|
307
|
+
│ "<span accent=\"true\">with confidence</span>"
|
|
308
|
+
│ ]
|
|
251
309
|
```
|
|
252
310
|
|
|
253
|
-
|
|
311
|
+
**Rule:** Same-level continuation only applies before going deeper. Once a subtitle level is reached, same-level headings start new items instead of merging. Use `---` to force separate items when same-level headings would otherwise merge.
|
|
312
|
+
|
|
313
|
+
### Icons
|
|
314
|
+
|
|
315
|
+
Use image syntax with library prefix: ``. Supported libraries: `lu` (Lucide), `hi2` (Heroicons), `fi` (Feather), `pi` (Phosphor), `tb` (Tabler), `bs` (Bootstrap), `md` (Material), `fa6` (Font Awesome 6), and others. Browse at [react-icons.github.io/react-icons](https://react-icons.github.io/react-icons/).
|
|
316
|
+
|
|
317
|
+
Custom SVGs: `{role=icon}`
|
|
254
318
|
|
|
255
319
|
### Links and Media Attributes
|
|
256
320
|
|
|
@@ -262,24 +326,25 @@ Inset components must declare `inset: true` in their `meta.js`. They render at t
|
|
|
262
326
|
|
|
263
327
|
**Quote values that contain spaces:** `{note="Ready to go"}` not `{note=Ready to go}`. Unquoted values end at the first space.
|
|
264
328
|
|
|
265
|
-
Standalone links (alone on a line) become buttons. Inline links stay as
|
|
266
|
-
|
|
267
|
-
**Standalone links** — paragraphs that contain *only* links (no other text) are promoted to `content.links[]`. This works for single links and for multiple links sharing a paragraph:
|
|
329
|
+
Standalone links (alone on a line) become buttons in `content.links[]`. Inline links stay as `<a>` tags within `content.paragraphs[]`. Multiple links sharing a paragraph are all promoted to `content.links[]`:
|
|
268
330
|
|
|
269
331
|
```markdown
|
|
270
332
|
[Primary](/start) ← standalone → content.links[0]
|
|
271
|
-
|
|
272
|
-
[
|
|
273
|
-
|
|
274
|
-
[One](/a) [Two](/b) ← links-only paragraph → content.links[0], content.links[1]
|
|
333
|
+
[One](/a) [Two](/b) ← links-only paragraph → both in content.links[]
|
|
334
|
+
Check out [this](/a) link. ← inline → stays in paragraphs as <a> tag
|
|
275
335
|
```
|
|
276
336
|
|
|
277
|
-
|
|
337
|
+
### Inline Text Styling
|
|
278
338
|
|
|
279
339
|
```markdown
|
|
280
|
-
|
|
340
|
+
# Build [faster]{accent} with structure
|
|
341
|
+
This is [less important]{muted} context.
|
|
281
342
|
```
|
|
282
343
|
|
|
344
|
+
`accent` (colored + bold) and `muted` (subtle) adapt to context automatically. Components receive HTML strings with spans applied: `<span accent="true">faster</span>`.
|
|
345
|
+
|
|
346
|
+
Sites can define additional named styles in `theme.yml`'s `inline:` section.
|
|
347
|
+
|
|
283
348
|
### Structured Data
|
|
284
349
|
|
|
285
350
|
Tagged code blocks pass structured data via `content.data`:
|
|
@@ -295,73 +360,116 @@ submitLabel: Send
|
|
|
295
360
|
|
|
296
361
|
Access: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
|
|
297
362
|
|
|
298
|
-
**
|
|
363
|
+
**Untagged code blocks** (plain ``` js) are only visible to sequential-rendering components. If a component needs to access code blocks by name, tag them (`jsx:before`, `jsx:after` → `content.data?.before`, `content.data?.after`).
|
|
299
364
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
365
|
+
### Composition: Nesting and Embedding
|
|
366
|
+
|
|
367
|
+
Pages are sequences of sections — that's the obvious composition layer. But the framework supports real nesting: sections containing other sections, and sections containing embedded components. And it does this without leaving markdown.
|
|
368
|
+
|
|
369
|
+
**Insets — embedding components in content.** Many section types need a "visual" — a hero's illustration, a split-content section's media. The classic is an image or video. But what if it's a JSX + SVG diagram? A ThreeJS animation? An interactive code playground?
|
|
370
|
+
|
|
371
|
+
In other frameworks, this is where you'd reach for MDX, or prop-drill a component. In Uniweb, the content author writes:
|
|
372
|
+
|
|
373
|
+
```markdown
|
|
374
|
+
{variant=compact}
|
|
303
375
|
```
|
|
304
376
|
|
|
305
|
-
|
|
306
|
-
|
|
377
|
+
Standard markdown image syntax — `{attributes}`. The content author placed a full React component with content and params, and it looks like an image reference. The developer builds `NetworkDiagram` as an ordinary React component with `inset: true` in its `meta.js`. The kit's `<Visual>` component renders the first non-empty candidate — so the same section type works whether the author provides a static image, a video, or an interactive component:
|
|
378
|
+
|
|
379
|
+
```jsx
|
|
380
|
+
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="rounded-2xl" />
|
|
307
381
|
```
|
|
308
|
-
````
|
|
309
382
|
|
|
310
|
-
|
|
383
|
+
The content author controls what goes in the visual slot. The developer's component doesn't need to know or care whether it's rendering an image or a ThreeJS scene.
|
|
311
384
|
|
|
312
|
-
|
|
385
|
+
**Child sections — composing layouts from reusable pieces.** You encounter a complex layout — a 2:1 split with a panel and a main area, or a grid with different card types in each cell. Your instinct says: build a specialized component. But step back.
|
|
313
386
|
|
|
314
|
-
|
|
387
|
+
The panel? A reusable section type. The main area? Another one. The split? A Grid with `columns: "1fr 2fr"`. And your child components already adapt to narrow containers — container queries handle that.
|
|
315
388
|
|
|
316
|
-
|
|
389
|
+
But if you hardcode which components go where, the author can't rearrange or swap them. This is where child sections solve it:
|
|
317
390
|
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
391
|
+
```
|
|
392
|
+
pages/home/
|
|
393
|
+
├── 2-dashboard.md # type: Grid, columns: "1fr 2fr"
|
|
394
|
+
├── @sidebar-stats.md # type: StatPanel
|
|
395
|
+
└── @main-chart.md # type: PerformanceChart
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
```yaml
|
|
399
|
+
# page.yml
|
|
400
|
+
nest:
|
|
401
|
+
dashboard: [sidebar-stats, main-chart]
|
|
322
402
|
```
|
|
323
403
|
|
|
324
|
-
|
|
404
|
+
Each child is a regular section with its own type, params, and content. The Grid renders them with `<ChildBlocks from={block} />` — and you're in the middle: you can wrap each child, filter by type, reorder, add container classes. The author decides *what* goes in the grid; your component decides *how* it's rendered.
|
|
325
405
|
|
|
326
|
-
|
|
406
|
+
The author can swap a child for a different section type tomorrow without the developer changing a line of code. And the developer's components are reusable wherever child sections are accepted, not locked to this one layout.
|
|
327
407
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
408
|
+
**Choosing the right pattern:**
|
|
409
|
+
|
|
410
|
+
| Pattern | How authored | Use when |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| **Items** (`content.items`) | Heading groups within one `.md` file | Repeating content within one section: cards, features, FAQ entries |
|
|
413
|
+
| **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children that need their own section type, rich content, or independent editing |
|
|
414
|
+
| **Insets** (`block.insets`) | `` in markdown | Self-contained visuals/widgets: charts, diagrams, code demos |
|
|
415
|
+
|
|
416
|
+
Does the content author write content *inside* the nested element? **Yes** → child sections. **No** (self-contained, param-driven) → inset. Repeating same-structure groups within one section → items. These compose: a child section can contain insets, items work inside children.
|
|
417
|
+
|
|
418
|
+
Inset components declare `inset: true` in meta.js. Don't use `hidden: true` on insets — `hidden` means "don't export this component at all" (for internal helpers), while `inset: true` means "available for `@Component` references in markdown."
|
|
419
|
+
|
|
420
|
+
**SSG:** Insets, `<ChildBlocks>`, and `<Visual>` all render correctly during prerender. Inset components that use React hooks internally (useState, useEffect) will trigger prerender warnings — this is expected and harmless; the page renders correctly client-side.
|
|
421
|
+
|
|
422
|
+
### Section Nesting Details
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
pages/home/
|
|
426
|
+
├── page.yml
|
|
427
|
+
├── 1-hero.md
|
|
428
|
+
├── 2-features.md # Parent section (type: Grid)
|
|
429
|
+
├── 3-cta.md
|
|
430
|
+
├── @card-a.md # Child of features (@ = not top-level)
|
|
431
|
+
├── @card-b.md
|
|
432
|
+
└── @card-c.md
|
|
335
433
|
```
|
|
336
434
|
|
|
337
|
-
|
|
435
|
+
```yaml
|
|
436
|
+
# page.yml
|
|
437
|
+
nest:
|
|
438
|
+
features: [card-a, card-b, card-c]
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Rules:**
|
|
442
|
+
- `@`-prefixed files are excluded from the top-level section list
|
|
443
|
+
- `nest:` declares parent-child relationships (parent name → child names)
|
|
444
|
+
- `@@` prefix for deeper nesting (grandchildren)
|
|
445
|
+
- `nest:` is flat: `{ features: [a, b], a: [sub-1] }`
|
|
446
|
+
- Children ordered by position in the `nest:` array
|
|
338
447
|
|
|
339
448
|
```jsx
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
))
|
|
449
|
+
import { ChildBlocks } from '@uniweb/kit'
|
|
450
|
+
|
|
451
|
+
export default function Grid({ block, params }) {
|
|
452
|
+
return (
|
|
453
|
+
<div className={`grid grid-cols-${params.columns || 2} gap-6`}>
|
|
454
|
+
<ChildBlocks from={block} />
|
|
455
|
+
</div>
|
|
456
|
+
)
|
|
457
|
+
}
|
|
350
458
|
```
|
|
351
459
|
|
|
352
460
|
### Section Backgrounds
|
|
353
461
|
|
|
354
|
-
Set `background` in frontmatter — the runtime renders it automatically
|
|
462
|
+
Set `background` in frontmatter — the runtime renders it automatically:
|
|
355
463
|
|
|
356
464
|
```yaml
|
|
357
|
-
background: /images/hero.jpg # Image
|
|
358
|
-
background: /videos/hero.mp4 # Video
|
|
359
|
-
background: linear-gradient(135deg, #667eea, #764ba2) #
|
|
465
|
+
background: /images/hero.jpg # Image
|
|
466
|
+
background: /videos/hero.mp4 # Video
|
|
467
|
+
background: linear-gradient(135deg, #667eea, #764ba2) # Gradient
|
|
360
468
|
background: '#1a1a2e' # Color (hex — quote in YAML)
|
|
361
|
-
background: var(--primary-900) #
|
|
469
|
+
background: var(--primary-900) # CSS variable
|
|
362
470
|
```
|
|
363
471
|
|
|
364
|
-
|
|
472
|
+
Object form for more control:
|
|
365
473
|
|
|
366
474
|
```yaml
|
|
367
475
|
background:
|
|
@@ -369,8 +477,6 @@ background:
|
|
|
369
477
|
overlay: { enabled: true, type: dark, opacity: 0.5 }
|
|
370
478
|
```
|
|
371
479
|
|
|
372
|
-
Overlay shorthand — `overlay: 0.5` is equivalent to `{ enabled: true, type: dark, opacity: 0.5 }`.
|
|
373
|
-
|
|
374
480
|
Components that render their own background declare `background: 'self'` in `meta.js`.
|
|
375
481
|
|
|
376
482
|
### Page Organization
|
|
@@ -384,7 +490,7 @@ site/layout/
|
|
|
384
490
|
site/pages/
|
|
385
491
|
└── home/
|
|
386
492
|
├── page.yml # title, description, order
|
|
387
|
-
├── hero.md # Single section
|
|
493
|
+
├── hero.md # Single section
|
|
388
494
|
└── (or for multi-section pages:)
|
|
389
495
|
├── 1-hero.md # Numeric prefix sets order
|
|
390
496
|
├── 2-features.md
|
|
@@ -393,9 +499,7 @@ site/pages/
|
|
|
393
499
|
|
|
394
500
|
Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`.
|
|
395
501
|
|
|
396
|
-
**Ignored
|
|
397
|
-
- `README.md` — repo documentation, not site content
|
|
398
|
-
- `_*.md` or `_*/` — drafts and private content (e.g., `_drafts/`, `_old-hero.md`)
|
|
502
|
+
**Ignored:** `README.md` (repo docs), `_*.md` or `_*/` (drafts/private).
|
|
399
503
|
|
|
400
504
|
**page.yml:**
|
|
401
505
|
```yaml
|
|
@@ -413,97 +517,73 @@ pages: [home, about, ...] # Order pages (... = rest, first = homepage)
|
|
|
413
517
|
pages: [home, about] # Strict: only listed pages in nav
|
|
414
518
|
```
|
|
415
519
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
### Section Nesting (Child Sections)
|
|
520
|
+
### Lists as Navigation Menus
|
|
419
521
|
|
|
420
|
-
|
|
522
|
+
Markdown lists model nav, menus, and grouped links. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
|
|
421
523
|
|
|
524
|
+
**Header nav:**
|
|
525
|
+
```markdown
|
|
526
|
+
-  [Home](/)
|
|
527
|
+
-  [Docs](/docs)
|
|
528
|
+
-  [Contact](/contact)
|
|
422
529
|
```
|
|
423
|
-
|
|
424
|
-
├── page.yml
|
|
425
|
-
├── 1-hero.md
|
|
426
|
-
├── 2-features.md # Parent section (type: Grid)
|
|
427
|
-
├── 3-cta.md
|
|
428
|
-
├── @card-a.md # Child of features (@ = not top-level)
|
|
429
|
-
├── @card-b.md
|
|
430
|
-
└── @card-c.md
|
|
431
|
-
```
|
|
530
|
+
Access: `content.lists[0]` — each item has `item.links[0]` and `item.icons[0]`.
|
|
432
531
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
532
|
+
**Footer columns:**
|
|
533
|
+
```markdown
|
|
534
|
+
- Product
|
|
535
|
+
- [Features](/features)
|
|
536
|
+
- [Pricing](/pricing)
|
|
537
|
+
- Company
|
|
538
|
+
- [About](/about)
|
|
539
|
+
- [Careers](/careers)
|
|
437
540
|
```
|
|
541
|
+
Access: `content.lists[0]` — `group.paragraphs[0]` (label), `group.lists[0]` (sub-items with `subItem.links[0]`).
|
|
438
542
|
|
|
439
|
-
|
|
440
|
-
- `@`-prefixed files are excluded from the top-level section list
|
|
441
|
-
- `nest:` declares parent-child relationships (parent name → array of child names)
|
|
442
|
-
- Child files **must** use the `@` prefix — the filename and YAML must agree
|
|
443
|
-
- `@@` prefix signals deeper nesting (e.g., `@@sub-item.md` for grandchildren)
|
|
444
|
-
- `nest:` is flat — each key is a parent: `nest: { features: [a, b], a: [sub-1] }`
|
|
445
|
-
- Children are ordered by their position in the `nest:` array
|
|
446
|
-
- Orphaned `@` files (no parent in `nest:`) appear at top-level with a warning
|
|
447
|
-
|
|
448
|
-
Components receive children via `block.childBlocks`. Use `ChildBlocks` from kit to render them — the runtime handles component resolution:
|
|
543
|
+
Render list item text with Kit components — list items contain HTML strings, not plain text:
|
|
449
544
|
|
|
450
545
|
```jsx
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
546
|
+
content.lists[0]?.map((group, i) => (
|
|
547
|
+
<div key={i}>
|
|
548
|
+
<Span text={group.paragraphs[0]} className="font-semibold text-heading" />
|
|
549
|
+
<ul>
|
|
550
|
+
{group.lists[0]?.map((subItem, j) => (
|
|
551
|
+
<li key={j}><Link to={subItem.links[0]?.href}>{subItem.links[0]?.label}</Link></li>
|
|
552
|
+
))}
|
|
553
|
+
</ul>
|
|
554
|
+
</div>
|
|
555
|
+
))
|
|
460
556
|
```
|
|
461
557
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
Section nesting and insets together give content authors significant layout power without requiring new components. A single Grid section type composes *any* combination of children — each child is its own section type with its own content:
|
|
465
|
-
|
|
466
|
-
```
|
|
467
|
-
pages/home/
|
|
468
|
-
├── page.yml
|
|
469
|
-
├── 1-hero.md
|
|
470
|
-
├── 2-highlights.md # type: Grid, columns: 3
|
|
471
|
-
├── 3-cta.md
|
|
472
|
-
├── @stats.md # type: StatCard — numbers and labels
|
|
473
|
-
├── @testimonial.md # type: Testimonial — quote with attribution
|
|
474
|
-
└── @demo.md # type: SplitContent — text +  inset
|
|
475
|
-
```
|
|
558
|
+
**For richer navigation with icons, descriptions, or hierarchy**, use `yaml:nav` tagged blocks:
|
|
476
559
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
560
|
+
````markdown
|
|
561
|
+
```yaml:nav
|
|
562
|
+
- label: Dashboard
|
|
563
|
+
href: /
|
|
564
|
+
icon: lu:layout-grid
|
|
565
|
+
- label: Docs
|
|
566
|
+
href: /docs
|
|
567
|
+
icon: lu:book-open
|
|
568
|
+
children:
|
|
569
|
+
- label: Getting Started
|
|
570
|
+
href: /docs/quickstart
|
|
480
571
|
```
|
|
572
|
+
````
|
|
481
573
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
This is functional composition applied to content: small, focused section types that combine into richer layouts. The developer builds reusable pieces (Grid, StatCard, Testimonial, SplitContent); the content author composes them. Adding a fourth card means creating one `@`-prefixed file and adding its name to the `nest:` array.
|
|
485
|
-
|
|
486
|
-
### When to use which pattern
|
|
487
|
-
|
|
488
|
-
| Pattern | Authoring | Use when |
|
|
489
|
-
|---------|-----------|----------|
|
|
490
|
-
| **Items** (`content.items`) | Heading groups in one `.md` file | Repeating content within one section (cards, FAQ entries) |
|
|
491
|
-
| **Insets** (`block.insets`) | `` in markdown | Embedding a self-contained visual (chart, diagram, widget) |
|
|
492
|
-
| **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children with rich authored content (testimonials, carousel slides) |
|
|
493
|
-
|
|
494
|
-
Does the content author write content *inside* the nested component? **Yes** → child sections. **No** (self-contained, driven by params/data) → insets. Repeating groups within one section → items. These patterns compose: a child section can contain insets, and items work inside children.
|
|
574
|
+
Access: `content.data?.nav` — array of `{ label, href, icon, text, children, target }`. Components can support both modes: use `content.data?.nav` when provided, fall back to `website.getPageHierarchy()` for automatic nav. See `reference/navigation-patterns.md` for the full pattern.
|
|
495
575
|
|
|
496
|
-
|
|
576
|
+
---
|
|
497
577
|
|
|
498
578
|
## Semantic Theming
|
|
499
579
|
|
|
500
|
-
|
|
580
|
+
Components use **semantic CSS tokens** instead of hardcoded colors. The runtime applies a context class (`context-light`, `context-medium`, `context-dark`) to each section based on `theme:` frontmatter. The `theme` value is also available as `params.theme` — useful when a component needs conditional logic beyond CSS tokens (e.g., switching between a light and dark logo).
|
|
501
581
|
|
|
502
582
|
```jsx
|
|
503
|
-
// ❌ Hardcoded — breaks in dark context
|
|
583
|
+
// ❌ Hardcoded — breaks in dark context
|
|
504
584
|
<h2 className="text-slate-900">...</h2>
|
|
505
585
|
|
|
506
|
-
// ✅ Semantic — adapts to any context and brand
|
|
586
|
+
// ✅ Semantic — adapts to any context and brand
|
|
507
587
|
<h2 className="text-heading">...</h2>
|
|
508
588
|
```
|
|
509
589
|
|
|
@@ -519,48 +599,9 @@ CCA separates theme from code. Components use **semantic CSS tokens** instead of
|
|
|
519
599
|
| `bg-muted` | Hover states, zebra rows |
|
|
520
600
|
| `border-border` | Borders |
|
|
521
601
|
| `text-link` | Link color |
|
|
522
|
-
| `bg-primary` | Primary
|
|
523
|
-
| `text-
|
|
524
|
-
| `
|
|
525
|
-
| `border-primary-border` | Primary border (transparent by default) |
|
|
526
|
-
| `bg-secondary` | Secondary action background |
|
|
527
|
-
| `text-secondary-foreground` | Text on secondary background |
|
|
528
|
-
| `hover:bg-secondary-hover` | Secondary hover state |
|
|
529
|
-
| `border-secondary-border` | Secondary border |
|
|
530
|
-
| `text-success` / `bg-success-subtle` | Status: success |
|
|
531
|
-
| `text-error` / `bg-error-subtle` | Status: error |
|
|
532
|
-
| `text-warning` / `bg-warning-subtle` | Status: warning |
|
|
533
|
-
| `text-info` / `bg-info-subtle` | Status: info |
|
|
534
|
-
|
|
535
|
-
### What the runtime handles (don't write this yourself)
|
|
536
|
-
|
|
537
|
-
The runtime does significant work that other frameworks push onto components. Understanding this prevents writing unnecessary code:
|
|
538
|
-
|
|
539
|
-
1. **Section backgrounds** — The runtime renders image, video, gradient, color, and overlay backgrounds from frontmatter. Components never set their own section background.
|
|
540
|
-
2. **Context classes** — The runtime wraps every section in `<section class="context-{theme}">`, which auto-applies `background-color: var(--section)` and sets all token values.
|
|
541
|
-
3. **Token resolution** — All 24+ semantic tokens resolve automatically per context. A component using `text-heading` gets dark text in light context, white text in dark context — zero conditional logic.
|
|
542
|
-
4. **Colored section backgrounds** — Content authors create tinted sections via frontmatter, not component code:
|
|
543
|
-
```yaml
|
|
544
|
-
---
|
|
545
|
-
type: Features
|
|
546
|
-
theme: light
|
|
547
|
-
background:
|
|
548
|
-
color: var(--primary-50) # Light blue tint with light-context tokens
|
|
549
|
-
---
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
**What components should NOT contain:**
|
|
553
|
-
|
|
554
|
-
| Don't write | Why |
|
|
555
|
-
|-------------|-----|
|
|
556
|
-
| `bg-white` or `bg-gray-900` on section wrapper | Engine applies `bg-section` via context class |
|
|
557
|
-
| `const themes = { light: {...}, dark: {...} }` | Context system replaces theme maps entirely |
|
|
558
|
-
| `isDark ? 'text-white' : 'text-gray-900'` | Just write `text-heading` — it adapts |
|
|
559
|
-
| Background rendering code | Declare `background:` in frontmatter instead |
|
|
560
|
-
| Color constants / tokens files | Colors come from `theme.yml` |
|
|
561
|
-
| Parallel color system (`--ink`, `--paper`) that duplicates what tokens already provide | Map source color roles to `theme.yml` colors/neutral. The build generates `--primary-50` through `--primary-950`, `--neutral-50` through `--neutral-950`, etc. Use palette shades directly (`var(--primary-300)`) for specific tones. Additive design classes that BUILD ON tokens are fine — a parallel system that REPLACES them bypasses context adaptation. |
|
|
562
|
-
|
|
563
|
-
**What to hardcode** (not semantic — same in every context): layout (`grid`, `flex`, `max-w-6xl`), spacing (`p-6`, `gap-8`), typography scale (`text-3xl`, `font-bold`), animations, border-radius.
|
|
602
|
+
| `bg-primary` / `text-primary-foreground` / `hover:bg-primary-hover` | Primary actions |
|
|
603
|
+
| `bg-secondary` / `text-secondary-foreground` / `hover:bg-secondary-hover` | Secondary actions |
|
|
604
|
+
| `text-success` / `text-error` / `text-warning` / `text-info` | Status colors |
|
|
564
605
|
|
|
565
606
|
**Content authors control context** in frontmatter:
|
|
566
607
|
|
|
@@ -571,34 +612,19 @@ theme: dark ← sets context-dark, all tokens resolve to dark values
|
|
|
571
612
|
---
|
|
572
613
|
```
|
|
573
614
|
|
|
574
|
-
Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm
|
|
575
|
-
|
|
576
|
-
```markdown
|
|
577
|
-
<!-- 1-hero.md -->
|
|
578
|
-
theme: dark
|
|
579
|
-
|
|
580
|
-
<!-- 2-features.md -->
|
|
581
|
-
(no theme — defaults to light)
|
|
615
|
+
Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm.
|
|
582
616
|
|
|
583
|
-
|
|
584
|
-
theme: medium
|
|
585
|
-
|
|
586
|
-
<!-- 4-cta.md -->
|
|
587
|
-
theme: dark
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**Per-section token overrides** — the object form lets authors fine-tune individual tokens for a specific section:
|
|
617
|
+
**But the three presets aren't the limit.** The object form gives fine-grained control per section:
|
|
591
618
|
|
|
592
619
|
```yaml
|
|
593
620
|
theme:
|
|
594
621
|
mode: light
|
|
595
|
-
|
|
596
|
-
|
|
622
|
+
section: neutral-100 # Subtle off-white surface
|
|
623
|
+
card: neutral-50 # Cards lighter than surface
|
|
624
|
+
primary: neutral-900 # Dark buttons instead of brand color
|
|
597
625
|
```
|
|
598
626
|
|
|
599
|
-
Any semantic token
|
|
600
|
-
|
|
601
|
-
**Site controls the palette** in `theme.yml`. The same foundation looks different across sites because tokens resolve from the site's color configuration, not from component code.
|
|
627
|
+
Any semantic token can be overridden. And `background:` accepts CSS variables and hex colors, so authors can alternate between `var(--neutral-50)`, `var(--neutral-100)`, and `var(--primary-50)` surfaces — all without component code. If a source design uses subtle surface variations (e.g., `--surface-base` vs `--surface-sunken`), map those to specific backgrounds or token overrides in frontmatter, not to component code.
|
|
602
628
|
|
|
603
629
|
### theme.yml
|
|
604
630
|
|
|
@@ -621,98 +647,70 @@ fonts:
|
|
|
621
647
|
body: "'Inter', system-ui, sans-serif"
|
|
622
648
|
|
|
623
649
|
inline:
|
|
624
|
-
|
|
650
|
+
accent:
|
|
625
651
|
color: var(--link)
|
|
626
652
|
font-weight: '600'
|
|
627
653
|
|
|
628
|
-
vars:
|
|
654
|
+
vars:
|
|
629
655
|
header-height: 5rem
|
|
630
656
|
```
|
|
631
657
|
|
|
632
|
-
Each color generates 11 OKLCH shades (50–950).
|
|
658
|
+
Each color generates 11 OKLCH shades (50–950). `neutral` uses a named preset rather than hex. Shade 500 = your exact input color. Context override keys match token names: `section:` not `bg:`, `primary:` not `btn-primary-bg:`.
|
|
633
659
|
|
|
634
660
|
### How colors reach components
|
|
635
661
|
|
|
636
|
-
Your hex
|
|
637
|
-
|
|
638
|
-
**Shade 500 = your exact input color.** The build generates lighter shades (50–400) above it and darker shades (600–950) below it, redistributing lightness proportionally to maintain a smooth scale. Set `exactMatch: false` on a color to opt out and use fixed lightness values instead.
|
|
662
|
+
Your hex → 11 shades (50–950) → semantic tokens → components.
|
|
639
663
|
|
|
640
|
-
Semantic tokens map shades to roles.
|
|
664
|
+
Semantic tokens map shades to roles. In light/medium: `--primary` uses shade 600, `--link` uses 600, `--ring` uses 500. In dark: `--primary` uses 500, `--link` uses 400.
|
|
641
665
|
|
|
642
|
-
|
|
643
|
-
|-------|-------|---------|
|
|
644
|
-
| `--primary` | 600 | Button background |
|
|
645
|
-
| `--primary-hover` | 700 | Button hover |
|
|
646
|
-
| `--link` | 600 | Link color |
|
|
647
|
-
| `--ring` | 500 | Focus ring |
|
|
648
|
-
|
|
649
|
-
In dark contexts, `--primary` uses shade 500 and `--link` uses shade 400.
|
|
650
|
-
|
|
651
|
-
**Buttons and links use shade 600 — darker than your input.** This is an accessibility choice: shade 600 provides better contrast with white button text. For medium-bright brand colors like orange, buttons will be noticeably darker than the brand color.
|
|
652
|
-
|
|
653
|
-
**Recipe — brand-exact buttons:**
|
|
666
|
+
**Buttons use shade 600 — darker than your input color.** This is an accessibility choice for contrast with white text. For brand-exact buttons:
|
|
654
667
|
|
|
655
668
|
```yaml
|
|
656
669
|
colors:
|
|
657
670
|
primary: "#E35D25"
|
|
658
|
-
|
|
659
671
|
contexts:
|
|
660
672
|
light:
|
|
661
673
|
primary: primary-500 # Your exact color on buttons
|
|
662
|
-
primary-hover: primary-600
|
|
674
|
+
primary-hover: primary-600
|
|
663
675
|
```
|
|
664
676
|
|
|
665
|
-
> **Contrast warning:** Bright brand colors (orange, yellow, light green) at shade 500 may not meet WCAG contrast (4.5:1) with white foreground text. Test buttons for readability — if contrast is insufficient, keep the default shade 600 mapping
|
|
677
|
+
> **Contrast warning:** Bright brand colors (orange, yellow, light green) at shade 500 may not meet WCAG contrast (4.5:1) with white foreground text. Test buttons for readability — if contrast is insufficient, keep the default shade 600 mapping.
|
|
666
678
|
|
|
667
679
|
### Foundation variables
|
|
668
680
|
|
|
669
|
-
Foundations declare customizable layout
|
|
681
|
+
Foundations declare customizable layout values in `foundation.js`:
|
|
670
682
|
|
|
671
683
|
```js
|
|
672
684
|
export const vars = {
|
|
673
685
|
'header-height': { default: '4rem', description: 'Fixed header height' },
|
|
674
686
|
'max-content-width': { default: '80rem', description: 'Maximum content width' },
|
|
675
|
-
'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical padding
|
|
687
|
+
'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical section padding' },
|
|
676
688
|
}
|
|
677
689
|
```
|
|
678
690
|
|
|
679
|
-
Sites override
|
|
680
|
-
|
|
681
|
-
The `section-padding-y` default uses `clamp()` for fluid spacing — tighter on mobile, more breathing room on large screens. Use this variable for consistent section spacing instead of hardcoding padding in each component. Sites can override to a fixed value (`section-padding-y: 3rem`) or a different clamp in `theme.yml`.
|
|
682
|
-
|
|
683
|
-
**When to break the rules:** Header/footer components that float over content may need direct color logic (reading the first section's theme). Decorative elements with fixed branding (logos) use literal colors.
|
|
691
|
+
Sites override in `theme.yml` under `vars:`. Components use: `py-[var(--section-padding-y)]`, `h-[var(--header-height)]`.
|
|
684
692
|
|
|
685
693
|
### Design richness beyond tokens
|
|
686
694
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
The token set is deliberately small (24 tokens). It covers the dimensions that change per context. Everything that stays constant across contexts — border weights, shadow depth, radius scales, gradient angles, accent borders, glassmorphism, elevation layers — belongs in your foundation's `styles.css` or component code.
|
|
690
|
-
|
|
691
|
-
**Don't flatten a rich design to fit the token set.** If a source design has 4 border tones, create them:
|
|
695
|
+
Tokens handle context adaptation — the hard problem. **They are a floor, not a ceiling.** A great foundation adds design vocabulary on top:
|
|
692
696
|
|
|
693
697
|
```css
|
|
694
698
|
/* foundation/src/styles.css */
|
|
695
699
|
.border-subtle { border-color: color-mix(in oklch, var(--border), transparent 50%); }
|
|
696
700
|
.border-strong { border-color: color-mix(in oklch, var(--border), var(--heading) 30%); }
|
|
697
|
-
.
|
|
701
|
+
.text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
|
|
698
702
|
```
|
|
699
703
|
|
|
700
|
-
These compose with
|
|
704
|
+
These compose with tokens — they adapt per context because they reference token variables. But they add nuance the 24-token set doesn't provide. Use palette shades directly (`var(--primary-300)`, `bg-neutral-200`) for fine-grained color control.
|
|
701
705
|
|
|
702
|
-
**The priority:** Design quality > portability > configurability.
|
|
706
|
+
**The priority:** Design quality > portability > configurability. A beautiful foundation for one site is more valuable than a generic one that looks flat.
|
|
703
707
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
```css
|
|
707
|
-
/* foundation/src/styles.css */
|
|
708
|
-
.text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
|
|
709
|
-
.text-disabled { color: color-mix(in oklch, var(--subtle), transparent 40%); }
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
**When migrating from an existing design**, map every visual detail — not just the ones that have a semantic token. Shadow systems, border hierarchies, custom hover effects, accent tints: create CSS classes or Tailwind utilities in `styles.css` for anything the original has that tokens don't cover. Use palette shades directly (`var(--primary-300)`, `bg-neutral-200`) for fine-grained color control beyond the semantic tokens.
|
|
708
|
+
---
|
|
713
709
|
|
|
714
710
|
## Component Development
|
|
715
711
|
|
|
712
|
+
You're not building pages — you're building a **system** of section types that content authors compose into pages. Name by purpose, not content: `Testimonial` not `WhatClientsSay`, `SplitContent` not `AboutSection`. Expect consolidation: a React site with 30+ components typically maps to 8–15 Uniweb section types.
|
|
713
|
+
|
|
716
714
|
### Props Interface
|
|
717
715
|
|
|
718
716
|
```jsx
|
|
@@ -723,9 +721,25 @@ function MyComponent({ content, params, block }) {
|
|
|
723
721
|
}
|
|
724
722
|
```
|
|
725
723
|
|
|
724
|
+
All non-reserved frontmatter fields become `params`. Reserved: `type`, `preset`, `input`, `data`, `id`, `background`, `theme`. Everything else flows to the component.
|
|
725
|
+
|
|
726
|
+
### block properties
|
|
727
|
+
|
|
728
|
+
| Property | Type | Description |
|
|
729
|
+
|----------|------|-------------|
|
|
730
|
+
| `block.page` | Page | Parent page |
|
|
731
|
+
| `block.website` | Website | Site-level data and navigation |
|
|
732
|
+
| `block.type` | string | Component type name |
|
|
733
|
+
| `block.childBlocks` | Block[] | File-based child sections |
|
|
734
|
+
| `block.insets` | Block[] | Inline `@Component` references |
|
|
735
|
+
| `block.getInset(refId)` | Block | Lookup inset by refId |
|
|
736
|
+
| `block.properties` | object | Raw frontmatter |
|
|
737
|
+
| `block.themeName` | string | `"light"`, `"medium"`, `"dark"` |
|
|
738
|
+
| `block.stableId` | string | Stable ID from filename or `id:` |
|
|
739
|
+
|
|
726
740
|
### Section Wrapper
|
|
727
741
|
|
|
728
|
-
The runtime wraps every section
|
|
742
|
+
The runtime wraps every section in `<section>` with context class and background. Customize with static properties:
|
|
729
743
|
|
|
730
744
|
```jsx
|
|
731
745
|
function Hero({ content, params }) {
|
|
@@ -736,29 +750,80 @@ function Hero({ content, params }) {
|
|
|
736
750
|
)
|
|
737
751
|
}
|
|
738
752
|
|
|
739
|
-
Hero.className = 'pt-32 md:pt-48' // Override spacing
|
|
740
|
-
Hero.as = 'div' // Change wrapper element
|
|
753
|
+
Hero.className = 'pt-32 md:pt-48' // Override spacing
|
|
754
|
+
Hero.as = 'div' // Change wrapper element
|
|
741
755
|
|
|
742
756
|
export default Hero
|
|
743
757
|
```
|
|
744
758
|
|
|
745
|
-
- `Component.className` — adds classes to the runtime
|
|
746
|
-
- `Component.as` — changes
|
|
759
|
+
- `Component.className` — adds classes to the runtime wrapper. Section-level spacing, borders, overflow.
|
|
760
|
+
- `Component.as` — changes wrapper element: `'nav'` for headers, `'footer'` for footers.
|
|
747
761
|
|
|
748
|
-
**Layout components**
|
|
762
|
+
**Layout components** typically need `p-0` to suppress default padding:
|
|
749
763
|
|
|
750
764
|
```jsx
|
|
751
|
-
function Header({ content, block }) { /* ... */ }
|
|
752
765
|
Header.className = 'p-0'
|
|
753
766
|
Header.as = 'header'
|
|
754
|
-
export default Header
|
|
755
767
|
```
|
|
756
768
|
|
|
769
|
+
### Rendering Content with Kit
|
|
770
|
+
|
|
771
|
+
Content fields are **HTML strings** — they contain `<strong>`, `<em>`, `<a>` from markdown. Never render them with raw `{content.title}` in JSX — that shows HTML tags as visible text. Use Kit components:
|
|
772
|
+
|
|
773
|
+
**Extracted fields** (most common — custom layout with content from markdown):
|
|
774
|
+
|
|
775
|
+
```jsx
|
|
776
|
+
import { H1, H2, P, Span } from '@uniweb/kit'
|
|
777
|
+
|
|
778
|
+
<H1 text={content.title} className="text-heading text-5xl font-bold" />
|
|
779
|
+
<P text={content.paragraphs} className="text-body" />
|
|
780
|
+
<Span text={listItem.paragraphs[0]} className="text-subtle" />
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
These render their own HTML tag — don't wrap: `<H2 text={...} />` not `<h2><H2 text={...} /></h2>`.
|
|
784
|
+
|
|
785
|
+
**Full content rendering** (article/docs sections where the author controls the flow):
|
|
786
|
+
|
|
787
|
+
```jsx
|
|
788
|
+
import { Section, Render } from '@uniweb/kit'
|
|
789
|
+
|
|
790
|
+
<Section block={block} width="lg" padding="md" />
|
|
791
|
+
<Render content={block.parsedContent} block={block} />
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**Visuals:**
|
|
795
|
+
|
|
796
|
+
```jsx
|
|
797
|
+
import { Visual } from '@uniweb/kit'
|
|
798
|
+
|
|
799
|
+
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="rounded-2xl" />
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### Kit API by Use Case
|
|
803
|
+
|
|
804
|
+
**Rendering text:** `H1`–`H6`, `P`, `Span`, `Div`, `Text` (with `as` prop)
|
|
805
|
+
|
|
806
|
+
**Rendering content:** `Section` (Render + prose + layout), `Render` (ProseMirror → React), `ChildBlocks` (render child sections)
|
|
807
|
+
|
|
808
|
+
**Rendering media:** `Visual` (first non-empty: inset/video/image), `Image`, `Media`, `Icon`
|
|
809
|
+
|
|
810
|
+
**Navigation and routing:** `Link` (`to`/`href`, `to="page:about"` for page ID resolution, auto `target="_blank"` for external, `reload` for full page reload), `useActiveRoute()`, `useWebsite()`, `useRouting()`
|
|
811
|
+
|
|
812
|
+
**Header and layout:** `useScrolled(threshold)`, `useMobileMenu()`, `useAppearance()` (light/dark mode)
|
|
813
|
+
|
|
814
|
+
**Layout helpers:** `useGridLayout(columns, { gap })`, `useAccordion({ multiple, defaultOpen })`, `useTheme(name)`
|
|
815
|
+
|
|
816
|
+
**Data and theming:** `useThemeData()` (programmatic color access), `useColorContext(block)`
|
|
817
|
+
|
|
818
|
+
**Utilities:** `cn()` (Tailwind class merge), `Link`, `Image`, `Asset`, `SafeHtml`, `SocialIcon`, `filterSocialLinks(links)`, `getSocialPlatform(url)`
|
|
819
|
+
|
|
820
|
+
**Other styled:** `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
821
|
+
|
|
757
822
|
### Content Patterns for Header and Footer
|
|
758
823
|
|
|
759
|
-
Header and Footer
|
|
824
|
+
Header and Footer combine several content categories. Use different parts of the content shape for each role:
|
|
760
825
|
|
|
761
|
-
**Header** — title for logo, list for nav
|
|
826
|
+
**Header** — title for logo, list for nav, standalone link for CTA:
|
|
762
827
|
|
|
763
828
|
````markdown
|
|
764
829
|
---
|
|
@@ -781,15 +846,14 @@ version: v2.1.0
|
|
|
781
846
|
|
|
782
847
|
```jsx
|
|
783
848
|
function Header({ content, block }) {
|
|
784
|
-
const logo = content.title
|
|
785
|
-
const navItems = content.lists[0] || []
|
|
786
|
-
const cta = content.links[0]
|
|
787
|
-
const config = content.data?.config
|
|
788
|
-
// ...
|
|
849
|
+
const logo = content.title
|
|
850
|
+
const navItems = content.lists[0] || []
|
|
851
|
+
const cta = content.links[0]
|
|
852
|
+
const config = content.data?.config
|
|
789
853
|
}
|
|
790
854
|
```
|
|
791
855
|
|
|
792
|
-
**Footer** — paragraph for tagline, nested list for
|
|
856
|
+
**Footer** — paragraph for tagline, nested list for columns, YAML for legal:
|
|
793
857
|
|
|
794
858
|
````markdown
|
|
795
859
|
---
|
|
@@ -804,9 +868,6 @@ Build something great.
|
|
|
804
868
|
- Developers
|
|
805
869
|
- [Docs](/docs)
|
|
806
870
|
- [GitHub](https://github.com/acme){target=_blank}
|
|
807
|
-
- Community
|
|
808
|
-
- [Discord](#)
|
|
809
|
-
- [Blog](/blog)
|
|
810
871
|
|
|
811
872
|
```yaml:legal
|
|
812
873
|
copyright: © 2025 Acme Inc
|
|
@@ -814,12 +875,11 @@ copyright: © 2025 Acme Inc
|
|
|
814
875
|
````
|
|
815
876
|
|
|
816
877
|
```jsx
|
|
817
|
-
function Footer({ content
|
|
818
|
-
const tagline = content.paragraphs[0]
|
|
819
|
-
const columns = content.lists[0] || []
|
|
820
|
-
const legal = content.data?.legal
|
|
878
|
+
function Footer({ content }) {
|
|
879
|
+
const tagline = content.paragraphs[0]
|
|
880
|
+
const columns = content.lists[0] || []
|
|
881
|
+
const legal = content.data?.legal
|
|
821
882
|
|
|
822
|
-
// Each column: group.paragraphs[0] = label, group.lists[0] = links
|
|
823
883
|
columns.map(group => ({
|
|
824
884
|
label: group.paragraphs[0],
|
|
825
885
|
links: group.lists[0]?.map(item => item.links[0])
|
|
@@ -834,11 +894,11 @@ export default {
|
|
|
834
894
|
title: 'Feature Grid',
|
|
835
895
|
description: 'Grid of feature cards with icons',
|
|
836
896
|
category: 'marketing',
|
|
837
|
-
// hidden: true, //
|
|
897
|
+
// hidden: true, // Exclude from export
|
|
838
898
|
// background: 'self', // Component renders its own background
|
|
839
|
-
// inset: true, // Available for @ComponentName
|
|
840
|
-
// visuals: 1, // Expects 1 visual
|
|
841
|
-
// children: true, // Accepts
|
|
899
|
+
// inset: true, // Available for @ComponentName in markdown
|
|
900
|
+
// visuals: 1, // Expects 1 visual
|
|
901
|
+
// children: true, // Accepts child sections
|
|
842
902
|
|
|
843
903
|
content: {
|
|
844
904
|
title: 'Section heading',
|
|
@@ -856,237 +916,131 @@ export default {
|
|
|
856
916
|
compact: { label: 'Compact', params: { columns: 4 } },
|
|
857
917
|
},
|
|
858
918
|
|
|
859
|
-
// Static capabilities for cross-block coordination
|
|
860
919
|
context: {
|
|
861
|
-
allowTranslucentTop: true,
|
|
920
|
+
allowTranslucentTop: true,
|
|
862
921
|
},
|
|
863
922
|
}
|
|
864
923
|
```
|
|
865
924
|
|
|
866
925
|
All defaults belong in `meta.js`, not inline in component code.
|
|
867
926
|
|
|
868
|
-
###
|
|
927
|
+
### The Front Desk Pattern
|
|
869
928
|
|
|
870
|
-
|
|
929
|
+
Section types naturally use params to adjust their own rendering — `variant: flipped` reverses a flex direction, `columns: 3` sets a grid. That's not a pattern, that's the baseline.
|
|
871
930
|
|
|
872
|
-
**
|
|
931
|
+
The **Front Desk pattern** is when a section type does virtually no rendering itself. It reads the author's params, picks the right helper component from `src/components/`, and translates author-friendly vocabulary into developer-oriented props. The section type is a front desk — it greets the request and routes it to the right specialist:
|
|
873
932
|
|
|
874
933
|
```jsx
|
|
875
|
-
|
|
934
|
+
// sections/Features/index.jsx — the front desk
|
|
935
|
+
import { CardGrid } from '../../components/CardGrid'
|
|
936
|
+
import { CardList } from '../../components/CardList'
|
|
937
|
+
import { ComparisonTable } from '../../components/ComparisonTable'
|
|
876
938
|
|
|
877
|
-
|
|
878
|
-
<P text={content.paragraphs[0]} className="text-body" />
|
|
879
|
-
<P text={content.paragraphs} /> // array → each string becomes its own <p>
|
|
880
|
-
<Span text={listItem.paragraphs[0]} className="text-subtle" />
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
`H1`–`H6`, `P`, `Span`, `Div` are all wrappers around `Text` with a preset tag:
|
|
884
|
-
|
|
885
|
-
```jsx
|
|
886
|
-
<Text text={content.title} as="h2" className="..." /> // explicit tag
|
|
887
|
-
```
|
|
939
|
+
const variants = { grid: CardGrid, list: CardList, comparison: ComparisonTable }
|
|
888
940
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
Don't render content strings with `{content.paragraphs[0]}` in JSX — that shows HTML tags as visible text. Use `<P>`, `<H2>`, `<Span>`, etc. for content strings.
|
|
892
|
-
|
|
893
|
-
**Rendering full content** (`@uniweb/kit`):
|
|
894
|
-
|
|
895
|
-
```jsx
|
|
896
|
-
import { Section, Render } from '@uniweb/kit'
|
|
941
|
+
export default function Features({ content, block, params }) {
|
|
942
|
+
const Layout = variants[params.variant] || CardGrid
|
|
897
943
|
|
|
898
|
-
|
|
899
|
-
<
|
|
944
|
+
return (
|
|
945
|
+
<Layout
|
|
946
|
+
title={content.title}
|
|
947
|
+
subtitle={content.paragraphs[0]}
|
|
948
|
+
items={content.items}
|
|
949
|
+
block={block}
|
|
950
|
+
columns={params.columns}
|
|
951
|
+
showIcons={params.showIcon !== false}
|
|
952
|
+
compact={params.density === 'compact'}
|
|
953
|
+
/>
|
|
954
|
+
)
|
|
955
|
+
}
|
|
900
956
|
```
|
|
901
957
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
```jsx
|
|
913
|
-
<Link to="/about">About</Link> // SPA navigation via React Router
|
|
914
|
-
<Link to="page:about">About</Link> // Resolves page ID to route
|
|
915
|
-
<Link reload href={localeUrl}>ES</Link> // Full page reload, prepends basePath
|
|
916
|
-
// External URLs auto-get target="_blank" and rel="noopener noreferrer"
|
|
958
|
+
```js
|
|
959
|
+
// meta.js — author-friendly language
|
|
960
|
+
export default {
|
|
961
|
+
params: {
|
|
962
|
+
variant: { type: 'select', options: ['grid', 'list', 'comparison'], default: 'grid' },
|
|
963
|
+
columns: { type: 'number', default: 3 },
|
|
964
|
+
showIcon: { type: 'boolean', default: true },
|
|
965
|
+
density: { type: 'select', options: ['default', 'compact'], default: 'default' },
|
|
966
|
+
}
|
|
967
|
+
}
|
|
917
968
|
```
|
|
918
969
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
**Hooks:**
|
|
922
|
-
- `useScrolled(threshold)` → boolean for scroll-based header styling
|
|
923
|
-
- `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
|
|
924
|
-
- `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
|
|
925
|
-
- `useActiveRoute()` → `{ route, rootSegment, isActive(page), isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
926
|
-
- `useGridLayout(columns, { gap })` → responsive grid class string
|
|
927
|
-
- `useTheme(name)` → standardized theme classes
|
|
928
|
-
- `useAppearance()` → `{ scheme, toggle, canToggle, setScheme, schemes }` — light/dark mode control with localStorage persistence
|
|
929
|
-
- `useRouting()` → `{ useLocation, useParams, useNavigate, Link, isRoutingAvailable }` — SSG-safe routing access (returns no-op fallbacks during prerender)
|
|
930
|
-
- `useWebsite()` → `{ website, localize, makeHref, getLanguage, getLanguages, getRoutingComponents }` — primary runtime hook
|
|
931
|
-
- `useThemeData()` → Theme instance for programmatic color access (`getColor(name, shade)`, `getPalette(name)`)
|
|
932
|
-
- `useColorContext(block)` → `'light' | 'medium' | 'dark'` — current section's color context
|
|
970
|
+
The content author writes `variant: comparison` — they don't know or care about `ComparisonTable`. The section type translates `density: compact` into a `compact={true}` prop. `CardGrid`, `CardList`, `ComparisonTable` live in `src/components/` — ordinary React, reusable across multiple section types, testable independently.
|
|
933
971
|
|
|
934
|
-
**
|
|
972
|
+
This is the system-building pattern at its clearest: **section types are the public interface** to your content system (author-friendly names, documented in `meta.js`). **Helper components are the implementation** (developer-friendly APIs, ordinary React props). The section type is the thin translation layer that connects the two worlds.
|
|
935
973
|
|
|
936
974
|
### Foundation Organization
|
|
937
975
|
|
|
938
976
|
```
|
|
939
977
|
foundation/src/
|
|
940
|
-
├── sections/ # Section types (auto-discovered
|
|
941
|
-
│ ├── Hero
|
|
942
|
-
│
|
|
978
|
+
├── sections/ # Section types (auto-discovered)
|
|
979
|
+
│ ├── Hero.jsx # Bare file — no folder needed
|
|
980
|
+
│ ├── Features/ # Folder when you need meta.js
|
|
981
|
+
│ │ ├── index.jsx
|
|
943
982
|
│ │ └── meta.js
|
|
944
|
-
│ └──
|
|
945
|
-
│
|
|
946
|
-
│
|
|
983
|
+
│ └── insets/ # Organizational subdirectory (lowercase)
|
|
984
|
+
│ └── Diagram/
|
|
985
|
+
│ ├── index.jsx
|
|
986
|
+
│ └── meta.js
|
|
947
987
|
├── components/ # Your React components (no meta.js, not selectable)
|
|
948
|
-
│ ├── ui/
|
|
988
|
+
│ ├── ui/
|
|
949
989
|
│ │ └── button.jsx
|
|
950
990
|
│ └── Card.jsx
|
|
951
991
|
└── styles.css
|
|
952
992
|
```
|
|
953
993
|
|
|
954
|
-
|
|
994
|
+
**Discovery:** PascalCase files/folders at root of `sections/` are auto-discovered. Nested levels require `meta.js`. Lowercase directories are organizational only. `hidden: true` excludes a component entirely. Everything outside `sections/` is ordinary React.
|
|
955
995
|
|
|
956
996
|
### Website and Page APIs
|
|
957
997
|
|
|
958
998
|
```jsx
|
|
959
999
|
const { website } = useWebsite()
|
|
1000
|
+
const page = website.activePage
|
|
960
1001
|
|
|
961
1002
|
// Navigation
|
|
962
|
-
|
|
1003
|
+
website.getPageHierarchy({ for: 'header' })
|
|
963
1004
|
// → [{ route, navigableRoute, label, hasContent, children }]
|
|
964
1005
|
|
|
1006
|
+
// Core properties
|
|
1007
|
+
website.name // Site name from site.yml
|
|
1008
|
+
website.basePath // Deployment base path (e.g., '/docs/')
|
|
1009
|
+
|
|
965
1010
|
// Locale
|
|
966
1011
|
website.hasMultipleLocales()
|
|
967
1012
|
website.getLocales() // [{ code, label, isDefault }]
|
|
968
|
-
website.getActiveLocale()
|
|
1013
|
+
website.getActiveLocale()
|
|
969
1014
|
website.getLocaleUrl('es')
|
|
970
1015
|
|
|
971
|
-
// Core properties
|
|
972
|
-
website.name // Site name from site.yml
|
|
973
|
-
website.basePath // Deployment base path (e.g., '/docs/')
|
|
974
|
-
|
|
975
1016
|
// Route detection
|
|
976
1017
|
const { isActive, isActiveOrAncestor } = useActiveRoute()
|
|
977
|
-
isActive(page) // Exact match
|
|
978
|
-
isActiveOrAncestor(page) // Ancestor match (for parent highlighting in nav)
|
|
979
1018
|
|
|
980
|
-
// Appearance
|
|
1019
|
+
// Appearance
|
|
981
1020
|
const { scheme, toggle, canToggle } = useAppearance()
|
|
982
1021
|
|
|
983
1022
|
// Page properties
|
|
984
|
-
page.title
|
|
985
|
-
page.
|
|
986
|
-
page.
|
|
987
|
-
page.isHidden() // Hidden from navigation
|
|
988
|
-
page.showInHeader() // Visible in header nav
|
|
989
|
-
page.showInFooter() // Visible in footer nav
|
|
990
|
-
page.hasChildren() // Has child pages
|
|
991
|
-
page.children // Array of child Page objects
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
### Insets and the Visual Component
|
|
995
|
-
|
|
996
|
-
Components access inline `@` references via `block.insets` (separate from `block.childBlocks`):
|
|
997
|
-
|
|
998
|
-
```jsx
|
|
999
|
-
import { Visual } from '@uniweb/kit'
|
|
1000
|
-
|
|
1001
|
-
// Visual renders the first non-empty candidate: inset > video > image
|
|
1002
|
-
function SplitContent({ content, block }) {
|
|
1003
|
-
return (
|
|
1004
|
-
<div className="flex gap-12">
|
|
1005
|
-
<div className="flex-1">
|
|
1006
|
-
<h2 className="text-heading">{content.title}</h2>
|
|
1007
|
-
</div>
|
|
1008
|
-
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-lg" />
|
|
1009
|
-
</div>
|
|
1010
|
-
)
|
|
1011
|
-
}
|
|
1012
|
-
```
|
|
1013
|
-
|
|
1014
|
-
- `<Visual>` — renders first non-empty candidate from the props you pass (`inset`, `video`, `image`)
|
|
1015
|
-
- `<Render>` / `<Section>` — automatically handles `@Component` references placed in content flow
|
|
1016
|
-
- `block.insets` — array of Block instances from `@` references
|
|
1017
|
-
- `block.getInset(refId)` — lookup by refId (used by sequential renderers)
|
|
1018
|
-
- `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
|
|
1019
|
-
|
|
1020
|
-
**SSG and hooks:** Inset components that use React hooks (useState, useEffect) will trigger prerender warnings during `pnpm build`. This is expected — the SSG pipeline cannot render hooks due to dual React instances in the build. The warnings are informational; the page renders correctly client-side. If you see `"Skipped SSG for /..."` or `"Invalid hook call"`, this is the cause.
|
|
1021
|
-
|
|
1022
|
-
Inset components declare `inset: true` in meta.js. Use `hidden: true` for inset-only components:
|
|
1023
|
-
|
|
1024
|
-
```js
|
|
1025
|
-
// sections/insets/NetworkDiagram/meta.js
|
|
1026
|
-
export default {
|
|
1027
|
-
inset: true,
|
|
1028
|
-
hidden: true,
|
|
1029
|
-
params: { variant: { type: 'select', options: ['full', 'compact'], default: 'full' } },
|
|
1030
|
-
}
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
### Dispatcher Pattern
|
|
1034
|
-
|
|
1035
|
-
One section type with a `variant` param replaces multiple near-duplicates. Instead of `HeroLeft`, `HeroCentered`, `HeroSplit` — one `Hero` with `variant: left | centered | split`:
|
|
1036
|
-
|
|
1037
|
-
```jsx
|
|
1038
|
-
function SplitContent({ content, block, params }) {
|
|
1039
|
-
const flipped = params.variant === 'flipped'
|
|
1040
|
-
return (
|
|
1041
|
-
<div className={`flex gap-16 items-center ${flipped ? 'flex-row-reverse' : ''}`}>
|
|
1042
|
-
<div className="flex-1">
|
|
1043
|
-
{content.pretitle && (
|
|
1044
|
-
<p className="text-xs font-bold uppercase tracking-widest text-subtle mb-4">
|
|
1045
|
-
{content.pretitle}
|
|
1046
|
-
</p>
|
|
1047
|
-
)}
|
|
1048
|
-
<h2 className="text-heading text-3xl font-bold">{content.title}</h2>
|
|
1049
|
-
<p className="text-body mt-4">{content.paragraphs[0]}</p>
|
|
1050
|
-
</div>
|
|
1051
|
-
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-2xl" />
|
|
1052
|
-
</div>
|
|
1053
|
-
)
|
|
1054
|
-
}
|
|
1023
|
+
page.title, page.label, page.route
|
|
1024
|
+
page.isHidden(), page.showInHeader(), page.showInFooter()
|
|
1025
|
+
page.hasChildren(), page.children
|
|
1055
1026
|
```
|
|
1056
1027
|
|
|
1057
|
-
```js
|
|
1058
|
-
// meta.js
|
|
1059
|
-
export default {
|
|
1060
|
-
title: 'Split Content',
|
|
1061
|
-
content: { pretitle: 'Eyebrow label', title: 'Section heading', paragraphs: 'Description' },
|
|
1062
|
-
params: {
|
|
1063
|
-
variant: { type: 'select', options: ['default', 'flipped'], default: 'default' },
|
|
1064
|
-
},
|
|
1065
|
-
}
|
|
1066
|
-
```
|
|
1067
|
-
|
|
1068
|
-
Content authors choose the variant in frontmatter (`variant: flipped`), or the site can alternate it across sections. One component serves every "text + visual" layout on the site.
|
|
1069
|
-
|
|
1070
1028
|
### Cross-Block Communication
|
|
1071
1029
|
|
|
1072
|
-
Components read neighboring blocks for adaptive behavior (e.g., translucent header over hero):
|
|
1073
|
-
|
|
1074
1030
|
```jsx
|
|
1075
1031
|
const firstBody = block.page.getFirstBodyBlockInfo()
|
|
1076
1032
|
// → { type, theme, context: { allowTranslucentTop }, state }
|
|
1077
|
-
|
|
1078
|
-
// context = static (from meta.js), state = dynamic (from useBlockState)
|
|
1079
1033
|
```
|
|
1080
1034
|
|
|
1081
1035
|
### Custom Layouts
|
|
1082
1036
|
|
|
1083
|
-
Layouts live in `foundation/src/layouts/` and are auto-discovered
|
|
1037
|
+
Layouts live in `foundation/src/layouts/` and are auto-discovered:
|
|
1084
1038
|
|
|
1085
1039
|
```js
|
|
1086
1040
|
// foundation/src/foundation.js
|
|
1087
1041
|
export default {
|
|
1088
|
-
name: 'My Template',
|
|
1089
|
-
description: 'A brief description',
|
|
1042
|
+
name: 'My Template',
|
|
1043
|
+
description: 'A brief description',
|
|
1090
1044
|
defaultLayout: 'DocsLayout',
|
|
1091
1045
|
}
|
|
1092
1046
|
```
|
|
@@ -1108,80 +1062,67 @@ export default function DocsLayout({ header, body, footer, left, right, params }
|
|
|
1108
1062
|
}
|
|
1109
1063
|
```
|
|
1110
1064
|
|
|
1111
|
-
Layout
|
|
1065
|
+
**Layout meta.js** declares areas: `{ areas: ['header', 'footer', 'left'] }`. Area names are arbitrary.
|
|
1112
1066
|
|
|
1113
|
-
**Layout
|
|
1114
|
-
|
|
1115
|
-
```js
|
|
1116
|
-
// foundation/src/layouts/DocsLayout/meta.js
|
|
1117
|
-
export default {
|
|
1118
|
-
areas: ['header', 'footer', 'left'],
|
|
1119
|
-
}
|
|
1120
|
-
```
|
|
1121
|
-
|
|
1122
|
-
Area names are arbitrary strings — `header`, `footer`, `left`, `right` are conventional, but a dashboard layout could use `topbar`, `sidebar`, `statusbar`.
|
|
1123
|
-
|
|
1124
|
-
**Site-side layout content** — each layout can have its own section files:
|
|
1067
|
+
**Layout content** — each layout has section files in `site/layout/`:
|
|
1125
1068
|
|
|
1126
1069
|
```
|
|
1127
1070
|
site/layout/
|
|
1128
|
-
├── header.md # Default layout
|
|
1071
|
+
├── header.md # Default layout
|
|
1129
1072
|
├── footer.md
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
├── header.md # Different header for marketing pages
|
|
1073
|
+
└── marketing/ # Named layout sections
|
|
1074
|
+
├── header.md
|
|
1133
1075
|
└── footer.md
|
|
1134
1076
|
```
|
|
1135
1077
|
|
|
1136
|
-
Named subdirectories are self-contained — no inheritance
|
|
1137
|
-
|
|
1138
|
-
**Layout cascade** (first match wins): `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
|
|
1078
|
+
Named subdirectories are self-contained — no inheritance. Layout cascade: `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
|
|
1139
1079
|
|
|
1140
|
-
|
|
1141
|
-
# page.yml — select layout and hide areas
|
|
1142
|
-
layout:
|
|
1143
|
-
name: MarketingLayout
|
|
1144
|
-
hide: [left, right]
|
|
1145
|
-
```
|
|
1080
|
+
---
|
|
1146
1081
|
|
|
1147
1082
|
## Migrating From Other Frameworks
|
|
1148
1083
|
|
|
1149
|
-
Don't port line-by-line. Study the
|
|
1084
|
+
Don't port line-by-line. Study the source, then rebuild from first principles. Other frameworks produce far more components than Uniweb needs — expect consolidation, not 1:1 correspondence.
|
|
1150
1085
|
|
|
1151
|
-
###
|
|
1086
|
+
### The mental model shift
|
|
1152
1087
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1088
|
+
| React / conventional | Uniweb equivalent |
|
|
1089
|
+
|---|---|
|
|
1090
|
+
| Props with typed data | Frontmatter params + `meta.js` |
|
|
1091
|
+
| Component variants via props | `variant` param in frontmatter; Front Desk pattern for complex routing |
|
|
1092
|
+
| Context / ThemeProvider | `theme:` frontmatter + semantic tokens (automatic) |
|
|
1093
|
+
| Wrapper/layout components | Section nesting or custom layouts |
|
|
1094
|
+
| Prop-drilling visuals into containers | Insets — `` rendered via `<Visual>` |
|
|
1095
|
+
| Content in JSX or `.js` data files | Markdown → parser → `content` prop |
|
|
1096
|
+
| CSS color tokens / design systems | `theme.yml` → palette shades + semantic tokens |
|
|
1097
|
+
| `isDark ? ... : ...` conditionals | `text-heading` — context classes handle it |
|
|
1098
|
+
| Per-component backgrounds | `background:` in frontmatter |
|
|
1099
|
+
| Multiple near-identical components | One section type + `variant` param, or Front Desk pattern |
|
|
1100
|
+
| i18n wrapping (`t()` / `<Trans>`) | Locale-specific content directories |
|
|
1162
1101
|
|
|
1163
1102
|
### Migration approach
|
|
1164
1103
|
|
|
1165
|
-
1. **
|
|
1104
|
+
1. **Scaffold the workspace:**
|
|
1166
1105
|
```bash
|
|
1167
1106
|
pnpm create uniweb my-project --template none
|
|
1168
1107
|
```
|
|
1169
1108
|
|
|
1170
|
-
|
|
1109
|
+
2. **Use named layouts** for different page groups — marketing layout for landing pages, docs layout for `/docs/*`.
|
|
1110
|
+
|
|
1111
|
+
3. **Dump legacy components under `src/components/`** — they're not section types. Import from section types during transition.
|
|
1171
1112
|
|
|
1172
|
-
4. **
|
|
1113
|
+
4. **Create section types one at a time.** Migration levels:
|
|
1114
|
+
- **Level 0**: Paste the original as one section type. Routing and dev tooling work immediately.
|
|
1115
|
+
- **Level 1**: Decompose into section types. Consolidate duplicates — use `variant` params or the Front Desk pattern.
|
|
1116
|
+
- **Level 2**: Move content from JSX to markdown. Authors can now edit without code.
|
|
1117
|
+
- **Level 3**: Replace hardcoded colors with semantic tokens. Components work in any context.
|
|
1173
1118
|
|
|
1174
|
-
5. **
|
|
1175
|
-
- **Level 0**: Paste the whole original file as one section type. You get routing and dev tooling immediately.
|
|
1176
|
-
- **Level 1**: Decompose into section types. Name by purpose (`Institutions` → `Testimonial`). Consolidate duplicates via dispatcher pattern.
|
|
1177
|
-
- **Level 2**: Move content from JSX to markdown. Components read from `content` instead of hardcoded strings. Content authors can now edit without touching code.
|
|
1178
|
-
- **Level 3**: Replace hardcoded Tailwind colors with semantic tokens. Components work in any context and any brand.
|
|
1119
|
+
5. **Map source colors to `theme.yml`.** The most common mistake is recreating source colors as CSS custom properties — this bypasses the token system. Instead: primary color → `colors.primary` in theme.yml. Neutral tone → `colors.neutral`. Context needs → `theme:` frontmatter.
|
|
1179
1120
|
|
|
1180
|
-
6. **
|
|
1121
|
+
6. **Name by purpose, not content** — `TheModel` → `SplitContent`, `WorkModes` → `FeatureColumns`.
|
|
1181
1122
|
|
|
1182
|
-
7. **
|
|
1123
|
+
7. **UI helpers → `components/`** — Buttons, badges, cards in `src/components/` (no `meta.js`, not selectable by authors).
|
|
1183
1124
|
|
|
1184
|
-
|
|
1125
|
+
---
|
|
1185
1126
|
|
|
1186
1127
|
## Tailwind CSS v4
|
|
1187
1128
|
|
|
@@ -1189,55 +1130,74 @@ Foundation styles in `foundation/src/styles.css`:
|
|
|
1189
1130
|
|
|
1190
1131
|
```css
|
|
1191
1132
|
@import "tailwindcss";
|
|
1192
|
-
@import "@uniweb/kit/theme-tokens.css";
|
|
1133
|
+
@import "@uniweb/kit/theme-tokens.css";
|
|
1193
1134
|
@source "./sections/**/*.{js,jsx}";
|
|
1194
|
-
@source "./components/**/*.{js,jsx}";
|
|
1135
|
+
@source "./components/**/*.{js,jsx}";
|
|
1195
1136
|
@source "../node_modules/@uniweb/kit/src/**/*.jsx";
|
|
1196
1137
|
|
|
1197
1138
|
@theme {
|
|
1198
|
-
/* Additional custom values — NOT for colors already in theme.yml */
|
|
1199
1139
|
--breakpoint-xs: 30rem;
|
|
1200
1140
|
}
|
|
1201
1141
|
```
|
|
1202
1142
|
|
|
1203
|
-
Semantic
|
|
1204
|
-
|
|
1205
|
-
**Custom CSS is expected alongside Tailwind.** Your foundation's `styles.css` is the design layer — shadow systems, border hierarchies, gradient effects, accent treatments, elevation scales, glassmorphism. If the source design has a visual detail, create a class for it. Tailwind handles layout and spacing; semantic tokens handle context adaptation; `styles.css` handles everything else that makes the design rich and distinctive.
|
|
1143
|
+
Semantic tokens come from `theme-tokens.css` (populated from `theme.yml`). Use `@theme` only for values tokens don't cover. **Custom CSS is expected alongside Tailwind** — shadow systems, border hierarchies, gradients, glassmorphism. Tailwind handles layout; tokens handle context; `styles.css` handles everything else.
|
|
1206
1144
|
|
|
1207
1145
|
## Troubleshooting
|
|
1208
1146
|
|
|
1209
|
-
**"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"
|
|
1147
|
+
**"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"`.
|
|
1148
|
+
|
|
1149
|
+
**Component not appearing** — Verify `meta.js` exists. Check for `hidden: true`. Rebuild: `cd foundation && pnpm build`.
|
|
1210
1150
|
|
|
1211
|
-
**
|
|
1151
|
+
**Styles not applying** — Verify `@source` includes your component paths.
|
|
1212
1152
|
|
|
1213
|
-
**
|
|
1153
|
+
**Prerender warnings about hooks** — Components with useState/useEffect show SSG warnings during build. Expected and harmless.
|
|
1214
1154
|
|
|
1215
|
-
**
|
|
1155
|
+
**Content not appearing as expected?**
|
|
1156
|
+
```bash
|
|
1157
|
+
uniweb inspect pages/home/hero.md # Single section
|
|
1158
|
+
uniweb inspect pages/home/ # Whole page
|
|
1159
|
+
uniweb inspect pages/home/hero.md --raw # ProseMirror AST
|
|
1160
|
+
```
|
|
1216
1161
|
|
|
1217
|
-
|
|
1162
|
+
## Learning from Official Templates
|
|
1218
1163
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1164
|
+
When you're unsure how to implement a pattern — data fetching, i18n, layouts, insets, theming — install an official template as a reference project in your workspace:
|
|
1165
|
+
|
|
1166
|
+
```bash
|
|
1167
|
+
uniweb add project marketing --from marketing
|
|
1168
|
+
pnpm install
|
|
1221
1169
|
```
|
|
1222
1170
|
|
|
1223
|
-
|
|
1171
|
+
This creates `marketing/foundation/` + `marketing/site/` alongside your existing project. You don't need to build or run it — just read the source files to see how working components handle content, params, theming, and data.
|
|
1172
|
+
|
|
1173
|
+
**What to study:**
|
|
1174
|
+
- `{name}/foundation/src/sections/` — components with meta.js (content expectations, params, presets)
|
|
1175
|
+
- `{name}/site/pages/` — real content files showing markdown → component mapping
|
|
1176
|
+
- `{name}/site/theme.yml` + `site.yml` — theming and configuration patterns
|
|
1177
|
+
|
|
1178
|
+
**Available templates:**
|
|
1179
|
+
|
|
1180
|
+
| Template | Demonstrates |
|
|
1181
|
+
|----------|-------------|
|
|
1182
|
+
| `marketing` | Semantic tokens, insets, grids, multi-line headings, inline styling |
|
|
1183
|
+
| `docs` | Sidebar navigation, navigation levels, code highlighting |
|
|
1184
|
+
| `dynamic` | Live API data fetching, loading states, transforms |
|
|
1185
|
+
| `international` | i18n, blog with collections, multi-locale routing |
|
|
1186
|
+
| `store` | Product grid, collections, e-commerce patterns |
|
|
1187
|
+
| `academic` | Publications, team grid, timeline, math |
|
|
1188
|
+
| `extensions` | Multi-foundation architecture, runtime loading |
|
|
1189
|
+
|
|
1190
|
+
You can install multiple templates. Each becomes an independent project in the workspace. To run one in dev: `cd {name}/site && pnpm dev`
|
|
1224
1191
|
|
|
1225
1192
|
## Further Documentation
|
|
1226
1193
|
|
|
1227
|
-
Full
|
|
1194
|
+
Full documentation: **https://github.com/uniweb/docs**
|
|
1228
1195
|
|
|
1229
1196
|
| Section | Path | Topics |
|
|
1230
1197
|
|---------|------|--------|
|
|
1231
|
-
| **Getting Started** | `getting-started/` | What is Uniweb, quickstart
|
|
1232
|
-
| **Authoring** | `authoring/` | Writing content, site setup, collections, theming,
|
|
1233
|
-
| **Development** | `development/` |
|
|
1234
|
-
| **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit
|
|
1235
|
-
|
|
1236
|
-
**Quick access
|
|
1237
|
-
|
|
1238
|
-
Examples:
|
|
1239
|
-
- Content structure details: `reference/content-structure.md`
|
|
1240
|
-
- Component metadata (meta.js): `reference/component-metadata.md`
|
|
1241
|
-
- Kit hooks and components: `reference/kit-reference.md`
|
|
1242
|
-
- Theming tokens: `reference/site-theming.md`
|
|
1243
|
-
- Data fetching patterns: `reference/data-fetching.md`
|
|
1198
|
+
| **Getting Started** | `getting-started/` | What is Uniweb, quickstart, templates |
|
|
1199
|
+
| **Authoring** | `authoring/` | Writing content, site setup, collections, theming, translations |
|
|
1200
|
+
| **Development** | `development/` | Foundations, component patterns, data fetching, layouts, i18n |
|
|
1201
|
+
| **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit API, CLI, deployment |
|
|
1202
|
+
|
|
1203
|
+
**Quick access:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
|