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.
@@ -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
+ ![Image](./image.jpg)
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: `![](lu-house)`. 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: `![Logo](./logo.svg){role=icon}`
131
+
132
+ ### Insets (Component References)
133
+
134
+ Place a foundation component inline within content using `@` syntax:
135
+
136
+ ```markdown
137
+ ![description](@ComponentName)
138
+ ![description](@ComponentName){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
+ ![Architecture diagram](@NetworkDiagram){variant=compact}
148
+ ![Cache metrics](@PerformanceChart){period=30d}
149
+ ![](@GradientBlob){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
+ ![alt](./img.jpg){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 + ![](@LiveDemo) 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`) | `![](@Component)` 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 `![](@CommandBlock)` 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** — `![](@ComponentName)` 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`