uniweb 0.8.8 → 0.8.10
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 +6 -4
- package/partials/agents.md +562 -625
- package/src/commands/inspect.js +358 -0
- package/src/index.js +8 -0
- package/src/utils/scaffold.js +32 -8
- package/templates/foundation/src/foundation.js.hbs +23 -1
- package/templates/site/site.yml.hbs +49 -0
package/partials/agents.md
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## The Architecture in One Sentence
|
|
4
4
|
|
|
5
|
-
Uniweb
|
|
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
|
+
---
|
|
6
33
|
|
|
7
34
|
## Documentation
|
|
8
35
|
|
|
9
|
-
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
|
|
10
37
|
|
|
11
38
|
**To read a specific page:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
|
|
12
39
|
|
|
@@ -23,34 +50,35 @@ This project was created with [Uniweb](https://github.com/uniweb/cli). Full docu
|
|
|
23
50
|
| Component metadata (meta.js) | `reference/component-metadata.md` |
|
|
24
51
|
| Migrating existing designs | `development/converting-existing.md` |
|
|
25
52
|
|
|
26
|
-
> **npm registry:** Use `https://registry.npmjs.org/uniweb` for package metadata — the npmjs.com website blocks automated requests.
|
|
27
|
-
|
|
28
53
|
## Project Structure
|
|
29
54
|
|
|
55
|
+
Most projects start as a workspace with two packages:
|
|
56
|
+
|
|
30
57
|
```
|
|
31
58
|
project/
|
|
32
|
-
├── foundation/ #
|
|
33
|
-
├── site/ # Content
|
|
59
|
+
├── foundation/ # Component developer's domain
|
|
60
|
+
├── site/ # Content author's domain
|
|
34
61
|
└── pnpm-workspace.yaml
|
|
35
62
|
```
|
|
36
63
|
|
|
37
|
-
|
|
64
|
+
- **Foundation** (developer): React components. Those in `src/sections` and `src/layouts` are *section types* — selectable by content authors via `type:` in frontmatter, or used for site-level layout areas (header, footer, panel). Most have an a `meta.js` with metadata in them. Everything in `src/components` (or elsewhere) is ordinary React.
|
|
65
|
+
- **Site** (content author): Markdown content + configuration. Each section file references a section type. Authors work here without touching foundation code. It may also contain collections of structured content and/or references to external data sources.
|
|
38
66
|
|
|
39
|
-
-
|
|
40
|
-
- **Site**: Markdown content + configuration. Each section file references a section type.
|
|
67
|
+
> Multi-site projects use sub-folders with site/foundation pairs in them, or segregate foundations and sites into separate folders (`foundations/`, `sites/`).
|
|
41
68
|
|
|
42
69
|
## Project Setup
|
|
43
70
|
|
|
44
71
|
Always use the CLI to scaffold projects — never write `package.json`, `vite.config.js`, `main.js`, or `index.html` manually. The CLI resolves correct versions and structure.
|
|
45
72
|
|
|
73
|
+
**npm or pnpm.** Projects include both `pnpm-workspace.yaml` and npm workspaces. Replace `pnpm` with `npm` in any command below.
|
|
74
|
+
|
|
46
75
|
### New workspace
|
|
47
76
|
|
|
48
77
|
```bash
|
|
49
|
-
pnpm create uniweb my-project
|
|
78
|
+
pnpm create uniweb my-project --template <n>
|
|
50
79
|
cd my-project && pnpm install
|
|
51
80
|
```
|
|
52
|
-
|
|
53
|
-
This creates a workspace with foundation + site + starter content — two commands to a dev server. Use `--template <name>` for an official template (`marketing`, `docs`, `academic`, etc.), `--template none` for foundation + site with no content, or `--blank` for an empty workspace.
|
|
81
|
+
Use `--template <n>` for an official template (`none`, `starter`, `marketing`, `docs`, `academic`, etc.), `--template none` for foundation + site with no content, or `--blank` for an empty workspace.
|
|
54
82
|
|
|
55
83
|
### Adding a co-located project
|
|
56
84
|
|
|
@@ -70,7 +98,7 @@ pnpm uniweb add site # First site → ./site/
|
|
|
70
98
|
pnpm uniweb add site blog # Named → ./blog/
|
|
71
99
|
```
|
|
72
100
|
|
|
73
|
-
The name is both the directory name and the package name. Use `--project <
|
|
101
|
+
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/`).
|
|
74
102
|
|
|
75
103
|
### Adding section types
|
|
76
104
|
|
|
@@ -102,10 +130,14 @@ pnpm build # Build for production
|
|
|
102
130
|
pnpm preview # Preview production build (SSG + SPA)
|
|
103
131
|
```
|
|
104
132
|
|
|
105
|
-
|
|
133
|
+
---
|
|
106
134
|
|
|
107
135
|
## Content Authoring
|
|
108
136
|
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
109
141
|
### Section Format
|
|
110
142
|
|
|
111
143
|
Each `.md` file is a section. Frontmatter on top, content below:
|
|
@@ -172,7 +204,28 @@ Lightning quick. ← items[0].paragraphs[0]
|
|
|
172
204
|
Enterprise-grade. ← items[1].paragraphs[0]
|
|
173
205
|
```
|
|
174
206
|
|
|
175
|
-
|
|
207
|
+
**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:
|
|
208
|
+
|
|
209
|
+
```markdown
|
|
210
|
+
### The Problem ← items[0].pretitle
|
|
211
|
+
## Content gets trapped ← items[0].title
|
|
212
|
+
Body text here. ← items[0].paragraphs[0]
|
|
213
|
+
|
|
214
|
+
### The Solution ← items[1].pretitle
|
|
215
|
+
## Separate content from code ← items[1].title
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
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:
|
|
219
|
+
|
|
220
|
+
````markdown
|
|
221
|
+
### Starter ← items[0].title
|
|
222
|
+
$9/month ← items[0].paragraphs[0]
|
|
223
|
+
|
|
224
|
+
```yaml:details
|
|
225
|
+
trial: 14 days
|
|
226
|
+
seats: 1
|
|
227
|
+
``` ← items[0].data.details = { trial: "14 days", seats: 1 }
|
|
228
|
+
````
|
|
176
229
|
|
|
177
230
|
**Complete example — markdown and resulting content shape side by side:**
|
|
178
231
|
|
|
@@ -196,83 +249,65 @@ Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise
|
|
|
196
249
|
|
|
197
250
|
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.
|
|
198
251
|
|
|
199
|
-
###
|
|
252
|
+
### Choosing how to model content
|
|
200
253
|
|
|
201
|
-
|
|
254
|
+
You have three layers. Most of the design skill is choosing between them:
|
|
202
255
|
|
|
203
|
-
|
|
204
|
-
# Build the future │ content.title = ["Build the future", "with confidence"]
|
|
205
|
-
# with confidence │
|
|
206
|
-
```
|
|
256
|
+
**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
257
|
|
|
208
|
-
|
|
258
|
+
**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.
|
|
209
259
|
|
|
210
|
-
**
|
|
260
|
+
**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.
|
|
211
261
|
|
|
212
|
-
|
|
213
|
-
# Build the future │ content.title = [
|
|
214
|
-
# [with confidence]{accent} │ "Build the future",
|
|
215
|
-
│ "<span accent=\"true\">with confidence</span>"
|
|
216
|
-
│ ]
|
|
217
|
-
```
|
|
262
|
+
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.
|
|
218
263
|
|
|
219
|
-
**
|
|
264
|
+
**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
265
|
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
266
|
+
```js
|
|
267
|
+
// You design this — it's not a fixed schema
|
|
268
|
+
export default {
|
|
269
|
+
params: {
|
|
270
|
+
columns: { type: 'number', default: 3 },
|
|
271
|
+
cardStyle: { type: 'select', options: ['minimal', 'bordered', 'elevated'], default: 'minimal' },
|
|
272
|
+
showIcon: { type: 'boolean', default: true },
|
|
273
|
+
maxItems: { type: 'number', default: 6 },
|
|
274
|
+
}
|
|
275
|
+
}
|
|
227
276
|
```
|
|
228
277
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
│
|
|
236
|
-
### Fast │ items[0].title = "Fast"
|
|
237
|
-
### Secure │ items[1].title = "Secure" ← new item, not merged
|
|
278
|
+
````markdown
|
|
279
|
+
<!-- You invent the tag name — the framework parses it -->
|
|
280
|
+
```yaml:speakers
|
|
281
|
+
- name: Ada Lovelace
|
|
282
|
+
role: Keynote
|
|
283
|
+
topic: The Future of Computing
|
|
238
284
|
```
|
|
285
|
+
````
|
|
286
|
+
Access: `content.data?.speakers` — an array of objects. You defined this. The framework parsed it.
|
|
239
287
|
|
|
240
|
-
|
|
288
|
+
**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.
|
|
241
289
|
|
|
242
|
-
|
|
243
|
-
# Line one │ title = "Line one"
|
|
244
|
-
--- │ ← divider forces split
|
|
245
|
-
# Line two │ items[0].title = "Line two"
|
|
246
|
-
```
|
|
290
|
+
### Multi-Line Headings
|
|
247
291
|
|
|
248
|
-
|
|
292
|
+
Consecutive headings at the same level merge into a title array — a single heading split across visual lines:
|
|
249
293
|
|
|
250
294
|
```markdown
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
- Fast builds ← lists[0][0].paragraphs[0]
|
|
254
|
-
- **Hot** reload ← lists[0][1].paragraphs[0] (HTML: "<strong>Hot</strong> reload")
|
|
295
|
+
# Build the future │ content.title = ["Build the future", "with confidence"]
|
|
296
|
+
# with confidence │
|
|
255
297
|
```
|
|
256
298
|
|
|
257
|
-
|
|
299
|
+
Kit's `<H1>`, `<H2>`, etc. render arrays as a single tag with line breaks. This is how you create dramatic multi-line hero headlines.
|
|
258
300
|
|
|
259
|
-
|
|
260
|
-
### Starter ← items[0].title
|
|
261
|
-
$9/month ← items[0].paragraphs[0]
|
|
301
|
+
**Works with accent styling:**
|
|
262
302
|
|
|
263
|
-
|
|
264
|
-
|
|
303
|
+
```markdown
|
|
304
|
+
# Build the future │ content.title = [
|
|
305
|
+
# [with confidence]{accent} │ "Build the future",
|
|
306
|
+
│ "<span accent=\"true\">with confidence</span>"
|
|
307
|
+
│ ]
|
|
265
308
|
```
|
|
266
309
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
```jsx
|
|
270
|
-
import { Span } from '@uniweb/kit'
|
|
271
|
-
|
|
272
|
-
content.lists[0]?.map((listItem, i) => (
|
|
273
|
-
<li key={i}><Span text={listItem.paragraphs[0]} /></li>
|
|
274
|
-
))
|
|
275
|
-
```
|
|
310
|
+
**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.
|
|
276
311
|
|
|
277
312
|
### Icons
|
|
278
313
|
|
|
@@ -280,29 +315,6 @@ Use image syntax with library prefix: ``. Supported libraries: `lu`
|
|
|
280
315
|
|
|
281
316
|
Custom SVGs: `{role=icon}`
|
|
282
317
|
|
|
283
|
-
### Insets (Component References)
|
|
284
|
-
|
|
285
|
-
Place a foundation component inline within content using `@` syntax:
|
|
286
|
-
|
|
287
|
-
```markdown
|
|
288
|
-

