uniweb 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/partials/agents.md +809 -0
- package/src/commands/add.js +8 -2
- package/src/commands/build.js +23 -46
- package/src/utils/config.js +1 -1
- package/templates/workspace/AGENTS.md.hbs +1 -1
- package/partials/agents-md.hbs +0 -431
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Uniweb is a Component Content Architecture (CCA). Content lives in markdown, code lives in React components, and a runtime connects them. The runtime handles section wrapping, background rendering, context theming, and token resolution — components receive pre-parsed content and render it with semantic tokens. Understanding what the runtime does (and therefore what components should *not* do) is the key to working effectively in this architecture.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
project/
|
|
9
|
+
├── foundation/ # React component library
|
|
10
|
+
├── site/ # Content (markdown pages)
|
|
11
|
+
└── pnpm-workspace.yaml
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Multi-site variant uses `foundations/` and `sites/` (plural) folders.
|
|
15
|
+
|
|
16
|
+
- **Foundation**: React components. Those with `meta.js` are *section types* — selectable by content authors via `type:` in frontmatter. Everything else is ordinary React.
|
|
17
|
+
- **Site**: Markdown content + configuration. Each section file references a section type.
|
|
18
|
+
|
|
19
|
+
## Project Setup
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
### New workspace
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm create uniweb my-project
|
|
27
|
+
cd my-project && pnpm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Use `--template blank` for an empty workspace, or `--template <name>` for an official template (`marketing`, `docs`, `academic`, etc.).
|
|
31
|
+
|
|
32
|
+
### Adding to an existing workspace
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm uniweb add foundation myname --project myname
|
|
36
|
+
pnpm uniweb add site myname --project myname
|
|
37
|
+
pnpm install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `--project` flag co-locates foundation and site under `myname/`. The CLI names them `myname` (foundation) and `myname-site` (site) to avoid workspace name collisions.
|
|
41
|
+
|
|
42
|
+
### What the CLI generates
|
|
43
|
+
|
|
44
|
+
**Foundation** (`vite.config.js`, `package.json`, `src/foundation.js`, `src/styles.css`):
|
|
45
|
+
- `defineFoundationConfig()` in vite.config.js
|
|
46
|
+
- Dependencies pinned to current npm versions
|
|
47
|
+
- `@import "@uniweb/kit/theme-tokens.css"` in styles.css
|
|
48
|
+
|
|
49
|
+
**Site** (`vite.config.js`, `package.json`, `main.js`, `index.html`, `site.yml`):
|
|
50
|
+
- `defineSiteConfig()` in vite.config.js
|
|
51
|
+
- `react-router-dom` in devDependencies (required by pnpm strict mode)
|
|
52
|
+
- Standard `start()` call in main.js
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm install # Install dependencies
|
|
58
|
+
pnpm dev # Start dev server
|
|
59
|
+
pnpm build # Build for production
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Content Authoring
|
|
63
|
+
|
|
64
|
+
### Section Format
|
|
65
|
+
|
|
66
|
+
Each `.md` file is a section. Frontmatter on top, content below:
|
|
67
|
+
|
|
68
|
+
```markdown
|
|
69
|
+
---
|
|
70
|
+
type: Hero
|
|
71
|
+
theme: dark
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### Eyebrow Text ← pretitle (heading before a more important one)
|
|
75
|
+
|
|
76
|
+
# Main Headline ← title
|
|
77
|
+
|
|
78
|
+
## Subtitle ← subtitle
|
|
79
|
+
|
|
80
|
+
Description paragraph.
|
|
81
|
+
|
|
82
|
+
[Call to Action](/link)
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Content Shape
|
|
88
|
+
|
|
89
|
+
The semantic parser extracts markdown into a flat, guaranteed structure. No null checks needed — empty strings/arrays if content is absent:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
content = {
|
|
93
|
+
title: '', // Main heading
|
|
94
|
+
pretitle: '', // Heading before main title (auto-detected)
|
|
95
|
+
subtitle: '', // Heading after title
|
|
96
|
+
subtitle2: '', // Third-level heading
|
|
97
|
+
paragraphs: [], // Text blocks
|
|
98
|
+
links: [], // { href, label, role } — standalone links become buttons
|
|
99
|
+
imgs: [], // { src, alt, role }
|
|
100
|
+
icons: [], // { library, name, role }
|
|
101
|
+
videos: [], // Video embeds
|
|
102
|
+
insets: [], // Inline @Component references — { refId }
|
|
103
|
+
lists: [], // Bullet/ordered lists
|
|
104
|
+
quotes: [], // Blockquotes
|
|
105
|
+
data: {}, // From tagged code blocks (```yaml:tagname)
|
|
106
|
+
headings: [], // Overflow headings after subtitle2
|
|
107
|
+
items: [], // Each has the same flat structure — from headings after body content
|
|
108
|
+
sequence: [], // All elements in document order
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Items** are repeating content groups (cards, features, FAQ entries). Created when a heading appears after body content:
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
# Our Features ← title
|
|
116
|
+
|
|
117
|
+
We built this for you. ← paragraph
|
|
118
|
+
|
|
119
|
+
### Fast ← items[0].title
|
|
120
|
+
Lightning quick. ← items[0].paragraphs[0]
|
|
121
|
+
|
|
122
|
+
### Secure ← items[1].title
|
|
123
|
+
Enterprise-grade. ← items[1].paragraphs[0]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Icons
|
|
127
|
+
|
|
128
|
+
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/).
|
|
129
|
+
|
|
130
|
+
Custom SVGs: `{role=icon}`
|
|
131
|
+
|
|
132
|
+
### Insets (Component References)
|
|
133
|
+
|
|
134
|
+
Place a foundation component inline within content using `@` syntax:
|
|
135
|
+
|
|
136
|
+
```markdown
|
|
137
|
+

|
|
138
|
+
{param=value other=thing}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The three parts carry distinct information:
|
|
142
|
+
- `[description]` — text passed to the component as `block.content.title`
|
|
143
|
+
- `(@Name)` — foundation component to render
|
|
144
|
+
- `{params}` — configuration attributes passed as `block.properties`
|
|
145
|
+
|
|
146
|
+
```markdown
|
|
147
|
+
{variant=compact}
|
|
148
|
+
{period=30d}
|
|
149
|
+
{position=top-right}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
### Links and Media Attributes
|
|
155
|
+
|
|
156
|
+
```markdown
|
|
157
|
+
[text](url){target=_blank} <!-- Open in new tab -->
|
|
158
|
+
[text](./file.pdf){download} <!-- Download -->
|
|
159
|
+
{role=banner} <!-- Role determines array: imgs, icons, or videos -->
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Standalone links (alone on a line) become buttons. Inline links stay as text links.
|
|
163
|
+
|
|
164
|
+
### Structured Data
|
|
165
|
+
|
|
166
|
+
Tagged code blocks pass structured data via `content.data`:
|
|
167
|
+
|
|
168
|
+
````markdown
|
|
169
|
+
```yaml:form
|
|
170
|
+
fields:
|
|
171
|
+
- name: email
|
|
172
|
+
type: email
|
|
173
|
+
submitLabel: Send
|
|
174
|
+
```
|
|
175
|
+
````
|
|
176
|
+
|
|
177
|
+
Access: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
|
|
178
|
+
|
|
179
|
+
### Section Backgrounds
|
|
180
|
+
|
|
181
|
+
Set `background` in frontmatter — the runtime renders it automatically:
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
---
|
|
185
|
+
type: Hero
|
|
186
|
+
theme: dark
|
|
187
|
+
background: /images/hero.jpg # Simple: URL (image or video auto-detected)
|
|
188
|
+
---
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Full syntax supports `image`, `video`, `gradient`, `color` modes plus overlays:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
background:
|
|
195
|
+
image: { src: /img.jpg, position: center top }
|
|
196
|
+
overlay: { enabled: true, type: dark, opacity: 0.5 }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Components that render their own background declare `background: 'self'` in `meta.js`.
|
|
200
|
+
|
|
201
|
+
### Page Organization
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
site/layout/
|
|
205
|
+
├── header.md # type: Header — rendered on every page
|
|
206
|
+
├── footer.md # type: Footer — rendered on every page
|
|
207
|
+
└── left.md # type: Sidebar — optional sidebar
|
|
208
|
+
|
|
209
|
+
site/pages/
|
|
210
|
+
└── home/
|
|
211
|
+
├── page.yml # title, description, order
|
|
212
|
+
├── hero.md # Single section — no prefix needed
|
|
213
|
+
└── (or for multi-section pages:)
|
|
214
|
+
├── 1-hero.md # Numeric prefix sets order
|
|
215
|
+
├── 2-features.md
|
|
216
|
+
└── 3-cta.md
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`.
|
|
220
|
+
|
|
221
|
+
**Ignored files/folders:**
|
|
222
|
+
- `README.md` — repo documentation, not site content
|
|
223
|
+
- `_*.md` or `_*/` — drafts and private content (e.g., `_drafts/`, `_old-hero.md`)
|
|
224
|
+
|
|
225
|
+
**page.yml:**
|
|
226
|
+
```yaml
|
|
227
|
+
title: About Us
|
|
228
|
+
description: Learn about our company
|
|
229
|
+
order: 2 # Navigation sort position
|
|
230
|
+
pages: [team, history, ...] # Child page order (... = rest). Without ... = strict (hides unlisted)
|
|
231
|
+
index: getting-started # Which child page is the index
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**site.yml:**
|
|
235
|
+
```yaml
|
|
236
|
+
index: home # Just set the homepage
|
|
237
|
+
pages: [home, about, ...] # Order pages (... = rest, first = homepage)
|
|
238
|
+
pages: [home, about] # Strict: only listed pages in nav
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use `pages:` with `...` for ordering, without `...` for strict visibility control. Use `index:` for simple homepage selection.
|
|
242
|
+
|
|
243
|
+
### Section Nesting (Child Sections)
|
|
244
|
+
|
|
245
|
+
Some section types need children — a Grid that arranges cards, a TabGroup that holds panels. Use the `@` prefix and `nest:` property:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
pages/home/
|
|
249
|
+
├── page.yml
|
|
250
|
+
├── 1-hero.md
|
|
251
|
+
├── 2-features.md # Parent section (type: Grid)
|
|
252
|
+
├── 3-cta.md
|
|
253
|
+
├── @card-a.md # Child of features (@ = not top-level)
|
|
254
|
+
├── @card-b.md
|
|
255
|
+
└── @card-c.md
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```yaml
|
|
259
|
+
# page.yml
|
|
260
|
+
nest:
|
|
261
|
+
features: [card-a, card-b, card-c]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Rules:**
|
|
265
|
+
- `@`-prefixed files are excluded from the top-level section list
|
|
266
|
+
- `nest:` declares parent-child relationships (parent name → array of child names)
|
|
267
|
+
- Child files **must** use the `@` prefix — the filename and YAML must agree
|
|
268
|
+
- `@@` prefix signals deeper nesting (e.g., `@@sub-item.md` for grandchildren)
|
|
269
|
+
- `nest:` is flat — each key is a parent: `nest: { features: [a, b], a: [sub-1] }`
|
|
270
|
+
- Children are ordered by their position in the `nest:` array
|
|
271
|
+
- Orphaned `@` files (no parent in `nest:`) appear at top-level with a warning
|
|
272
|
+
|
|
273
|
+
Components receive children via `block.childBlocks`:
|
|
274
|
+
|
|
275
|
+
```jsx
|
|
276
|
+
export default function Grid({ block, params }) {
|
|
277
|
+
return (
|
|
278
|
+
<div className={`grid grid-cols-${params.columns || 2} gap-6`}>
|
|
279
|
+
{block.childBlocks.map(child => {
|
|
280
|
+
const Comp = child.initComponent()
|
|
281
|
+
return Comp ? <Comp key={child.id} block={child} /> : null
|
|
282
|
+
})}
|
|
283
|
+
</div>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Composition in practice
|
|
289
|
+
|
|
290
|
+
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:
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
pages/home/
|
|
294
|
+
├── page.yml
|
|
295
|
+
├── 1-hero.md
|
|
296
|
+
├── 2-highlights.md # type: Grid, columns: 3
|
|
297
|
+
├── 3-cta.md
|
|
298
|
+
├── @stats.md # type: StatCard — numbers and labels
|
|
299
|
+
├── @testimonial.md # type: Testimonial — quote with attribution
|
|
300
|
+
└── @demo.md # type: SplitContent — text +  inset
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```yaml
|
|
304
|
+
nest:
|
|
305
|
+
highlights: [stats, testimonial, demo]
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The content author chose three different section types as children, arranged them in a grid, and embedded an interactive component inside one of them — all through markdown and YAML. The developer wrote one Grid component, a few card-level section types, and an inset. No bespoke "highlights" component needed.
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
### When to use which pattern
|
|
313
|
+
|
|
314
|
+
| Pattern | Authoring | Use when |
|
|
315
|
+
|---------|-----------|----------|
|
|
316
|
+
| **Items** (`content.items`) | Heading groups in one `.md` file | Repeating content within one section (cards, FAQ entries) |
|
|
317
|
+
| **Insets** (`block.insets`) | `` in markdown | Embedding a self-contained visual (chart, diagram, widget) |
|
|
318
|
+
| **Child sections** (`block.childBlocks`) | `@`-prefixed `.md` files + `nest:` | Children with rich authored content (testimonials, carousel slides) |
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
**Inset rule of thumb:** If the same interactive widget or self-contained visual appears inside multiple different sections (a copy-able command block, a chart, a demo player), make it an inset. The content author places it with `` wherever it's needed — no prop drilling, no imports.
|
|
323
|
+
|
|
324
|
+
## Semantic Theming
|
|
325
|
+
|
|
326
|
+
CCA separates theme from code. 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.
|
|
327
|
+
|
|
328
|
+
```jsx
|
|
329
|
+
// ❌ Hardcoded — breaks in dark context, locked to one palette
|
|
330
|
+
<h2 className="text-slate-900">...</h2>
|
|
331
|
+
|
|
332
|
+
// ✅ Semantic — adapts to any context and brand automatically
|
|
333
|
+
<h2 className="text-heading">...</h2>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Core tokens** (available as Tailwind classes):
|
|
337
|
+
|
|
338
|
+
| Token | Purpose |
|
|
339
|
+
|-------|---------|
|
|
340
|
+
| `text-heading` | Headings |
|
|
341
|
+
| `text-body` | Body text |
|
|
342
|
+
| `text-subtle` | Secondary/de-emphasized text |
|
|
343
|
+
| `bg-section` | Section background |
|
|
344
|
+
| `bg-card` | Card/panel background |
|
|
345
|
+
| `bg-muted` | Hover states, zebra rows |
|
|
346
|
+
| `border-border` | Borders |
|
|
347
|
+
| `text-link` | Link color |
|
|
348
|
+
| `bg-primary` | Primary action background |
|
|
349
|
+
| `text-primary-foreground` | Text on primary background |
|
|
350
|
+
| `bg-secondary` | Secondary action background |
|
|
351
|
+
| `text-success` / `bg-success-subtle` | Status: success |
|
|
352
|
+
| `text-error` / `bg-error-subtle` | Status: error |
|
|
353
|
+
| `text-warning` / `bg-warning-subtle` | Status: warning |
|
|
354
|
+
| `text-info` / `bg-info-subtle` | Status: info |
|
|
355
|
+
|
|
356
|
+
### What the runtime handles (don't write this yourself)
|
|
357
|
+
|
|
358
|
+
The runtime does significant work that other frameworks push onto components. Understanding this prevents writing unnecessary code:
|
|
359
|
+
|
|
360
|
+
1. **Section backgrounds** — The runtime renders image, video, gradient, color, and overlay backgrounds from frontmatter. Components never set their own section background.
|
|
361
|
+
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.
|
|
362
|
+
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.
|
|
363
|
+
4. **Colored section backgrounds** — Content authors create tinted sections via frontmatter, not component code:
|
|
364
|
+
```yaml
|
|
365
|
+
---
|
|
366
|
+
type: Features
|
|
367
|
+
theme: light
|
|
368
|
+
background:
|
|
369
|
+
color: var(--primary-50) # Light blue tint with light-context tokens
|
|
370
|
+
---
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**What components should NOT contain:**
|
|
374
|
+
|
|
375
|
+
| Don't write | Why |
|
|
376
|
+
|-------------|-----|
|
|
377
|
+
| `bg-white` or `bg-gray-900` on section wrapper | Engine applies `bg-section` via context class |
|
|
378
|
+
| `const themes = { light: {...}, dark: {...} }` | Context system replaces theme maps entirely |
|
|
379
|
+
| `isDark ? 'text-white' : 'text-gray-900'` | Just write `text-heading` — it adapts |
|
|
380
|
+
| Background rendering code | Declare `background:` in frontmatter instead |
|
|
381
|
+
| Color constants / tokens files | Colors come from `theme.yml` |
|
|
382
|
+
| Custom CSS variables for colors (`--ink`, `--paper`, `--accent`) in `styles.css` | Map source colors to `theme.yml` colors/neutral. The build generates `--primary-50` through `--primary-950`, `--neutral-50` through `--neutral-950`, etc. Components use semantic tokens (`text-heading`, `bg-section`) that resolve from these palettes per context. A parallel color system bypasses all of this. |
|
|
383
|
+
|
|
384
|
+
**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.
|
|
385
|
+
|
|
386
|
+
**Content authors control context** in frontmatter:
|
|
387
|
+
|
|
388
|
+
```markdown
|
|
389
|
+
---
|
|
390
|
+
type: Testimonial
|
|
391
|
+
theme: dark ← sets context-dark, all tokens resolve to dark values
|
|
392
|
+
---
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**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.
|
|
396
|
+
|
|
397
|
+
### theme.yml
|
|
398
|
+
|
|
399
|
+
```yaml
|
|
400
|
+
# site/theme.yml
|
|
401
|
+
colors:
|
|
402
|
+
primary:
|
|
403
|
+
base: '#3b82f6'
|
|
404
|
+
exactMatch: true # Use this exact hex at the 500 shade
|
|
405
|
+
secondary: '#64748b'
|
|
406
|
+
accent: '#8b5cf6'
|
|
407
|
+
neutral: stone # Named preset: stone, zinc, gray, slate, neutral
|
|
408
|
+
|
|
409
|
+
contexts:
|
|
410
|
+
light:
|
|
411
|
+
section: '#fafaf9' # Override individual tokens per context
|
|
412
|
+
primary: var(--primary-500)
|
|
413
|
+
primary-hover: var(--primary-600)
|
|
414
|
+
|
|
415
|
+
fonts:
|
|
416
|
+
import:
|
|
417
|
+
- url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'
|
|
418
|
+
heading: "'Inter', system-ui, sans-serif"
|
|
419
|
+
body: "'Inter', system-ui, sans-serif"
|
|
420
|
+
|
|
421
|
+
inline:
|
|
422
|
+
emphasis: # For [text]{emphasis} in markdown
|
|
423
|
+
color: var(--link)
|
|
424
|
+
font-weight: '600'
|
|
425
|
+
|
|
426
|
+
vars: # Override foundation-declared variables
|
|
427
|
+
header-height: 5rem
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Each color generates 11 OKLCH shades (50–950). The `neutral` palette is special — use a named preset (`stone` for warm) rather than a hex value. Three contexts are built-in: `light` (default), `medium`, `dark`. Context override keys match token names — `section:` not `bg:`, `primary:` not `btn-primary-bg:`.
|
|
431
|
+
|
|
432
|
+
### Foundation variables
|
|
433
|
+
|
|
434
|
+
Foundations declare customizable layout/spacing values in `foundation.js`:
|
|
435
|
+
|
|
436
|
+
```js
|
|
437
|
+
export default {
|
|
438
|
+
vars: {
|
|
439
|
+
'header-height': { default: '4rem' },
|
|
440
|
+
'sidebar-width': { default: '280px' },
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Sites override them in `theme.yml` under `vars:`. Components use them as `var(--header-height)`.
|
|
446
|
+
|
|
447
|
+
**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.
|
|
448
|
+
|
|
449
|
+
## Component Development
|
|
450
|
+
|
|
451
|
+
### Props Interface
|
|
452
|
+
|
|
453
|
+
```jsx
|
|
454
|
+
function MyComponent({ content, params, block }) {
|
|
455
|
+
const { title, paragraphs, links, items } = content // Guaranteed shape
|
|
456
|
+
const { columns, variant } = params // Defaults from meta.js
|
|
457
|
+
const { website } = useWebsite() // Or block.website
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Section Wrapper
|
|
462
|
+
|
|
463
|
+
The runtime wraps every section type in a `<section>` element with context class, background, and semantic tokens. Use static properties to customize this wrapper:
|
|
464
|
+
|
|
465
|
+
```jsx
|
|
466
|
+
function Hero({ content, params }) {
|
|
467
|
+
return (
|
|
468
|
+
<div className="max-w-7xl mx-auto px-6">
|
|
469
|
+
<h1 className="text-heading text-5xl font-bold">{content.title}</h1>
|
|
470
|
+
</div>
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
Hero.className = 'pt-32 md:pt-48' // Classes on the <section> wrapper
|
|
475
|
+
Hero.as = 'div' // Change wrapper element (default: 'section')
|
|
476
|
+
|
|
477
|
+
export default Hero
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
- `Component.className` — adds classes to the runtime's wrapper. Use for section-level padding, borders, overflow. The component's own JSX handles inner layout only (`max-w-7xl mx-auto px-6`).
|
|
481
|
+
- `Component.as` — changes the wrapper element. Use `'nav'` for headers, `'footer'` for footers, `'div'` when `<section>` isn't semantically appropriate.
|
|
482
|
+
|
|
483
|
+
### meta.js Structure
|
|
484
|
+
|
|
485
|
+
```javascript
|
|
486
|
+
export default {
|
|
487
|
+
title: 'Feature Grid',
|
|
488
|
+
description: 'Grid of feature cards with icons',
|
|
489
|
+
category: 'marketing',
|
|
490
|
+
// hidden: true, // Hide from content authors
|
|
491
|
+
// background: 'self', // Component renders its own background
|
|
492
|
+
// inset: true, // Available for @ComponentName references in markdown
|
|
493
|
+
// visuals: 1, // Expects 1 visual (image, video, or inset)
|
|
494
|
+
// children: true, // Accepts file-based child sections
|
|
495
|
+
|
|
496
|
+
content: {
|
|
497
|
+
title: 'Section heading',
|
|
498
|
+
paragraphs: 'Introduction [0-1]',
|
|
499
|
+
items: 'Feature cards with icon, title, description',
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
params: {
|
|
503
|
+
columns: { type: 'number', default: 3 },
|
|
504
|
+
variant: { type: 'select', options: ['default', 'centered', 'split'], default: 'default' },
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
presets: {
|
|
508
|
+
default: { label: 'Standard', params: { columns: 3 } },
|
|
509
|
+
compact: { label: 'Compact', params: { columns: 4 } },
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
// Static capabilities for cross-block coordination
|
|
513
|
+
context: {
|
|
514
|
+
allowTranslucentTop: true, // Header can overlay this section
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
All defaults belong in `meta.js`, not inline in component code.
|
|
520
|
+
|
|
521
|
+
### @uniweb/kit
|
|
522
|
+
|
|
523
|
+
**Primitives** (`@uniweb/kit`): `H1`–`H6`, `P`, `Span`, `Text`, `Link`, `Image`, `Icon`, `Media`, `Asset`, `SocialIcon`, `FileLogo`, `cn()`
|
|
524
|
+
|
|
525
|
+
**Styled** (`@uniweb/kit/styled`): `Section`, `Render`, `Visual`, `SidebarLayout`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
526
|
+
|
|
527
|
+
**Hooks:**
|
|
528
|
+
- `useScrolled(threshold)` → boolean for scroll-based header styling
|
|
529
|
+
- `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
|
|
530
|
+
- `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
|
|
531
|
+
- `useActiveRoute()` → `{ route, isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
532
|
+
- `useGridLayout(columns, { gap })` → responsive grid class string
|
|
533
|
+
- `useTheme(name)` → standardized theme classes
|
|
534
|
+
|
|
535
|
+
**Utilities:** `cn()` (Tailwind class merge), `filterSocialLinks(links)`, `getSocialPlatform(url)`
|
|
536
|
+
|
|
537
|
+
### Foundation Organization
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
foundation/src/
|
|
541
|
+
├── sections/ # Section types (auto-discovered via meta.js)
|
|
542
|
+
│ ├── Hero/
|
|
543
|
+
│ │ ├── Hero.jsx # Entry — or index.jsx, both work
|
|
544
|
+
│ │ └── meta.js
|
|
545
|
+
│ └── Features/
|
|
546
|
+
│ ├── Features.jsx
|
|
547
|
+
│ └── meta.js
|
|
548
|
+
├── components/ # Your React components (no meta.js, not selectable)
|
|
549
|
+
│ ├── ui/ # shadcn-compatible primitives
|
|
550
|
+
│ │ └── button.jsx
|
|
551
|
+
│ └── Card.jsx
|
|
552
|
+
└── styles.css
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Only folders with `meta.js` in `sections/` (or `components/` for older foundations) become section types. Everything else is ordinary React — organize however you like.
|
|
556
|
+
|
|
557
|
+
### Website and Page APIs
|
|
558
|
+
|
|
559
|
+
```jsx
|
|
560
|
+
const { website } = useWebsite()
|
|
561
|
+
|
|
562
|
+
// Navigation
|
|
563
|
+
const pages = website.getPageHierarchy({ for: 'header' }) // or 'footer'
|
|
564
|
+
// → [{ route, navigableRoute, label, hasContent, children }]
|
|
565
|
+
|
|
566
|
+
// Locale
|
|
567
|
+
website.hasMultipleLocales()
|
|
568
|
+
website.getLocales() // [{ code, label, isDefault }]
|
|
569
|
+
website.getActiveLocale() // 'en'
|
|
570
|
+
website.getLocaleUrl('es')
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Insets and the Visual Component
|
|
574
|
+
|
|
575
|
+
Components access inline `@` references via `block.insets` (separate from `block.childBlocks`):
|
|
576
|
+
|
|
577
|
+
```jsx
|
|
578
|
+
import { Visual } from '@uniweb/kit/styled'
|
|
579
|
+
|
|
580
|
+
// Visual renders the first visual: inset > video > image
|
|
581
|
+
function SplitContent({ content, block, params }) {
|
|
582
|
+
return (
|
|
583
|
+
<div className="flex gap-12">
|
|
584
|
+
<div className="flex-1">
|
|
585
|
+
<h2 className="text-heading">{content.title}</h2>
|
|
586
|
+
</div>
|
|
587
|
+
<Visual content={content} block={block} className="flex-1 rounded-lg" />
|
|
588
|
+
</div>
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
- `block.insets` — array of Block instances from `@` references
|
|
594
|
+
- `block.getInset(refId)` — lookup by refId (used by sequential renderers)
|
|
595
|
+
- `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
|
|
596
|
+
- `<Visual>` — renders first inset > video > image from content (from `@uniweb/kit/styled`)
|
|
597
|
+
|
|
598
|
+
Inset components declare `inset: true` in meta.js. Use `hidden: true` for inset-only components:
|
|
599
|
+
|
|
600
|
+
```js
|
|
601
|
+
// sections/insets/NetworkDiagram/meta.js
|
|
602
|
+
export default {
|
|
603
|
+
inset: true,
|
|
604
|
+
hidden: true,
|
|
605
|
+
params: { variant: { type: 'select', options: ['full', 'compact'], default: 'full' } },
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Dispatcher Pattern
|
|
610
|
+
|
|
611
|
+
One section type with a `variant` param replaces multiple near-duplicates. Instead of `HeroLeft`, `HeroCentered`, `HeroSplit` — one `Hero` with `variant: left | centered | split`:
|
|
612
|
+
|
|
613
|
+
```jsx
|
|
614
|
+
function SplitContent({ content, block, params }) {
|
|
615
|
+
const flipped = params.variant === 'flipped'
|
|
616
|
+
return (
|
|
617
|
+
<div className={`flex gap-16 items-center ${flipped ? 'flex-row-reverse' : ''}`}>
|
|
618
|
+
<div className="flex-1">
|
|
619
|
+
{content.pretitle && (
|
|
620
|
+
<p className="text-xs font-bold uppercase tracking-widest text-subtle mb-4">
|
|
621
|
+
{content.pretitle}
|
|
622
|
+
</p>
|
|
623
|
+
)}
|
|
624
|
+
<h2 className="text-heading text-3xl font-bold">{content.title}</h2>
|
|
625
|
+
<p className="text-body mt-4">{content.paragraphs[0]}</p>
|
|
626
|
+
</div>
|
|
627
|
+
<Visual content={content} block={block} className="flex-1 rounded-2xl" />
|
|
628
|
+
</div>
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
```js
|
|
634
|
+
// meta.js
|
|
635
|
+
export default {
|
|
636
|
+
title: 'Split Content',
|
|
637
|
+
content: { pretitle: 'Eyebrow label', title: 'Section heading', paragraphs: 'Description' },
|
|
638
|
+
params: {
|
|
639
|
+
variant: { type: 'select', options: ['default', 'flipped'], default: 'default' },
|
|
640
|
+
},
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
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.
|
|
645
|
+
|
|
646
|
+
### Cross-Block Communication
|
|
647
|
+
|
|
648
|
+
Components read neighboring blocks for adaptive behavior (e.g., translucent header over hero):
|
|
649
|
+
|
|
650
|
+
```jsx
|
|
651
|
+
const firstBody = block.page.getFirstBodyBlockInfo()
|
|
652
|
+
// → { type, theme, context: { allowTranslucentTop }, state }
|
|
653
|
+
|
|
654
|
+
// context = static (from meta.js), state = dynamic (from useBlockState)
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Custom Layouts
|
|
658
|
+
|
|
659
|
+
Layouts live in `foundation/src/layouts/` and are auto-discovered. Set the default in `foundation.js`:
|
|
660
|
+
|
|
661
|
+
```js
|
|
662
|
+
// foundation/src/foundation.js
|
|
663
|
+
export default {
|
|
664
|
+
name: 'My Template', // Display name (falls back to package.json name)
|
|
665
|
+
description: 'A brief description', // Falls back to package.json description
|
|
666
|
+
defaultLayout: 'DocsLayout',
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
```jsx
|
|
671
|
+
// foundation/src/layouts/DocsLayout/index.jsx
|
|
672
|
+
export default function DocsLayout({ header, body, footer, left, right, params }) {
|
|
673
|
+
return (
|
|
674
|
+
<div className="min-h-screen flex flex-col">
|
|
675
|
+
{header && <header>{header}</header>}
|
|
676
|
+
<div className="flex-1 flex">
|
|
677
|
+
{left && <aside className="w-64">{left}</aside>}
|
|
678
|
+
<main className="flex-1">{body}</main>
|
|
679
|
+
{right && <aside className="w-64">{right}</aside>}
|
|
680
|
+
</div>
|
|
681
|
+
{footer && <footer>{footer}</footer>}
|
|
682
|
+
</div>
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Layout receives pre-rendered areas as props plus `params`, `page`, and `website`. The `body` area is always implicit.
|
|
688
|
+
|
|
689
|
+
**Layout meta.js** declares which areas the layout renders:
|
|
690
|
+
|
|
691
|
+
```js
|
|
692
|
+
// foundation/src/layouts/DocsLayout/meta.js
|
|
693
|
+
export default {
|
|
694
|
+
areas: ['header', 'footer', 'left'],
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Area names are arbitrary strings — `header`, `footer`, `left`, `right` are conventional, but a dashboard layout could use `topbar`, `sidebar`, `statusbar`.
|
|
699
|
+
|
|
700
|
+
**Site-side layout content** — each layout can have its own section files:
|
|
701
|
+
|
|
702
|
+
```
|
|
703
|
+
site/layout/
|
|
704
|
+
├── header.md # Default layout sections
|
|
705
|
+
├── footer.md
|
|
706
|
+
├── left.md
|
|
707
|
+
└── marketing/ # Sections for the "marketing" layout
|
|
708
|
+
├── header.md # Different header for marketing pages
|
|
709
|
+
└── footer.md
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Named subdirectories are self-contained — no inheritance from the root. If `marketing/` has no `left.md`, marketing pages have no left panel.
|
|
713
|
+
|
|
714
|
+
**Layout cascade** (first match wins): `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
|
|
715
|
+
|
|
716
|
+
```yaml
|
|
717
|
+
# page.yml — select layout and hide areas
|
|
718
|
+
layout:
|
|
719
|
+
name: MarketingLayout
|
|
720
|
+
hide: [left, right]
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Migrating From Other Frameworks
|
|
724
|
+
|
|
725
|
+
Don't port line-by-line. Study the original implementation, then plan a new one in Uniweb from first principles. Other frameworks produce far more components than Uniweb needs — expect consolidation, not 1:1 correspondence.
|
|
726
|
+
|
|
727
|
+
### Why fewer components
|
|
728
|
+
|
|
729
|
+
Uniweb section types do more with less because the framework handles concerns that other frameworks push onto components:
|
|
730
|
+
|
|
731
|
+
- **Dispatcher pattern** — one section type with a `variant` param replaces multiple near-duplicate components (`HeroHomepage` + `HeroPricing` → `Hero` with `variant: homepage | pricing`)
|
|
732
|
+
- **Section nesting** — `@`-prefixed child files replace wrapper components that exist only to arrange children
|
|
733
|
+
- **Insets** — `` replaces prop-drilling of visual components into containers
|
|
734
|
+
- **Visual component** — `<Visual>` renders image/video/inset from content, replacing manual media handling
|
|
735
|
+
- **Semantic theming** — the runtime orchestrates context classes and token resolution, replacing per-component dark mode logic
|
|
736
|
+
- **Engine backgrounds** — the runtime renders section backgrounds from frontmatter, replacing background-handling code in every section
|
|
737
|
+
- **Rich params** — `meta.js` params with types, defaults, and presets replace config objects and conditional logic
|
|
738
|
+
|
|
739
|
+
### Migration approach
|
|
740
|
+
|
|
741
|
+
1. **Check if you're inside an existing Uniweb workspace** (look for `pnpm-workspace.yaml` and a `package.json` with `uniweb` as a dependency). If yes, use `pnpm uniweb add` to create projects inside it. If no, create a new workspace:
|
|
742
|
+
```bash
|
|
743
|
+
pnpm create uniweb my-project --template blank
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
3. **Use named layouts** for different page groups — a marketing layout for landing pages, a docs layout for `/docs/*`. One site, multiple layouts, each with its own header/footer/sidebar content.
|
|
747
|
+
|
|
748
|
+
4. **Dump legacy components under `src/components/`** — they're not section types. Import them from section types as needed during the transition.
|
|
749
|
+
|
|
750
|
+
5. **Create section types one at a time.** Each is independent — one can use hardcoded content while another reads from markdown. Staged migration levels:
|
|
751
|
+
- **Level 0**: Paste the whole original file as one section type. You get routing and dev tooling immediately.
|
|
752
|
+
- **Level 1**: Decompose into section types. Name by purpose (`Institutions` → `Testimonial`). Consolidate duplicates via dispatcher pattern.
|
|
753
|
+
- **Level 2**: Move content from JSX to markdown. Components read from `content` instead of hardcoded strings. Content authors can now edit without touching code.
|
|
754
|
+
- **Level 3**: Replace hardcoded Tailwind colors with semantic tokens. Components work in any context and any brand.
|
|
755
|
+
|
|
756
|
+
6. **Map source colors to `theme.yml`, not to foundation CSS.** The most common migration mistake is recreating the source site's color tokens as CSS custom properties in `styles.css` (e.g., `--ink`, `--paper`, `--accent`). This creates a parallel color system that bypasses CCA's semantic tokens, context classes, and site-level theming entirely. Instead: identify the source's primary color → set it as `colors.primary` in theme.yml. Identify the neutral tone → set it as `colors.neutral` (e.g., `stone` for warm). Identify context needs → use `theme:` frontmatter per section. Components use `text-heading`, `bg-section`, `bg-card` — never custom color variables.
|
|
757
|
+
|
|
758
|
+
7. **Name by purpose, not by content** — `TheModel` → `SplitContent`, `WorkModes` → `FeatureColumns`, `FinalCTA` → `CallToAction`. Components render a *kind* of content, not specific content.
|
|
759
|
+
|
|
760
|
+
8. **UI helpers → `components/`** — Buttons, badges, cards go in `src/components/` (no `meta.js` needed, not selectable by content authors).
|
|
761
|
+
|
|
762
|
+
## Tailwind CSS v4
|
|
763
|
+
|
|
764
|
+
Foundation styles in `foundation/src/styles.css`:
|
|
765
|
+
|
|
766
|
+
```css
|
|
767
|
+
@import "tailwindcss";
|
|
768
|
+
@import "@uniweb/kit/theme-tokens.css"; /* Semantic tokens from theme.yml */
|
|
769
|
+
@source "./sections/**/*.{js,jsx}";
|
|
770
|
+
@source "./components/**/*.{js,jsx}"; /* UI helpers (Button, Card, etc.) */
|
|
771
|
+
@source "../node_modules/@uniweb/kit/src/**/*.jsx";
|
|
772
|
+
|
|
773
|
+
@theme {
|
|
774
|
+
/* Additional custom values — NOT for colors already in theme.yml */
|
|
775
|
+
--breakpoint-xs: 30rem;
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
Semantic color tokens (`text-heading`, `bg-section`, `bg-primary`, etc.) come from `theme-tokens.css` — which the runtime populates from the site's `theme.yml`. Don't redefine colors here that belong in `theme.yml`. Use `@theme` only for values the token system doesn't cover (custom breakpoints, animations, shadows).
|
|
780
|
+
|
|
781
|
+
**Custom CSS is fine alongside Tailwind.** Animations, keyframes, gradients with masks, always-dark code blocks, and other effects that aren't expressible as utility classes can go directly in `styles.css`. Tailwind handles layout and spacing; custom CSS handles visual effects.
|
|
782
|
+
|
|
783
|
+
## Troubleshooting
|
|
784
|
+
|
|
785
|
+
**"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"` (or `"default": "file:../../foundations/default"` for multi-site).
|
|
786
|
+
|
|
787
|
+
**Component not appearing** — Verify `meta.js` exists and doesn't have `hidden: true`. Rebuild: `cd foundation && pnpm build`.
|
|
788
|
+
|
|
789
|
+
**Styles not applying** — Verify `@source` in `styles.css` includes your component paths. Check custom colors match `@theme` definitions.
|
|
790
|
+
|
|
791
|
+
## Further Documentation
|
|
792
|
+
|
|
793
|
+
Full Uniweb documentation is available at **https://github.com/uniweb/docs** — raw markdown files you can fetch directly.
|
|
794
|
+
|
|
795
|
+
| Section | Path | Topics |
|
|
796
|
+
|---------|------|--------|
|
|
797
|
+
| **Getting Started** | `getting-started/` | What is Uniweb, quickstart guide, templates overview |
|
|
798
|
+
| **Authoring** | `authoring/` | Writing content, site setup, collections, theming, linking, search, recipes, translations |
|
|
799
|
+
| **Development** | `development/` | Building foundations, component patterns, data fetching, custom layouts, i18n, converting existing designs |
|
|
800
|
+
| **Reference** | `reference/` | site.yml, page.yml, content structure, meta.js, kit hooks/components, theming tokens, CLI commands, deployment |
|
|
801
|
+
|
|
802
|
+
**Quick access pattern:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
|
|
803
|
+
|
|
804
|
+
Examples:
|
|
805
|
+
- Content structure details: `reference/content-structure.md`
|
|
806
|
+
- Component metadata (meta.js): `reference/component-metadata.md`
|
|
807
|
+
- Kit hooks and components: `reference/kit-reference.md`
|
|
808
|
+
- Theming tokens: `reference/site-theming.md`
|
|
809
|
+
- Data fetching patterns: `reference/data-fetching.md`
|