|
|
289
|
-
{param=value other=thing}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
The three parts carry distinct information:
|
|
293
|
-
- `[description]` — text passed to the component as `block.content.title`
|
|
294
|
-
- `(@Name)` — foundation component to render
|
|
295
|
-
- `{params}` — configuration attributes passed as `block.properties`
|
|
296
|
-
|
|
297
|
-
```markdown
|
|
298
|
-
{variant=compact}
|
|
299
|
-
{period=30d}
|
|
300
|
-
{position=top-right}
|
|
301
|
-
{note="Vite + React + Routing — ready to go"}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Inset components must declare `inset: true` in their `meta.js`. They render at the exact position in the content flow where the author placed them. See meta.js section below for details.
|
|
305
|
-
|
|
306
318
|
### Links and Media Attributes
|
|
307
319
|
|
|
308
320
|
```markdown
|
|
@@ -313,66 +325,24 @@ Inset components must declare `inset: true` in their `meta.js`. They render at t
|
|
|
313
325
|
|
|
314
326
|
**Quote values that contain spaces:** `{note="Ready to go"}` not `{note=Ready to go}`. Unquoted values end at the first space.
|
|
315
327
|
|
|
316
|
-
Standalone links (alone on a line) become buttons. Inline links stay as
|
|
317
|
-
|
|
318
|
-
**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:
|
|
328
|
+
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[]`:
|
|
319
329
|
|
|
320
330
|
```markdown
|
|
321
331
|
[Primary](/start) ← standalone → content.links[0]
|
|
322
|
-
|
|
323
|
-
[
|
|
324
|
-
|
|
325
|
-
[One](/a) [Two](/b) ← links-only paragraph → content.links[0], content.links[1]
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Links mixed with non-link text stay as inline `<a>` tags within `content.paragraphs[]`:
|
|
329
|
-
|
|
330
|
-
```markdown
|
|
331
|
-
Check out [this](/a) and [that](/b). ← inline links in paragraph text, NOT in content.links[]
|
|
332
|
+
[One](/a) [Two](/b) ← links-only paragraph → both in content.links[]
|
|
333
|
+
Check out [this](/a) link. ← inline → stays in paragraphs as <a> tag
|
|
332
334
|
```
|
|
333
335
|
|
|
334
336
|
### Inline Text Styling
|
|
335
337
|
|
|
336
|
-
Style specific words or phrases using bracketed spans with boolean attributes:
|
|
337
|
-
|
|
338
338
|
```markdown
|
|
339
339
|
# Build [faster]{accent} with structure
|
|
340
|
-
|
|
341
340
|
This is [less important]{muted} context.
|
|
342
341
|
```
|
|
343
342
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
**What you write → what components receive:**
|
|
347
|
-
|
|
348
|
-
| Markdown | HTML in content string |
|
|
349
|
-
|----------|----------------------|
|
|
350
|
-
| `[text]{accent}` | `<span accent="true">text</span>` |
|
|
351
|
-
| `[text]{muted}` | `<span muted="true">text</span>` |
|
|
352
|
-
| `[text]{color=red}` | `<span style="color: red">text</span>` |
|
|
353
|
-
|
|
354
|
-
CSS is generated from `theme.yml`'s `inline:` section using attribute selectors (`span[accent] { ... }`). Sites can define additional named styles:
|
|
355
|
-
|
|
356
|
-
```yaml
|
|
357
|
-
inline:
|
|
358
|
-
accent:
|
|
359
|
-
color: var(--link)
|
|
360
|
-
font-weight: '600'
|
|
361
|
-
callout:
|
|
362
|
-
color: var(--accent-600)
|
|
363
|
-
font-style: italic
|
|
364
|
-
```
|
|
343
|
+
`accent` (colored + bold) and `muted` (subtle) adapt to context automatically. Components receive HTML strings with spans applied: `<span accent="true">faster</span>`.
|
|
365
344
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
```markdown
|
|
369
|
-
# Build the future
|
|
370
|
-
# [with confidence]{accent}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
This produces `content.title = ["Build the future", "<span accent=\"true\">with confidence</span>"]` — an array rendered as a single `<h1>` with visual line breaks. See [Multi-Line Headings](#multi-line-headings) for details.
|
|
374
|
-
|
|
375
|
-
Components receive HTML strings with the spans already applied. Kit's `<H1>`, `<P>`, etc. render them correctly via `dangerouslySetInnerHTML`.
|
|
345
|
+
Sites can define additional named styles in `theme.yml`'s `inline:` section.
|
|
376
346
|
|
|
377
347
|
### Structured Data
|
|
378
348
|
|
|
@@ -389,73 +359,120 @@ submitLabel: Send
|
|
|
389
359
|
|
|
390
360
|
Access: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
|
|
391
361
|
|
|
392
|
-
**
|
|
362
|
+
> **FIXME:** The claim below is wrong. I think tagged blocks an only be YAML/JSON. We could add regular code blocks to the parsed content as an array of code blocks or "pre" elements.
|
|
393
363
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
364
|
+
**Untagged code blocks** 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`).
|
|
365
|
+
|
|
366
|
+
### Composition: Nesting and Embedding
|
|
367
|
+
|
|
368
|
+
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.
|
|
369
|
+
|
|
370
|
+
**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?
|
|
371
|
+
|
|
372
|
+
In other frameworks, this is where you'd reach for MDX, or prop-drill a component. In Uniweb, the content author writes:
|
|
373
|
+
|
|
374
|
+
```markdown
|
|
375
|
+
{variant=compact}
|
|
397
376
|
```
|
|
398
377
|
|
|
399
|
-
|
|
400
|
-
|
|
378
|
+
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:
|
|
379
|
+
|
|
380
|
+
```jsx
|
|
381
|
+
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="rounded-2xl" />
|
|
401
382
|
```
|
|
402
|
-
````
|
|
403
383
|
|
|
404
|
-
|
|
384
|
+
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.
|
|
405
385
|
|
|
406
|
-
|
|
386
|
+
**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.
|
|
407
387
|
|
|
408
|
-
|
|
388
|
+
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.
|
|
409
389
|
|
|
410
|
-
|
|
390
|
+
But if you hardcode which components go where, the author can't rearrange or swap them. This is where child sections solve it:
|
|
411
391
|
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
392
|
+
```
|
|
393
|
+
pages/home/
|
|
394
|
+
├── 2-dashboard.md # type: Grid, columns: "1fr 2fr"
|
|
395
|
+
├── @sidebar-stats.md # type: StatPanel
|
|
396
|
+
└── @main-chart.md # type: PerformanceChart
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```yaml
|
|
400
|
+
# page.yml
|
|
401
|
+
nest:
|
|
402
|
+
dashboard: [sidebar-stats, main-chart]
|
|
416
403
|
```
|
|
417
404
|
|
|
418
|
-
|
|
405
|
+
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.
|
|
419
406
|
|
|
420
|
-
|
|
407
|
+
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.
|
|
408
|
+
|
|
409
|
+
**Choosing the right pattern:**
|
|
410
|
+
|
|
411
|
+
| Pattern | How authored | Use when |
|
|
412
|
+
|---|---|---|
|
|
413
|
+
| **Items** (`content.items`) | Heading groups within one `.md` file | Repeating content within one section: cards, features, FAQ entries |
|
|
414
|
+
| **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children that need their own section type, rich content, or independent editing |
|
|
415
|
+
| **Insets** (`block.insets`) | `` in markdown | Self-contained visuals/widgets: charts, diagrams, code demos |
|
|
416
|
+
|
|
417
|
+
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.
|
|
418
|
+
|
|
419
|
+
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."
|
|
420
|
+
|
|
421
|
+
**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.
|
|
422
|
+
|
|
423
|
+
### Section Nesting Details
|
|
421
424
|
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
-
|
|
427
|
-
|
|
428
|
-
|
|
425
|
+
```
|
|
426
|
+
pages/home/
|
|
427
|
+
├── page.yml
|
|
428
|
+
├── 1-hero.md
|
|
429
|
+
├── 2-features.md # Parent section (type: Grid)
|
|
430
|
+
├── 3-cta.md
|
|
431
|
+
├── @card-a.md # Child of features (@ = not top-level)
|
|
432
|
+
├── @card-b.md
|
|
433
|
+
└── @card-c.md
|
|
429
434
|
```
|
|
430
435
|
|
|
431
|
-
|
|
436
|
+
```yaml
|
|
437
|
+
# page.yml
|
|
438
|
+
nest:
|
|
439
|
+
features: [card-a, card-b, card-c]
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Rules:**
|
|
443
|
+
- `@`-prefixed files are excluded from the top-level section list
|
|
444
|
+
- `nest:` declares parent-child relationships (parent name → child names)
|
|
445
|
+
- `@@` prefix for deeper nesting (grandchildren)
|
|
446
|
+
- `nest:` is flat: `{ features: [a, b], a: [sub-1] }`
|
|
447
|
+
- Children ordered by position in the `nest:` array
|
|
432
448
|
|
|
433
449
|
```jsx
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
))
|
|
450
|
+
import { ChildBlocks } from '@uniweb/kit'
|
|
451
|
+
|
|
452
|
+
export default function Grid({ block, params }) {
|
|
453
|
+
return (
|
|
454
|
+
<div className={`grid grid-cols-${params.columns || 2} gap-6`}>
|
|
455
|
+
<ChildBlocks from={block} />
|
|
456
|
+
</div>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
444
459
|
```
|
|
445
460
|
|
|
446
461
|
### Section Backgrounds
|
|
447
462
|
|
|
448
|
-
Set `background` in frontmatter — the runtime renders it automatically
|
|
463
|
+
Set `background` in frontmatter — the runtime renders it automatically:
|
|
464
|
+
|
|
465
|
+
> **FIXME:** var(--primary-900) should be primary-900, right?
|
|
449
466
|
|
|
450
467
|
```yaml
|
|
451
|
-
background: /images/hero.jpg # Image
|
|
452
|
-
background: /videos/hero.mp4 # Video
|
|
453
|
-
background: linear-gradient(135deg, #667eea, #764ba2) #
|
|
468
|
+
background: /images/hero.jpg # Image
|
|
469
|
+
background: /videos/hero.mp4 # Video
|
|
470
|
+
background: linear-gradient(135deg, #667eea, #764ba2) # Gradient
|
|
454
471
|
background: '#1a1a2e' # Color (hex — quote in YAML)
|
|
455
|
-
background: var(--primary-900) #
|
|
472
|
+
background: var(--primary-900) # CSS variable
|
|
456
473
|
```
|
|
457
474
|
|
|
458
|
-
|
|
475
|
+
Object form for more control:
|
|
459
476
|
|
|
460
477
|
```yaml
|
|
461
478
|
background:
|
|
@@ -463,8 +480,6 @@ background:
|
|
|
463
480
|
overlay: { enabled: true, type: dark, opacity: 0.5 }
|
|
464
481
|
```
|
|
465
482
|
|
|
466
|
-
Overlay shorthand — `overlay: 0.5` is equivalent to `{ enabled: true, type: dark, opacity: 0.5 }`.
|
|
467
|
-
|
|
468
483
|
Components that render their own background declare `background: 'self'` in `meta.js`.
|
|
469
484
|
|
|
470
485
|
### Page Organization
|
|
@@ -478,7 +493,7 @@ site/layout/
|
|
|
478
493
|
site/pages/
|
|
479
494
|
└── home/
|
|
480
495
|
├── page.yml # title, description, order
|
|
481
|
-
├── hero.md # Single section
|
|
496
|
+
├── hero.md # Single section
|
|
482
497
|
└── (or for multi-section pages:)
|
|
483
498
|
├── 1-hero.md # Numeric prefix sets order
|
|
484
499
|
├── 2-features.md
|
|
@@ -487,9 +502,7 @@ site/pages/
|
|
|
487
502
|
|
|
488
503
|
Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`.
|
|
489
504
|
|
|
490
|
-
**Ignored
|
|
491
|
-
- `README.md` — repo documentation, not site content
|
|
492
|
-
- `_*.md` or `_*/` — drafts and private content (e.g., `_drafts/`, `_old-hero.md`)
|
|
505
|
+
**Ignored:** `README.md` (repo docs), `_*.md` or `_*/` (drafts/private).
|
|
493
506
|
|
|
494
507
|
**page.yml:**
|
|
495
508
|
```yaml
|
|
@@ -507,97 +520,73 @@ pages: [home, about, ...] # Order pages (... = rest, first = homepage)
|
|
|
507
520
|
pages: [home, about] # Strict: only listed pages in nav
|
|
508
521
|
```
|
|
509
522
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
### Section Nesting (Child Sections)
|
|
523
|
+
### Lists as Navigation Menus
|
|
513
524
|
|
|
514
|
-
|
|
525
|
+
Markdown lists model nav, menus, and grouped links. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
|
|
515
526
|
|
|
527
|
+
**Header nav:**
|
|
528
|
+
```markdown
|
|
529
|
+
-  [Home](/)
|
|
530
|
+
-  [Docs](/docs)
|
|
531
|
+
-  [Contact](/contact)
|
|
516
532
|
```
|
|
517
|
-
|
|
518
|
-
├── page.yml
|
|
519
|
-
├── 1-hero.md
|
|
520
|
-
├── 2-features.md # Parent section (type: Grid)
|
|
521
|
-
├── 3-cta.md
|
|
522
|
-
├── @card-a.md # Child of features (@ = not top-level)
|
|
523
|
-
├── @card-b.md
|
|
524
|
-
└── @card-c.md
|
|
525
|
-
```
|
|
533
|
+
Access: `content.lists[0]` — each item has `item.links[0]` and `item.icons[0]`.
|
|
526
534
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
535
|
+
**Footer columns:**
|
|
536
|
+
```markdown
|
|
537
|
+
- Product
|
|
538
|
+
- [Features](/features)
|
|
539
|
+
- [Pricing](/pricing)
|
|
540
|
+
- Company
|
|
541
|
+
- [About](/about)
|
|
542
|
+
- [Careers](/careers)
|
|
531
543
|
```
|
|
544
|
+
Access: `content.lists[0]` — `group.paragraphs[0]` (label), `group.lists[0]` (sub-items with `subItem.links[0]`).
|
|
532
545
|
|
|
533
|
-
|
|
534
|
-
- `@`-prefixed files are excluded from the top-level section list
|
|
535
|
-
- `nest:` declares parent-child relationships (parent name → array of child names)
|
|
536
|
-
- Child files **must** use the `@` prefix — the filename and YAML must agree
|
|
537
|
-
- `@@` prefix signals deeper nesting (e.g., `@@sub-item.md` for grandchildren)
|
|
538
|
-
- `nest:` is flat — each key is a parent: `nest: { features: [a, b], a: [sub-1] }`
|
|
539
|
-
- Children are ordered by their position in the `nest:` array
|
|
540
|
-
- Orphaned `@` files (no parent in `nest:`) appear at top-level with a warning
|
|
541
|
-
|
|
542
|
-
Components receive children via `block.childBlocks`. Use `ChildBlocks` from kit to render them — the runtime handles component resolution:
|
|
546
|
+
Render list item text with Kit components — list items contain HTML strings, not plain text:
|
|
543
547
|
|
|
544
548
|
```jsx
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
549
|
+
content.lists[0]?.map((group, i) => (
|
|
550
|
+
<div key={i}>
|
|
551
|
+
<Span text={group.paragraphs[0]} className="font-semibold text-heading" />
|
|
552
|
+
<ul>
|
|
553
|
+
{group.lists[0]?.map((subItem, j) => (
|
|
554
|
+
<li key={j}><Link to={subItem.links[0]?.href}>{subItem.links[0]?.label}</Link></li>
|
|
555
|
+
))}
|
|
556
|
+
</ul>
|
|
557
|
+
</div>
|
|
558
|
+
))
|
|
554
559
|
```
|
|
555
560
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
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:
|
|
559
|
-
|
|
560
|
-
```
|
|
561
|
-
pages/home/
|
|
562
|
-
├── page.yml
|
|
563
|
-
├── 1-hero.md
|
|
564
|
-
├── 2-highlights.md # type: Grid, columns: 3
|
|
565
|
-
├── 3-cta.md
|
|
566
|
-
├── @stats.md # type: StatCard — numbers and labels
|
|
567
|
-
├── @testimonial.md # type: Testimonial — quote with attribution
|
|
568
|
-
└── @demo.md # type: SplitContent — text +  inset
|
|
569
|
-
```
|
|
561
|
+
**For richer navigation with icons, descriptions, or hierarchy**, use `yaml:nav` tagged blocks:
|
|
570
562
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
563
|
+
````markdown
|
|
564
|
+
```yaml:nav
|
|
565
|
+
- label: Dashboard
|
|
566
|
+
href: /
|
|
567
|
+
icon: lu:layout-grid
|
|
568
|
+
- label: Docs
|
|
569
|
+
href: /docs
|
|
570
|
+
icon: lu:book-open
|
|
571
|
+
children:
|
|
572
|
+
- label: Getting Started
|
|
573
|
+
href: /docs/quickstart
|
|
574
574
|
```
|
|
575
|
+
````
|
|
575
576
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
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.
|
|
579
|
-
|
|
580
|
-
### When to use which pattern
|
|
581
|
-
|
|
582
|
-
| Pattern | Authoring | Use when |
|
|
583
|
-
|---------|-----------|----------|
|
|
584
|
-
| **Items** (`content.items`) | Heading groups in one `.md` file | Repeating content within one section (cards, FAQ entries) |
|
|
585
|
-
| **Insets** (`block.insets`) | `` in markdown | Embedding a self-contained visual (chart, diagram, widget) |
|
|
586
|
-
| **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children with rich authored content (testimonials, carousel slides) |
|
|
587
|
-
|
|
588
|
-
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.
|
|
577
|
+
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.
|
|
589
578
|
|
|
590
|
-
|
|
579
|
+
---
|
|
591
580
|
|
|
592
581
|
## Semantic Theming
|
|
593
582
|
|
|
594
|
-
|
|
583
|
+
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).
|
|
595
584
|
|
|
596
585
|
```jsx
|
|
597
|
-
// ❌ Hardcoded — breaks in dark context
|
|
586
|
+
// ❌ Hardcoded — breaks in dark context
|
|
598
587
|
<h2 className="text-slate-900">...</h2>
|
|
599
588
|
|
|
600
|
-
// ✅ Semantic — adapts to any context and brand
|
|
589
|
+
// ✅ Semantic — adapts to any context and brand
|
|
601
590
|
<h2 className="text-heading">...</h2>
|
|
602
591
|
```
|
|
603
592
|
|
|
@@ -613,48 +602,9 @@ CCA separates theme from code. Components use **semantic CSS tokens** instead of
|
|
|
613
602
|
| `bg-muted` | Hover states, zebra rows |
|
|
614
603
|
| `border-border` | Borders |
|
|
615
604
|
| `text-link` | Link color |
|
|
616
|
-
| `bg-primary` | Primary
|
|
617
|
-
| `text-
|
|
618
|
-
| `
|
|
619
|
-
| `border-primary-border` | Primary border (transparent by default) |
|
|
620
|
-
| `bg-secondary` | Secondary action background |
|
|
621
|
-
| `text-secondary-foreground` | Text on secondary background |
|
|
622
|
-
| `hover:bg-secondary-hover` | Secondary hover state |
|
|
623
|
-
| `border-secondary-border` | Secondary border |
|
|
624
|
-
| `text-success` / `bg-success-subtle` | Status: success |
|
|
625
|
-
| `text-error` / `bg-error-subtle` | Status: error |
|
|
626
|
-
| `text-warning` / `bg-warning-subtle` | Status: warning |
|
|
627
|
-
| `text-info` / `bg-info-subtle` | Status: info |
|
|
628
|
-
|
|
629
|
-
### What the runtime handles (don't write this yourself)
|
|
630
|
-
|
|
631
|
-
The runtime does significant work that other frameworks push onto components. Understanding this prevents writing unnecessary code:
|
|
632
|
-
|
|
633
|
-
1. **Section backgrounds** — The runtime renders image, video, gradient, color, and overlay backgrounds from frontmatter. Components never set their own section background.
|
|
634
|
-
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.
|
|
635
|
-
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.
|
|
636
|
-
4. **Colored section backgrounds** — Content authors create tinted sections via frontmatter, not component code:
|
|
637
|
-
```yaml
|
|
638
|
-
---
|
|
639
|
-
type: Features
|
|
640
|
-
theme: light
|
|
641
|
-
background:
|
|
642
|
-
color: var(--primary-50) # Light blue tint with light-context tokens
|
|
643
|
-
---
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
**What components should NOT contain:**
|
|
647
|
-
|
|
648
|
-
| Don't write | Why |
|
|
649
|
-
|-------------|-----|
|
|
650
|
-
| `bg-white` or `bg-gray-900` on section wrapper | Engine applies `bg-section` via context class |
|
|
651
|
-
| `const themes = { light: {...}, dark: {...} }` | Context system replaces theme maps entirely |
|
|
652
|
-
| `isDark ? 'text-white' : 'text-gray-900'` | Just write `text-heading` — it adapts |
|
|
653
|
-
| Background rendering code | Declare `background:` in frontmatter instead |
|
|
654
|
-
| Color constants / tokens files | Colors come from `theme.yml` |
|
|
655
|
-
| 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. |
|
|
656
|
-
|
|
657
|
-
**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.
|
|
605
|
+
| `bg-primary` / `text-primary-foreground` / `hover:bg-primary-hover` | Primary actions |
|
|
606
|
+
| `bg-secondary` / `text-secondary-foreground` / `hover:bg-secondary-hover` | Secondary actions |
|
|
607
|
+
| `text-success` / `text-error` / `text-warning` / `text-info` | Status colors |
|
|
658
608
|
|
|
659
609
|
**Content authors control context** in frontmatter:
|
|
660
610
|
|
|
@@ -665,34 +615,19 @@ theme: dark ← sets context-dark, all tokens resolve to dark values
|
|
|
665
615
|
---
|
|
666
616
|
```
|
|
667
617
|
|
|
668
|
-
Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm
|
|
669
|
-
|
|
670
|
-
```markdown
|
|
671
|
-
<!-- 1-hero.md -->
|
|
672
|
-
theme: dark
|
|
673
|
-
|
|
674
|
-
<!-- 2-features.md -->
|
|
675
|
-
(no theme — defaults to light)
|
|
676
|
-
|
|
677
|
-
<!-- 3-testimonials.md -->
|
|
678
|
-
theme: medium
|
|
679
|
-
|
|
680
|
-
<!-- 4-cta.md -->
|
|
681
|
-
theme: dark
|
|
682
|
-
```
|
|
618
|
+
Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm.
|
|
683
619
|
|
|
684
|
-
**
|
|
620
|
+
**But the three presets aren't the limit.** The object form gives fine-grained control per section:
|
|
685
621
|
|
|
686
622
|
```yaml
|
|
687
623
|
theme:
|
|
688
624
|
mode: light
|
|
689
|
-
|
|
690
|
-
|
|
625
|
+
section: neutral-100 # Subtle off-white surface
|
|
626
|
+
card: neutral-50 # Cards lighter than surface
|
|
627
|
+
primary: neutral-900 # Dark buttons instead of brand color
|
|
691
628
|
```
|
|
692
629
|
|
|
693
|
-
Any semantic token
|
|
694
|
-
|
|
695
|
-
**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.
|
|
630
|
+
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.
|
|
696
631
|
|
|
697
632
|
### theme.yml
|
|
698
633
|
|
|
@@ -715,98 +650,70 @@ fonts:
|
|
|
715
650
|
body: "'Inter', system-ui, sans-serif"
|
|
716
651
|
|
|
717
652
|
inline:
|
|
718
|
-
accent:
|
|
653
|
+
accent:
|
|
719
654
|
color: var(--link)
|
|
720
655
|
font-weight: '600'
|
|
721
656
|
|
|
722
|
-
vars:
|
|
657
|
+
vars:
|
|
723
658
|
header-height: 5rem
|
|
724
659
|
```
|
|
725
660
|
|
|
726
|
-
Each color generates 11 OKLCH shades (50–950).
|
|
661
|
+
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:`.
|
|
727
662
|
|
|
728
663
|
### How colors reach components
|
|
729
664
|
|
|
730
|
-
Your hex
|
|
731
|
-
|
|
732
|
-
**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.
|
|
733
|
-
|
|
734
|
-
Semantic tokens map shades to roles. The defaults for light/medium contexts:
|
|
665
|
+
Your hex → 11 shades (50–950) → semantic tokens → components.
|
|
735
666
|
|
|
736
|
-
|
|
737
|
-
|-------|-------|---------|
|
|
738
|
-
| `--primary` | 600 | Button background |
|
|
739
|
-
| `--primary-hover` | 700 | Button hover |
|
|
740
|
-
| `--link` | 600 | Link color |
|
|
741
|
-
| `--ring` | 500 | Focus ring |
|
|
667
|
+
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.
|
|
742
668
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
**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.
|
|
746
|
-
|
|
747
|
-
**Recipe — brand-exact buttons:**
|
|
669
|
+
**Buttons use shade 600 — darker than your input color.** This is an accessibility choice for contrast with white text. For brand-exact buttons:
|
|
748
670
|
|
|
749
671
|
```yaml
|
|
750
672
|
colors:
|
|
751
673
|
primary: "#E35D25"
|
|
752
|
-
|
|
753
674
|
contexts:
|
|
754
675
|
light:
|
|
755
676
|
primary: primary-500 # Your exact color on buttons
|
|
756
|
-
primary-hover: primary-600
|
|
677
|
+
primary-hover: primary-600
|
|
757
678
|
```
|
|
758
679
|
|
|
759
|
-
> **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
|
|
680
|
+
> **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.
|
|
760
681
|
|
|
761
682
|
### Foundation variables
|
|
762
683
|
|
|
763
|
-
Foundations declare customizable layout
|
|
684
|
+
Foundations declare customizable layout values in `foundation.js`:
|
|
764
685
|
|
|
765
686
|
```js
|
|
766
687
|
export const vars = {
|
|
767
688
|
'header-height': { default: '4rem', description: 'Fixed header height' },
|
|
768
689
|
'max-content-width': { default: '80rem', description: 'Maximum content width' },
|
|
769
|
-
'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical padding
|
|
690
|
+
'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical section padding' },
|
|
770
691
|
}
|
|
771
692
|
```
|
|
772
693
|
|
|
773
|
-
Sites override
|
|
774
|
-
|
|
775
|
-
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`.
|
|
776
|
-
|
|
777
|
-
**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.
|
|
694
|
+
Sites override in `theme.yml` under `vars:`. Components use: `py-[var(--section-padding-y)]`, `h-[var(--header-height)]`.
|
|
778
695
|
|
|
779
696
|
### Design richness beyond tokens
|
|
780
697
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
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.
|
|
784
|
-
|
|
785
|
-
**Don't flatten a rich design to fit the token set.** If a source design has 4 border tones, create them:
|
|
698
|
+
Tokens handle context adaptation — the hard problem. **They are a floor, not a ceiling.** A great foundation adds design vocabulary on top:
|
|
786
699
|
|
|
787
700
|
```css
|
|
788
701
|
/* foundation/src/styles.css */
|
|
789
702
|
.border-subtle { border-color: color-mix(in oklch, var(--border), transparent 50%); }
|
|
790
703
|
.border-strong { border-color: color-mix(in oklch, var(--border), var(--heading) 30%); }
|
|
791
|
-
.
|
|
704
|
+
.text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
|
|
792
705
|
```
|
|
793
706
|
|
|
794
|
-
These compose with
|
|
795
|
-
|
|
796
|
-
**The priority:** Design quality > portability > configurability. It's better to ship a foundation with beautiful, detailed design that's less configurable than to ship a generic one that looks flat. A foundation that looks great for one site is more valuable than one that looks mediocre for any site.
|
|
707
|
+
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.
|
|
797
708
|
|
|
798
|
-
**
|
|
709
|
+
**The priority:** Design quality > portability > configurability. A beautiful foundation for one site is more valuable than a generic one that looks flat.
|
|
799
710
|
|
|
800
|
-
|
|
801
|
-
/* foundation/src/styles.css */
|
|
802
|
-
.text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
|
|
803
|
-
.text-disabled { color: color-mix(in oklch, var(--subtle), transparent 40%); }
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
**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.
|
|
711
|
+
---
|
|
807
712
|
|
|
808
713
|
## Component Development
|
|
809
714
|
|
|
715
|
+
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.
|
|
716
|
+
|
|
810
717
|
### Props Interface
|
|
811
718
|
|
|
812
719
|
```jsx
|
|
@@ -817,9 +724,25 @@ function MyComponent({ content, params, block }) {
|
|
|
817
724
|
}
|
|
818
725
|
```
|
|
819
726
|
|
|
727
|
+
All non-reserved frontmatter fields become `params`. Reserved: `type`, `preset`, `input`, `data`, `id`, `background`, `theme`. Everything else flows to the component.
|
|
728
|
+
|
|
729
|
+
### block properties
|
|
730
|
+
|
|
731
|
+
| Property | Type | Description |
|
|
732
|
+
|----------|------|-------------|
|
|
733
|
+
| `block.page` | Page | Parent page |
|
|
734
|
+
| `block.website` | Website | Site-level data and navigation |
|
|
735
|
+
| `block.type` | string | Component type name |
|
|
736
|
+
| `block.childBlocks` | Block[] | File-based child sections |
|
|
737
|
+
| `block.insets` | Block[] | Inline `@Component` references |
|
|
738
|
+
| `block.getInset(refId)` | Block | Lookup inset by refId |
|
|
739
|
+
| `block.properties` | object | Raw frontmatter |
|
|
740
|
+
| `block.themeName` | string | `"light"`, `"medium"`, `"dark"` |
|
|
741
|
+
| `block.stableId` | string | Stable ID from filename or `id:` |
|
|
742
|
+
|
|
820
743
|
### Section Wrapper
|
|
821
744
|
|
|
822
|
-
The runtime wraps every section
|
|
745
|
+
The runtime wraps every section in `<section>` with context class and background. Customize with static properties:
|
|
823
746
|
|
|
824
747
|
```jsx
|
|
825
748
|
function Hero({ content, params }) {
|
|
@@ -830,29 +753,80 @@ function Hero({ content, params }) {
|
|
|
830
753
|
)
|
|
831
754
|
}
|
|
832
755
|
|
|
833
|
-
Hero.className = 'pt-32 md:pt-48' // Override spacing
|
|
834
|
-
Hero.as = 'div' // Change wrapper element
|
|
756
|
+
Hero.className = 'pt-32 md:pt-48' // Override spacing
|
|
757
|
+
Hero.as = 'div' // Change wrapper element
|
|
835
758
|
|
|
836
759
|
export default Hero
|
|
837
760
|
```
|
|
838
761
|
|
|
839
|
-
- `Component.className` — adds classes to the runtime
|
|
840
|
-
- `Component.as` — changes
|
|
762
|
+
- `Component.className` — adds classes to the runtime wrapper. Section-level spacing, borders, overflow.
|
|
763
|
+
- `Component.as` — changes wrapper element: `'nav'` for headers, `'footer'` for footers.
|
|
841
764
|
|
|
842
|
-
**Layout components**
|
|
765
|
+
**Layout components** typically need `p-0` to suppress default padding:
|
|
843
766
|
|
|
844
767
|
```jsx
|
|
845
|
-
function Header({ content, block }) { /* ... */ }
|
|
846
768
|
Header.className = 'p-0'
|
|
847
769
|
Header.as = 'header'
|
|
848
|
-
export default Header
|
|
849
770
|
```
|
|
850
771
|
|
|
772
|
+
### Rendering Content with Kit
|
|
773
|
+
|
|
774
|
+
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:
|
|
775
|
+
|
|
776
|
+
**Extracted fields** (most common — custom layout with content from markdown):
|
|
777
|
+
|
|
778
|
+
```jsx
|
|
779
|
+
import { H1, H2, P, Span } from '@uniweb/kit'
|
|
780
|
+
|
|
781
|
+
<H1 text={content.title} className="text-heading text-5xl font-bold" />
|
|
782
|
+
<P text={content.paragraphs} className="text-body" />
|
|
783
|
+
<Span text={listItem.paragraphs[0]} className="text-subtle" />
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
These render their own HTML tag — don't wrap: `<H2 text={...} />` not `<h2><H2 text={...} /></h2>`.
|
|
787
|
+
|
|
788
|
+
**Full content rendering** (article/docs sections where the author controls the flow):
|
|
789
|
+
|
|
790
|
+
```jsx
|
|
791
|
+
import { Section, Render } from '@uniweb/kit'
|
|
792
|
+
|
|
793
|
+
<Section block={block} width="lg" padding="md" />
|
|
794
|
+
<Render content={block.parsedContent} block={block} />
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
**Visuals:**
|
|
798
|
+
|
|
799
|
+
```jsx
|
|
800
|
+
import { Visual } from '@uniweb/kit'
|
|
801
|
+
|
|
802
|
+
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="rounded-2xl" />
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### Kit API by Use Case
|
|
806
|
+
|
|
807
|
+
**Rendering text:** `H1`–`H6`, `P`, `Span`, `Div`, `Text` (with `as` prop)
|
|
808
|
+
|
|
809
|
+
**Rendering content:** `Section` (Render + prose + layout), `Render` (ProseMirror → React), `ChildBlocks` (render child sections)
|
|
810
|
+
|
|
811
|
+
**Rendering media:** `Visual` (first non-empty: inset/video/image), `Image`, `Media`, `Icon`
|
|
812
|
+
|
|
813
|
+
**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()`
|
|
814
|
+
|
|
815
|
+
**Header and layout:** `useScrolled(threshold)`, `useMobileMenu()`, `useAppearance()` (light/dark mode)
|
|
816
|
+
|
|
817
|
+
**Layout helpers:** `useGridLayout(columns, { gap })`, `useAccordion({ multiple, defaultOpen })`, `useTheme(name)`
|
|
818
|
+
|
|
819
|
+
**Data and theming:** `useThemeData()` (programmatic color access), `useColorContext(block)`
|
|
820
|
+
|
|
821
|
+
**Utilities:** `cn()` (Tailwind class merge), `Link`, `Image`, `Asset`, `SafeHtml`, `SocialIcon`, `filterSocialLinks(links)`, `getSocialPlatform(url)`
|
|
822
|
+
|
|
823
|
+
**Other styled:** `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
824
|
+
|
|
851
825
|
### Content Patterns for Header and Footer
|
|
852
826
|
|
|
853
|
-
Header and Footer
|
|
827
|
+
Header and Footer combine several content categories. Use different parts of the content shape for each role:
|
|
854
828
|
|
|
855
|
-
**Header** — title for logo, list for nav
|
|
829
|
+
**Header** — title for logo, list for nav, standalone link for CTA:
|
|
856
830
|
|
|
857
831
|
````markdown
|
|
858
832
|
---
|
|
@@ -875,15 +849,14 @@ version: v2.1.0
|
|
|
875
849
|
|
|
876
850
|
```jsx
|
|
877
851
|
function Header({ content, block }) {
|
|
878
|
-
const logo = content.title
|
|
879
|
-
const navItems = content.lists[0] || []
|
|
880
|
-
const cta = content.links[0]
|
|
881
|
-
const config = content.data?.config
|
|
882
|
-
// ...
|
|
852
|
+
const logo = content.title
|
|
853
|
+
const navItems = content.lists[0] || []
|
|
854
|
+
const cta = content.links[0]
|
|
855
|
+
const config = content.data?.config
|
|
883
856
|
}
|
|
884
857
|
```
|
|
885
858
|
|
|
886
|
-
**Footer** — paragraph for tagline, nested list for
|
|
859
|
+
**Footer** — paragraph for tagline, nested list for columns, YAML for legal:
|
|
887
860
|
|
|
888
861
|
````markdown
|
|
889
862
|
---
|
|
@@ -898,9 +871,6 @@ Build something great.
|
|
|
898
871
|
- Developers
|
|
899
872
|
- [Docs](/docs)
|
|
900
873
|
- [GitHub](https://github.com/acme){target=_blank}
|
|
901
|
-
- Community
|
|
902
|
-
- [Discord](#)
|
|
903
|
-
- [Blog](/blog)
|
|
904
874
|
|
|
905
875
|
```yaml:legal
|
|
906
876
|
copyright: © 2025 Acme Inc
|
|
@@ -908,12 +878,11 @@ copyright: © 2025 Acme Inc
|
|
|
908
878
|
````
|
|
909
879
|
|
|
910
880
|
```jsx
|
|
911
|
-
function Footer({ content
|
|
912
|
-
const tagline = content.paragraphs[0]
|
|
913
|
-
const columns = content.lists[0] || []
|
|
914
|
-
const legal = content.data?.legal
|
|
881
|
+
function Footer({ content }) {
|
|
882
|
+
const tagline = content.paragraphs[0]
|
|
883
|
+
const columns = content.lists[0] || []
|
|
884
|
+
const legal = content.data?.legal
|
|
915
885
|
|
|
916
|
-
// Each column: group.paragraphs[0] = label, group.lists[0] = links
|
|
917
886
|
columns.map(group => ({
|
|
918
887
|
label: group.paragraphs[0],
|
|
919
888
|
links: group.lists[0]?.map(item => item.links[0])
|
|
@@ -928,11 +897,11 @@ export default {
|
|
|
928
897
|
title: 'Feature Grid',
|
|
929
898
|
description: 'Grid of feature cards with icons',
|
|
930
899
|
category: 'marketing',
|
|
931
|
-
// hidden: true, // Exclude from export
|
|
900
|
+
// hidden: true, // Exclude from export
|
|
932
901
|
// background: 'self', // Component renders its own background
|
|
933
|
-
// inset: true, // Available for @ComponentName
|
|
934
|
-
// visuals: 1, // Expects 1 visual
|
|
935
|
-
// children: true, // Accepts
|
|
902
|
+
// inset: true, // Available for @ComponentName in markdown
|
|
903
|
+
// visuals: 1, // Expects 1 visual
|
|
904
|
+
// children: true, // Accepts child sections
|
|
936
905
|
|
|
937
906
|
content: {
|
|
938
907
|
title: 'Section heading',
|
|
@@ -950,241 +919,203 @@ export default {
|
|
|
950
919
|
compact: { label: 'Compact', params: { columns: 4 } },
|
|
951
920
|
},
|
|
952
921
|
|
|
953
|
-
//
|
|
922
|
+
// context and initialState: keys are developer-defined, not framework fields.
|
|
923
|
+
// Design your own names for your foundation's cross-block communication.
|
|
924
|
+
|
|
925
|
+
// Static — neighbors read via getNextBlockInfo().context
|
|
954
926
|
context: {
|
|
955
|
-
|
|
927
|
+
// Example: a Hero might declare this so a Header knows it can float.
|
|
928
|
+
// allowTranslucentTop: true,
|
|
929
|
+
},
|
|
930
|
+
|
|
931
|
+
// Dynamic — neighbors read via getNextBlockInfo().state
|
|
932
|
+
// Component can update with useBlockState()
|
|
933
|
+
initialState: {
|
|
934
|
+
// Example: Hero starts translucent-ready, but component logic may disable it.
|
|
935
|
+
// allowTranslucentTop: true,
|
|
956
936
|
},
|
|
957
937
|
}
|
|
958
938
|
```
|
|
959
939
|
|
|
960
940
|
All defaults belong in `meta.js`, not inline in component code.
|
|
961
941
|
|
|
962
|
-
###
|
|
963
|
-
|
|
964
|
-
Content fields (`title`, `pretitle`, `paragraphs[]`, list item text) are **HTML strings** — they contain markup like `<strong>`, `<em>`, `<a>` from the author's markdown. The kit provides components to render them correctly.
|
|
942
|
+
### The Front Desk Pattern
|
|
965
943
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
```jsx
|
|
969
|
-
import { H1, H2, P, Span } from '@uniweb/kit'
|
|
944
|
+
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.
|
|
970
945
|
|
|
971
|
-
|
|
972
|
-
// string → single <h1>, array → single <h1> with line breaks (multi-line headings)
|
|
973
|
-
<H2 text={content.subtitle} className="text-heading text-3xl font-bold" />
|
|
974
|
-
<P text={content.paragraphs} className="text-body" />
|
|
975
|
-
// array → each string becomes its own <p>
|
|
976
|
-
<Span text={listItem.paragraphs[0]} className="text-subtle" />
|
|
977
|
-
```
|
|
946
|
+
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, 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.
|
|
978
947
|
|
|
979
|
-
|
|
948
|
+
The workers behind the front desk don't need to share the same interface. A `Hero` might delegate to a `SliderHero` that renders an image carousel and a `ContactHero` that renders a quote request form. They expect different content and different props — that's fine. The front desk declares the **union** of all content its workers might need. Some content won't be used for a given variant, and that's perfectly normal in CCA — params change behavior, and that includes not rendering some content:
|
|
980
949
|
|
|
981
|
-
```
|
|
982
|
-
|
|
950
|
+
```js
|
|
951
|
+
// meta.js — the union of all variants' needs
|
|
952
|
+
export default {
|
|
953
|
+
params: {
|
|
954
|
+
variant: { type: 'select', options: ['slider', 'contact'], default: 'slider' },
|
|
955
|
+
slideInterval: { type: 'number', default: 5 },
|
|
956
|
+
density: { type: 'select', options: ['default', 'compact'], default: 'default' },
|
|
957
|
+
style: { type: 'select', options: ['default', 'dramatic'], default: 'default' },
|
|
958
|
+
}
|
|
959
|
+
}
|
|
983
960
|
```
|
|
984
961
|
|
|
985
|
-
These components render their own HTML tag — don't wrap them in a matching tag. `<h2><H2 text={...} /></h2>` creates a nested `<h2><h2>...</h2></h2>`, which is invalid HTML. Just use `<H2 text={...} />` directly.
|
|
986
|
-
|
|
987
|
-
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.
|
|
988
|
-
|
|
989
|
-
**Rendering full content** (`@uniweb/kit`):
|
|
990
|
-
|
|
991
962
|
```jsx
|
|
992
|
-
|
|
963
|
+
// sections/Hero/index.jsx — the front desk
|
|
964
|
+
import { SliderHero } from '../../components/SliderHero'
|
|
965
|
+
import { ContactHero } from '../../components/ContactHero'
|
|
993
966
|
|
|
994
|
-
|
|
995
|
-
<Section block={block} width="lg" padding="md" /> // Render + prose styling + layout
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
`Render` processes ProseMirror nodes into React elements — paragraphs, headings, images, code blocks, lists, tables, alerts, and insets. `Section` wraps `Render` with prose typography and layout options. Use these when rendering a block's complete content. Use `P`, `H2`, etc. when you extract specific fields and arrange them with custom layout.
|
|
999
|
-
|
|
1000
|
-
**Rendering visuals** (`@uniweb/kit`):
|
|
1001
|
-
|
|
1002
|
-
`<Visual>` renders the first non-empty candidate from the props you pass (inset, video, image). See Insets section below.
|
|
967
|
+
const variants = { slider: SliderHero, contact: ContactHero }
|
|
1003
968
|
|
|
1004
|
-
|
|
969
|
+
export default function Hero({ content, block, params }) {
|
|
970
|
+
const Variant = variants[params.variant] || SliderHero
|
|
1005
971
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
972
|
+
return (
|
|
973
|
+
<Variant
|
|
974
|
+
// Shared — every variant gets these
|
|
975
|
+
title={content.title}
|
|
976
|
+
subtitle={content.paragraphs[0]}
|
|
977
|
+
links={content.links}
|
|
978
|
+
block={block}
|
|
979
|
+
// Content that only some variants use
|
|
980
|
+
images={content.imgs}
|
|
981
|
+
formData={content.data?.quote}
|
|
982
|
+
// Translated params — author vocabulary → developer props
|
|
983
|
+
interval={params.slideInterval}
|
|
984
|
+
compact={params.density === 'compact'}
|
|
985
|
+
transition={params.style === 'dramatic' ? 'zoom' : 'fade'}
|
|
986
|
+
/>
|
|
987
|
+
)
|
|
988
|
+
}
|
|
1013
989
|
```
|
|
1014
990
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
**Hooks:**
|
|
1018
|
-
- `useScrolled(threshold)` → boolean for scroll-based header styling
|
|
1019
|
-
- `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
|
|
1020
|
-
- `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
|
|
1021
|
-
- `useActiveRoute()` → `{ route, rootSegment, isActive(page), isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
1022
|
-
- `useGridLayout(columns, { gap })` → responsive grid class string
|
|
1023
|
-
- `useTheme(name)` → standardized theme classes
|
|
1024
|
-
- `useAppearance()` → `{ scheme, toggle, canToggle, setScheme, schemes }` — light/dark mode control with localStorage persistence
|
|
1025
|
-
- `useRouting()` → `{ useLocation, useParams, useNavigate, Link, isRoutingAvailable }` — SSG-safe routing access (returns no-op fallbacks during prerender)
|
|
1026
|
-
- `useWebsite()` → `{ website, localize, makeHref, getLanguage, getLanguages, getRoutingComponents }` — primary runtime hook
|
|
1027
|
-
- `useThemeData()` → Theme instance for programmatic color access (`getColor(name, shade)`, `getPalette(name)`)
|
|
1028
|
-
- `useColorContext(block)` → `'light' | 'medium' | 'dark'` — current section's color context
|
|
991
|
+
`SliderHero` uses `images`, `interval`, and `transition`; it ignores `formData` and `compact`. `ContactHero` uses `formData` and `compact`; it ignores `images` and `interval`. Each worker takes what it needs. Some params only matter for certain variants (`slideInterval` for slider, `density` for contact). Some are high-level names that the front desk translates into developer-oriented values (`style: dramatic` → `transition="zoom"`). The content author writes `variant: contact` — they don't know or care about `ContactHero`.
|
|
1029
992
|
|
|
1030
|
-
**
|
|
993
|
+
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.
|
|
1031
994
|
|
|
1032
995
|
### Foundation Organization
|
|
1033
996
|
|
|
1034
997
|
```
|
|
1035
998
|
foundation/src/
|
|
1036
|
-
├── sections/ # Section types (auto-discovered
|
|
1037
|
-
│ ├── Hero
|
|
1038
|
-
│
|
|
999
|
+
├── sections/ # Section types (auto-discovered)
|
|
1000
|
+
│ ├── Hero.jsx # Bare file — no folder needed
|
|
1001
|
+
│ ├── Features/ # Folder when you need meta.js
|
|
1002
|
+
│ │ ├── index.jsx
|
|
1039
1003
|
│ │ └── meta.js
|
|
1040
|
-
│ └──
|
|
1041
|
-
│
|
|
1042
|
-
│
|
|
1004
|
+
│ └── insets/ # Organizational subdirectory (lowercase)
|
|
1005
|
+
│ └── Diagram/
|
|
1006
|
+
│ ├── index.jsx
|
|
1007
|
+
│ └── meta.js
|
|
1043
1008
|
├── components/ # Your React components (no meta.js, not selectable)
|
|
1044
|
-
│ ├── ui/
|
|
1009
|
+
│ ├── ui/
|
|
1045
1010
|
│ │ └── button.jsx
|
|
1046
1011
|
│ └── Card.jsx
|
|
1047
1012
|
└── styles.css
|
|
1048
1013
|
```
|
|
1049
1014
|
|
|
1050
|
-
|
|
1015
|
+
**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.
|
|
1051
1016
|
|
|
1052
1017
|
### Website and Page APIs
|
|
1053
1018
|
|
|
1054
1019
|
```jsx
|
|
1055
|
-
const { website } = useWebsite()
|
|
1056
|
-
const page = website.activePage
|
|
1020
|
+
const { website } = useWebsite()
|
|
1021
|
+
const page = website.activePage
|
|
1057
1022
|
|
|
1058
1023
|
// Navigation
|
|
1059
|
-
|
|
1024
|
+
website.getPageHierarchy({ for: 'header' })
|
|
1060
1025
|
// → [{ route, navigableRoute, label, hasContent, children }]
|
|
1061
1026
|
|
|
1027
|
+
// Core properties
|
|
1028
|
+
website.name // Site name from site.yml
|
|
1029
|
+
website.basePath // Deployment base path (e.g., '/docs/')
|
|
1030
|
+
|
|
1062
1031
|
// Locale
|
|
1063
1032
|
website.hasMultipleLocales()
|
|
1064
1033
|
website.getLocales() // [{ code, label, isDefault }]
|
|
1065
|
-
website.getActiveLocale()
|
|
1034
|
+
website.getActiveLocale()
|
|
1066
1035
|
website.getLocaleUrl('es')
|
|
1067
1036
|
|
|
1068
|
-
// Core properties
|
|
1069
|
-
website.name // Site name from site.yml
|
|
1070
|
-
website.basePath // Deployment base path (e.g., '/docs/')
|
|
1071
|
-
|
|
1072
1037
|
// Route detection
|
|
1073
1038
|
const { isActive, isActiveOrAncestor } = useActiveRoute()
|
|
1074
|
-
isActive(page) // Exact match
|
|
1075
|
-
isActiveOrAncestor(page) // Ancestor match (for parent highlighting in nav)
|
|
1076
1039
|
|
|
1077
|
-
// Appearance
|
|
1040
|
+
// Appearance
|
|
1078
1041
|
const { scheme, toggle, canToggle } = useAppearance()
|
|
1079
1042
|
|
|
1080
1043
|
// Page properties
|
|
1081
|
-
page.title
|
|
1082
|
-
page.
|
|
1083
|
-
page.
|
|
1084
|
-
page.isHidden() // Hidden from navigation
|
|
1085
|
-
page.showInHeader() // Visible in header nav
|
|
1086
|
-
page.showInFooter() // Visible in footer nav
|
|
1087
|
-
page.hasChildren() // Has child pages
|
|
1088
|
-
page.children // Array of child Page objects
|
|
1044
|
+
page.title, page.label, page.route
|
|
1045
|
+
page.isHidden(), page.showInHeader(), page.showInFooter()
|
|
1046
|
+
page.hasChildren(), page.children
|
|
1089
1047
|
```
|
|
1090
1048
|
|
|
1091
|
-
###
|
|
1049
|
+
### Cross-Block Communication
|
|
1092
1050
|
|
|
1093
|
-
|
|
1051
|
+
Section types sometimes need to coordinate. The typical case: a Header needs to know whether the section below it supports a floating translucent overlay — a Hero with a full-bleed background does, a plain text section doesn't. The section that **owns the capability declares it**; the section that **needs to adapt reads it**.
|
|
1094
1052
|
|
|
1095
|
-
|
|
1096
|
-
import { Visual } from '@uniweb/kit'
|
|
1053
|
+
`getBlockInfo()` exposes two channels:
|
|
1097
1054
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
return (
|
|
1101
|
-
<div className="flex gap-12">
|
|
1102
|
-
<div className="flex-1">
|
|
1103
|
-
<h2 className="text-heading">{content.title}</h2>
|
|
1104
|
-
</div>
|
|
1105
|
-
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-lg" />
|
|
1106
|
-
</div>
|
|
1107
|
-
)
|
|
1108
|
-
}
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
- `<Visual>` — renders first non-empty candidate from the props you pass (`inset`, `video`, `image`)
|
|
1112
|
-
- `<Render>` / `<Section>` — automatically handles `@Component` references placed in content flow
|
|
1113
|
-
- `block.insets` — array of Block instances from `@` references
|
|
1114
|
-
- `block.getInset(refId)` — lookup by refId (used by sequential renderers)
|
|
1115
|
-
- `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
|
|
1055
|
+
- **`context`** — Static capabilities from `meta.js`. Never changes. The declaring section type always has this capability.
|
|
1056
|
+
- **`state`** — Dynamic runtime state via `useBlockState()`. Can change based on component logic. Initial value comes from `initialState` in `meta.js`.
|
|
1116
1057
|
|
|
1117
|
-
|
|
1058
|
+
```jsx
|
|
1059
|
+
// Header reads the next section's info to decide how to render
|
|
1060
|
+
const nextBlockInfo = block.getNextBlockInfo()
|
|
1061
|
+
// nextBlockInfo.context → static (meta.js)
|
|
1062
|
+
// nextBlockInfo.state → dynamic (useBlockState)
|
|
1063
|
+
```
|
|
1118
1064
|
|
|
1119
|
-
|
|
1065
|
+
**Static context** — Hero declares a permanent capability, Header reads it:
|
|
1120
1066
|
|
|
1121
1067
|
```js
|
|
1122
|
-
//
|
|
1068
|
+
// Hero/meta.js — "I always support a translucent header over me"
|
|
1123
1069
|
export default {
|
|
1124
|
-
|
|
1125
|
-
params: { variant: { type: 'select', options: ['full', 'compact'], default: 'full' } },
|
|
1070
|
+
context: { allowTranslucentTop: true },
|
|
1126
1071
|
}
|
|
1127
1072
|
```
|
|
1128
1073
|
|
|
1129
|
-
Whether an inset appears in a section palette is a concern of the parent component (via `children` and `insets` in its meta.js), not a property of the inset itself. Don't use `hidden: true` on insets — `hidden` means "don't export this component at all" (internal helpers, not-yet-ready components).
|
|
1130
|
-
|
|
1131
|
-
### Dispatcher Pattern
|
|
1132
|
-
|
|
1133
|
-
One section type with a `variant` param replaces multiple near-duplicates. Instead of `HeroLeft`, `HeroCentered`, `HeroSplit` — one `Hero` with `variant: left | centered | split`:
|
|
1134
|
-
|
|
1135
1074
|
```jsx
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
<div className={`flex gap-16 items-center ${flipped ? 'flex-row-reverse' : ''}`}>
|
|
1140
|
-
<div className="flex-1">
|
|
1141
|
-
{content.pretitle && (
|
|
1142
|
-
<p className="text-xs font-bold uppercase tracking-widest text-subtle mb-4">
|
|
1143
|
-
{content.pretitle}
|
|
1144
|
-
</p>
|
|
1145
|
-
)}
|
|
1146
|
-
<h2 className="text-heading text-3xl font-bold">{content.title}</h2>
|
|
1147
|
-
<p className="text-body mt-4">{content.paragraphs[0]}</p>
|
|
1148
|
-
</div>
|
|
1149
|
-
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-2xl" />
|
|
1150
|
-
</div>
|
|
1151
|
-
)
|
|
1152
|
-
}
|
|
1075
|
+
// Header/index.jsx — adapts based on what's below
|
|
1076
|
+
const nextBlockInfo = block.getNextBlockInfo()
|
|
1077
|
+
const isFloating = nextBlockInfo?.context?.allowTranslucentTop || false
|
|
1153
1078
|
```
|
|
1154
1079
|
|
|
1080
|
+
**Dynamic state** — Hero declares an initial value but can change it at runtime:
|
|
1081
|
+
|
|
1155
1082
|
```js
|
|
1156
|
-
// meta.js
|
|
1083
|
+
// Hero/meta.js — starts as true, but component logic may change it
|
|
1157
1084
|
export default {
|
|
1158
|
-
|
|
1159
|
-
content: { pretitle: 'Eyebrow label', title: 'Section heading', paragraphs: 'Description' },
|
|
1160
|
-
params: {
|
|
1161
|
-
variant: { type: 'select', options: ['default', 'flipped'], default: 'default' },
|
|
1162
|
-
},
|
|
1085
|
+
initialState: { allowTranslucentTop: true },
|
|
1163
1086
|
}
|
|
1164
1087
|
```
|
|
1165
1088
|
|
|
1166
|
-
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.
|
|
1167
|
-
|
|
1168
|
-
### Cross-Block Communication
|
|
1169
|
-
|
|
1170
|
-
Components read neighboring blocks for adaptive behavior (e.g., translucent header over hero):
|
|
1171
|
-
|
|
1172
1089
|
```jsx
|
|
1173
|
-
|
|
1174
|
-
|
|
1090
|
+
// Hero/index.jsx — conditionally updates
|
|
1091
|
+
function Hero({ content, block }) {
|
|
1092
|
+
const [state, setState] = block.useBlockState(useState)
|
|
1093
|
+
// state.allowTranslucentTop is true initially (from meta.js)
|
|
1094
|
+
// Component logic can change it: setState({ allowTranslucentTop: false })
|
|
1095
|
+
}
|
|
1096
|
+
```
|
|
1175
1097
|
|
|
1176
|
-
|
|
1098
|
+
```jsx
|
|
1099
|
+
// Header/index.jsx — reads dynamic state, falls back to static context
|
|
1100
|
+
const nextBlockInfo = block.getNextBlockInfo()
|
|
1101
|
+
const isFloating = nextBlockInfo?.state?.allowTranslucentTop
|
|
1102
|
+
?? nextBlockInfo?.context?.allowTranslucentTop
|
|
1103
|
+
?? false
|
|
1177
1104
|
```
|
|
1178
1105
|
|
|
1106
|
+
The key names (`allowTranslucentTop`, `expanded`, etc.) are yours to design — they're not framework fields. Define whatever protocol your foundation's sections need.
|
|
1107
|
+
|
|
1108
|
+
Other navigation methods: `block.getPrevBlockInfo()`, `block.page.getFirstBodyBlockInfo()`.
|
|
1109
|
+
|
|
1179
1110
|
### Custom Layouts
|
|
1180
1111
|
|
|
1181
|
-
Layouts live in `foundation/src/layouts/` and are auto-discovered
|
|
1112
|
+
Layouts live in `foundation/src/layouts/` and are auto-discovered:
|
|
1182
1113
|
|
|
1183
1114
|
```js
|
|
1184
1115
|
// foundation/src/foundation.js
|
|
1185
1116
|
export default {
|
|
1186
|
-
name: 'My Template',
|
|
1187
|
-
description: 'A brief description',
|
|
1117
|
+
name: 'My Template',
|
|
1118
|
+
description: 'A brief description',
|
|
1188
1119
|
defaultLayout: 'DocsLayout',
|
|
1189
1120
|
}
|
|
1190
1121
|
```
|
|
@@ -1206,80 +1137,67 @@ export default function DocsLayout({ header, body, footer, left, right, params }
|
|
|
1206
1137
|
}
|
|
1207
1138
|
```
|
|
1208
1139
|
|
|
1209
|
-
Layout
|
|
1140
|
+
**Layout meta.js** declares areas: `{ areas: ['header', 'footer', 'left'] }`. Area names are arbitrary.
|
|
1210
1141
|
|
|
1211
|
-
**Layout
|
|
1212
|
-
|
|
1213
|
-
```js
|
|
1214
|
-
// foundation/src/layouts/DocsLayout/meta.js
|
|
1215
|
-
export default {
|
|
1216
|
-
areas: ['header', 'footer', 'left'],
|
|
1217
|
-
}
|
|
1218
|
-
```
|
|
1219
|
-
|
|
1220
|
-
Area names are arbitrary strings — `header`, `footer`, `left`, `right` are conventional, but a dashboard layout could use `topbar`, `sidebar`, `statusbar`.
|
|
1221
|
-
|
|
1222
|
-
**Site-side layout content** — each layout can have its own section files:
|
|
1142
|
+
**Layout content** — each layout has section files in `site/layout/`:
|
|
1223
1143
|
|
|
1224
1144
|
```
|
|
1225
1145
|
site/layout/
|
|
1226
|
-
├── header.md # Default layout
|
|
1146
|
+
├── header.md # Default layout
|
|
1227
1147
|
├── footer.md
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
├── header.md # Different header for marketing pages
|
|
1148
|
+
└── marketing/ # Named layout sections
|
|
1149
|
+
├── header.md
|
|
1231
1150
|
└── footer.md
|
|
1232
1151
|
```
|
|
1233
1152
|
|
|
1234
|
-
Named subdirectories are self-contained — no inheritance
|
|
1235
|
-
|
|
1236
|
-
**Layout cascade** (first match wins): `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
|
|
1153
|
+
Named subdirectories are self-contained — no inheritance. Layout cascade: `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
|
|
1237
1154
|
|
|
1238
|
-
|
|
1239
|
-
# page.yml — select layout and hide areas
|
|
1240
|
-
layout:
|
|
1241
|
-
name: MarketingLayout
|
|
1242
|
-
hide: [left, right]
|
|
1243
|
-
```
|
|
1155
|
+
---
|
|
1244
1156
|
|
|
1245
1157
|
## Migrating From Other Frameworks
|
|
1246
1158
|
|
|
1247
|
-
Don't port line-by-line. Study the
|
|
1248
|
-
|
|
1249
|
-
### Why fewer components
|
|
1159
|
+
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.
|
|
1250
1160
|
|
|
1251
|
-
|
|
1161
|
+
### The mental model shift
|
|
1252
1162
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
-
|
|
1163
|
+
| React / conventional | Uniweb equivalent |
|
|
1164
|
+
|---|---|
|
|
1165
|
+
| Props with typed data | Frontmatter params + `meta.js` |
|
|
1166
|
+
| Component variants via props | `variant` param in frontmatter; Front Desk pattern for complex routing |
|
|
1167
|
+
| Context / ThemeProvider | `theme:` frontmatter + semantic tokens (automatic) |
|
|
1168
|
+
| Wrapper/layout components | Section nesting or custom layouts |
|
|
1169
|
+
| Prop-drilling visuals into containers | Insets — `` rendered via `<Visual>` |
|
|
1170
|
+
| Content in JSX or `.js` data files | Markdown → parser → `content` prop |
|
|
1171
|
+
| CSS color tokens / design systems | `theme.yml` → palette shades + semantic tokens |
|
|
1172
|
+
| `isDark ? ... : ...` conditionals | `text-heading` — context classes handle it |
|
|
1173
|
+
| Per-component backgrounds | `background:` in frontmatter |
|
|
1174
|
+
| Multiple near-identical components | One section type + `variant` param, or Front Desk pattern |
|
|
1175
|
+
| i18n wrapping (`t()` / `<Trans>`) | Locale-specific content directories |
|
|
1260
1176
|
|
|
1261
1177
|
### Migration approach
|
|
1262
1178
|
|
|
1263
|
-
1. **
|
|
1179
|
+
1. **Scaffold the workspace:**
|
|
1264
1180
|
```bash
|
|
1265
1181
|
pnpm create uniweb my-project --template none
|
|
1266
1182
|
```
|
|
1267
1183
|
|
|
1268
|
-
|
|
1184
|
+
2. **Use named layouts** for different page groups — marketing layout for landing pages, docs layout for `/docs/*`.
|
|
1269
1185
|
|
|
1270
|
-
|
|
1186
|
+
3. **Dump legacy components under `src/components/`** — they're not section types. Import from section types during transition.
|
|
1271
1187
|
|
|
1272
|
-
|
|
1273
|
-
- **Level 0**: Paste the
|
|
1274
|
-
- **Level 1**: Decompose into section types.
|
|
1275
|
-
- **Level 2**: Move content from JSX to markdown.
|
|
1276
|
-
- **Level 3**: Replace hardcoded
|
|
1188
|
+
4. **Create section types one at a time.** Migration levels:
|
|
1189
|
+
- **Level 0**: Paste the original as one section type. Routing and dev tooling work immediately.
|
|
1190
|
+
- **Level 1**: Decompose into section types. Consolidate duplicates — use `variant` params or the Front Desk pattern.
|
|
1191
|
+
- **Level 2**: Move content from JSX to markdown. Authors can now edit without code.
|
|
1192
|
+
- **Level 3**: Replace hardcoded colors with semantic tokens. Components work in any context.
|
|
1277
1193
|
|
|
1278
|
-
|
|
1194
|
+
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.
|
|
1279
1195
|
|
|
1280
|
-
|
|
1196
|
+
6. **Name by purpose, not content** — `TheModel` → `SplitContent`, `WorkModes` → `FeatureColumns`.
|
|
1281
1197
|
|
|
1282
|
-
|
|
1198
|
+
7. **UI helpers → `components/`** — Buttons, badges, cards in `src/components/` (no `meta.js`, not selectable by authors).
|
|
1199
|
+
|
|
1200
|
+
---
|
|
1283
1201
|
|
|
1284
1202
|
## Tailwind CSS v4
|
|
1285
1203
|
|
|
@@ -1287,55 +1205,74 @@ Foundation styles in `foundation/src/styles.css`:
|
|
|
1287
1205
|
|
|
1288
1206
|
```css
|
|
1289
1207
|
@import "tailwindcss";
|
|
1290
|
-
@import "@uniweb/kit/theme-tokens.css";
|
|
1208
|
+
@import "@uniweb/kit/theme-tokens.css";
|
|
1291
1209
|
@source "./sections/**/*.{js,jsx}";
|
|
1292
|
-
@source "./components/**/*.{js,jsx}";
|
|
1210
|
+
@source "./components/**/*.{js,jsx}";
|
|
1293
1211
|
@source "../node_modules/@uniweb/kit/src/**/*.jsx";
|
|
1294
1212
|
|
|
1295
1213
|
@theme {
|
|
1296
|
-
/* Additional custom values — NOT for colors already in theme.yml */
|
|
1297
1214
|
--breakpoint-xs: 30rem;
|
|
1298
1215
|
}
|
|
1299
1216
|
```
|
|
1300
1217
|
|
|
1301
|
-
Semantic
|
|
1302
|
-
|
|
1303
|
-
**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.
|
|
1218
|
+
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.
|
|
1304
1219
|
|
|
1305
1220
|
## Troubleshooting
|
|
1306
1221
|
|
|
1307
|
-
**"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"
|
|
1222
|
+
**"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"`.
|
|
1308
1223
|
|
|
1309
|
-
**Component not appearing** — Verify `meta.js` exists. Check for `hidden: true
|
|
1224
|
+
**Component not appearing** — Verify `meta.js` exists. Check for `hidden: true`. Rebuild: `cd foundation && pnpm build`.
|
|
1310
1225
|
|
|
1311
|
-
**Styles not applying** — Verify `@source`
|
|
1226
|
+
**Styles not applying** — Verify `@source` includes your component paths.
|
|
1312
1227
|
|
|
1313
|
-
**Prerender warnings about hooks
|
|
1228
|
+
**Prerender warnings about hooks** — Components with useState/useEffect show SSG warnings during build. Expected and harmless.
|
|
1314
1229
|
|
|
1315
|
-
**Content not appearing as expected?**
|
|
1230
|
+
**Content not appearing as expected?**
|
|
1231
|
+
```bash
|
|
1232
|
+
pnpm uniweb inspect pages/home/hero.md # Single section
|
|
1233
|
+
pnpm uniweb inspect pages/home/ # Whole page
|
|
1234
|
+
pnpm uniweb inspect pages/home/hero.md --raw # ProseMirror AST
|
|
1235
|
+
```
|
|
1316
1236
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1237
|
+
## Learning from Official Templates
|
|
1238
|
+
|
|
1239
|
+
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:
|
|
1240
|
+
|
|
1241
|
+
```bash
|
|
1242
|
+
pnpm uniweb add project marketing --from marketing
|
|
1243
|
+
pnpm install
|
|
1319
1244
|
```
|
|
1320
1245
|
|
|
1321
|
-
|
|
1246
|
+
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.
|
|
1247
|
+
|
|
1248
|
+
**What to study:**
|
|
1249
|
+
- `{name}/foundation/src/sections/` — components with meta.js (content expectations, params, presets)
|
|
1250
|
+
- `{name}/site/pages/` — real content files showing markdown → component mapping
|
|
1251
|
+
- `{name}/site/theme.yml` + `site.yml` — theming and configuration patterns
|
|
1252
|
+
|
|
1253
|
+
**Available templates:**
|
|
1254
|
+
|
|
1255
|
+
| Template | Demonstrates |
|
|
1256
|
+
|----------|-------------|
|
|
1257
|
+
| `marketing` | Semantic tokens, insets, grids, multi-line headings, inline styling |
|
|
1258
|
+
| `docs` | Sidebar navigation, navigation levels, code highlighting |
|
|
1259
|
+
| `dynamic` | Live API data fetching, loading states, transforms |
|
|
1260
|
+
| `international` | i18n, blog with collections, multi-locale routing |
|
|
1261
|
+
| `store` | Product grid, collections, e-commerce patterns |
|
|
1262
|
+
| `academic` | Publications, team grid, timeline, math |
|
|
1263
|
+
| `extensions` | Multi-foundation architecture, runtime loading |
|
|
1264
|
+
|
|
1265
|
+
You can install multiple templates. Each becomes an independent project in the workspace. To run one in dev: `cd {name}/site && pnpm dev`
|
|
1322
1266
|
|
|
1323
1267
|
## Further Documentation
|
|
1324
1268
|
|
|
1325
|
-
Full
|
|
1269
|
+
Full documentation: **https://github.com/uniweb/docs**
|
|
1326
1270
|
|
|
1327
1271
|
| Section | Path | Topics |
|
|
1328
1272
|
|---------|------|--------|
|
|
1329
|
-
| **Getting Started** | `getting-started/` | What is Uniweb, quickstart
|
|
1330
|
-
| **Authoring** | `authoring/` | Writing content, site setup, collections, theming,
|
|
1331
|
-
| **Development** | `development/` |
|
|
1332
|
-
| **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit
|
|
1333
|
-
|
|
1334
|
-
**Quick access
|
|
1335
|
-
|
|
1336
|
-
Examples:
|
|
1337
|
-
- Content structure details: `reference/content-structure.md`
|
|
1338
|
-
- Component metadata (meta.js): `reference/component-metadata.md`
|
|
1339
|
-
- Kit hooks and components: `reference/kit-reference.md`
|
|
1340
|
-
- Theming tokens: `reference/site-theming.md`
|
|
1341
|
-
- Data fetching patterns: `reference/data-fetching.md`
|
|
1273
|
+
| **Getting Started** | `getting-started/` | What is Uniweb, quickstart, templates |
|
|
1274
|
+
| **Authoring** | `authoring/` | Writing content, site setup, collections, theming, translations |
|
|
1275
|
+
| **Development** | `development/` | Foundations, component patterns, data fetching, layouts, i18n |
|
|
1276
|
+
| **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit API, CLI, deployment |
|
|
1277
|
+
|
|
1278
|
+
**Quick access:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
|