uniweb 0.8.6 → 0.8.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,9 +41,9 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/build": "0.8.5",
45
- "@uniweb/kit": "0.7.3",
46
- "@uniweb/core": "0.5.4",
47
- "@uniweb/runtime": "0.6.4"
44
+ "@uniweb/build": "0.8.7",
45
+ "@uniweb/kit": "0.7.5",
46
+ "@uniweb/core": "0.5.6",
47
+ "@uniweb/runtime": "0.6.6"
48
48
  }
49
49
  }
@@ -1,7 +1,30 @@
1
1
  # AGENTS.md
2
2
 
3
+ > A comprehensive guide to building with Uniweb — for developers and AI assistants alike.
4
+
3
5
  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
6
 
7
+ ## Documentation
8
+
9
+ This project was created with [Uniweb](https://github.com/uniweb/cli). Full documentation (markdown, fetchable): https://github.com/uniweb/docs
10
+
11
+ **To read a specific page:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
12
+
13
+ **By task:**
14
+
15
+ | Task | Doc page |
16
+ |------|----------|
17
+ | Writing page content | `authoring/writing-content.md` |
18
+ | Theming and styling | `authoring/theming.md` |
19
+ | Building components | `development/creating-components.md` |
20
+ | Kit API (hooks, components) | `reference/kit-reference.md` |
21
+ | Site configuration | `reference/site-configuration.md` |
22
+ | Content shape reference | `reference/content-structure.md` |
23
+ | Component metadata (meta.js) | `reference/component-metadata.md` |
24
+ | Migrating existing designs | `development/converting-existing.md` |
25
+
26
+ > **npm registry:** Use `https://registry.npmjs.org/uniweb` for package metadata — the npmjs.com website blocks automated requests.
27
+
5
28
  ## Project Structure
6
29
 
7
30
  ```
@@ -49,6 +72,15 @@ pnpm uniweb add site blog # Named → ./blog/
49
72
 
50
73
  The name is both the directory name and the package name. Use `--project <name>` to co-locate under a project directory (e.g., `--project docs` → `docs/foundation/`).
51
74
 
75
+ ### Adding section types
76
+
77
+ ```bash
78
+ pnpm uniweb add section Hero
79
+ pnpm uniweb add section Hero --foundation ui # When multiple foundations exist
80
+ ```
81
+
82
+ Creates `src/sections/Hero/index.jsx` and `meta.js` with a minimal CCA-proper starter. The dev server picks it up automatically — no build or install needed.
83
+
52
84
  ### What the CLI generates
53
85
 
54
86
  **Foundation** (`vite.config.js`, `package.json`, `src/foundation.js`, `src/styles.css`):
@@ -84,11 +116,11 @@ type: Hero
84
116
  theme: dark
85
117
  ---
86
118
 
87
- ### Eyebrow Text ← pretitle (heading before a more important one)
119
+ ### V1.0.0 IS OUT ← pretitle (small label above the title)
88
120
 
89
- # Main Headline ← title
121
+ # Build the system. ← title (the big headline)
90
122
 
91
- ## Subtitle ← subtitle
123
+ ## Not every page. ← subtitle
92
124
 
93
125
  Description paragraph.
94
126
 
@@ -97,15 +129,17 @@ Description paragraph.
97
129
  ![Image](./image.jpg)
98
130
  ```
99
131
 
132
+ Content authors don't need to understand *why* `###` means pretitle — just that putting a smaller heading before the main heading creates a small label above it. Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing.
133
+
100
134
  ### Content Shape
101
135
 
102
136
  The semantic parser extracts markdown into a flat, guaranteed structure. No null checks needed — empty strings/arrays if content is absent:
103
137
 
104
138
  ```js
105
139
  content = {
106
- title: '', // Main heading
140
+ title: '', // Main heading (string or string[] for multi-line)
107
141
  pretitle: '', // Heading before main title (auto-detected)
108
- subtitle: '', // Heading after title
142
+ subtitle: '', // Heading after title (string or string[] for multi-line)
109
143
  subtitle2: '', // Third-level heading
110
144
  paragraphs: [], // Text blocks
111
145
  links: [], // { href, label, role } — standalone links become buttons
@@ -140,6 +174,77 @@ Enterprise-grade. ← items[1].paragraphs[0]
140
174
 
141
175
  Each item has the same content shape as the top level — `title`, `paragraphs`, `icons`, `links`, `lists`, etc. are all available per item.
142
176
 
177
+ **Complete example — markdown and resulting content shape side by side:**
178
+
179
+ ```markdown
180
+ ### Eyebrow │ content.pretitle = "Eyebrow"
181
+ # Our Features │ content.title = "Our Features"
182
+ ## Build better products │ content.subtitle = "Build better products"
183
+
184
+ We help teams ship faster. │ content.paragraphs[0] = "We help teams..."
185
+
186
+ [Get Started](/start) │ content.links[0] = { href: "/start", label: "Get Started" }
187
+
188
+ ### Fast │ content.items[0].title = "Fast"
189
+ ![](lu-zap) │ content.items[0].icons[0] = { library: "lu", name: "zap" }
190
+ Lightning quick. │ content.items[0].paragraphs[0] = "Lightning quick."
191
+
192
+ ### Secure │ content.items[1].title = "Secure"
193
+ ![](lu-shield) │ content.items[1].icons[0] = { library: "lu", name: "shield" }
194
+ Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise-grade..."
195
+ ```
196
+
197
+ 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
+
199
+ ### Multi-Line Headings
200
+
201
+ Consecutive headings at the same level merge into a title array — a single heading split across visual lines:
202
+
203
+ ```markdown
204
+ # Build the future │ content.title = ["Build the future", "with confidence"]
205
+ # with confidence │
206
+ ```
207
+
208
+ Kit's `<H1>`, `<H2>`, etc. render arrays as a single tag with line breaks. This is how you create dramatic multi-line hero headlines.
209
+
210
+ **Works with accent styling:**
211
+
212
+ ```markdown
213
+ # Build the future │ content.title = [
214
+ # [with confidence]{accent} │ "Build the future",
215
+ │ "<span accent=\"true\">with confidence</span>"
216
+ │ ]
217
+ ```
218
+
219
+ **Works at any heading slot** — title, subtitle, items:
220
+
221
+ ```markdown
222
+ ### Our Mission │ content.pretitle = "Our Mission"
223
+ # Build the future │ content.title = ["Build the future",
224
+ # with confidence │ "with confidence"]
225
+ ## The platform for │ content.subtitle = ["The platform for",
226
+ ## modern teams │ "modern teams"]
227
+ ```
228
+
229
+ **Rule:** Same-level continuation only applies before going deeper. Once a subtitle level is reached, same-level headings start new items instead of merging:
230
+
231
+ ```markdown
232
+ # Features │ title = "Features"
233
+
234
+ We built this for you. │ paragraph
235
+
236
+ ### Fast │ items[0].title = "Fast"
237
+ ### Secure │ items[1].title = "Secure" ← new item, not merged
238
+ ```
239
+
240
+ Use `---` to force separate items when same-level headings would otherwise merge:
241
+
242
+ ```markdown
243
+ # Line one │ title = "Line one"
244
+ --- │ ← divider forces split
245
+ # Line two │ items[0].title = "Line two"
246
+ ```
247
+
143
248
  **Lists** contain bullet or ordered list items. Each list item is an object with the same content shape — not a plain string:
144
249
 
145
250
  ```markdown
@@ -210,6 +315,65 @@ Inset components must declare `inset: true` in their `meta.js`. They render at t
210
315
 
211
316
  Standalone links (alone on a line) become buttons. Inline links stay as text links.
212
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:
319
+
320
+ ```markdown
321
+ [Primary](/start) ← standalone → content.links[0]
322
+
323
+ [Secondary](/learn) ← standalone → content.links[1]
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
+ ```
333
+
334
+ ### Inline Text Styling
335
+
336
+ Style specific words or phrases using bracketed spans with boolean attributes:
337
+
338
+ ```markdown
339
+ # Build [faster]{accent} with structure
340
+
341
+ This is [less important]{muted} context.
342
+ ```
343
+
344
+ The framework provides two defaults: `accent` (colored + bold) and `muted` (subtle). These adapt to context automatically — in dark sections, `accent` resolves to a lighter shade.
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
+ ```
365
+
366
+ **Common pattern — accented multi-line hero heading:**
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`.
376
+
213
377
  ### Structured Data
214
378
 
215
379
  Tagged code blocks pass structured data via `content.data`:
@@ -239,6 +403,46 @@ const data = useData()
239
403
 
240
404
  Access: `content.data?.before`, `content.data?.after` → raw code strings.
241
405
 
406
+ ### Lists as Navigation Menus
407
+
408
+ Markdown lists are ideal for navigation, menus, and grouped link structures. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
409
+
410
+ **Header nav — flat list with icons and links:**
411
+
412
+ ```markdown
413
+ - ![](lu-home) [Home](/)
414
+ - ![](lu-book) [Docs](/docs)
415
+ - ![](lu-mail) [Contact](/contact)
416
+ ```
417
+
418
+ Access: `content.lists[0]` — each item has `item.links[0]` (href + label) and `item.icons[0]` (icon).
419
+
420
+ **Footer — nested list for grouped links:**
421
+
422
+ ```markdown
423
+ - Product
424
+ - [Features](/features)
425
+ - [Pricing](/pricing)
426
+ - Company
427
+ - [About](/about)
428
+ - [Careers](/careers)
429
+ ```
430
+
431
+ Access: `content.lists[0]` — each top-level item has `item.paragraphs[0]` (group label) and `item.lists[0]` (array of sub-items, each with `subItem.links[0]`).
432
+
433
+ ```jsx
434
+ content.lists[0]?.map((group, i) => (
435
+ <div key={i}>
436
+ <Span text={group.paragraphs[0]} className="font-semibold text-heading" />
437
+ <ul>
438
+ {group.lists[0]?.map((subItem, j) => (
439
+ <li key={j}><Link to={subItem.links[0]?.href}>{subItem.links[0]?.label}</Link></li>
440
+ ))}
441
+ </ul>
442
+ </div>
443
+ ))
444
+ ```
445
+
242
446
  ### Section Backgrounds
243
447
 
244
448
  Set `background` in frontmatter — the runtime renders it automatically. The string form auto-detects the type:
@@ -411,7 +615,12 @@ CCA separates theme from code. Components use **semantic CSS tokens** instead of
411
615
  | `text-link` | Link color |
412
616
  | `bg-primary` | Primary action background |
413
617
  | `text-primary-foreground` | Text on primary background |
618
+ | `hover:bg-primary-hover` | Primary hover state |
619
+ | `border-primary-border` | Primary border (transparent by default) |
414
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 |
415
624
  | `text-success` / `bg-success-subtle` | Status: success |
416
625
  | `text-error` / `bg-error-subtle` | Status: error |
417
626
  | `text-warning` / `bg-warning-subtle` | Status: warning |
@@ -477,8 +686,8 @@ theme: dark
477
686
  ```yaml
478
687
  theme:
479
688
  mode: light
480
- primary: var(--neutral-900) # Dark buttons in a light section
481
- primary-hover: var(--neutral-800)
689
+ primary: neutral-900 # Dark buttons in a light section
690
+ primary-hover: neutral-800
482
691
  ```
483
692
 
484
693
  Any semantic token (`section`, `heading`, `body`, `primary`, `link`, etc.) can be overridden this way. The overrides are applied as inline CSS custom properties on the section wrapper — components don't need to know about them.
@@ -506,7 +715,7 @@ fonts:
506
715
  body: "'Inter', system-ui, sans-serif"
507
716
 
508
717
  inline:
509
- emphasis: # For [text]{emphasis} in markdown
718
+ accent: # For [text]{accent} in markdown
510
719
  color: var(--link)
511
720
  font-weight: '600'
512
721
 
@@ -586,6 +795,14 @@ These compose with semantic tokens — they adapt per context because they refer
586
795
 
587
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.
588
797
 
798
+ **Text tones beyond the 3-token set.** Source designs often have 4+ text tones (primary, secondary, tertiary, disabled). Uniweb provides 3 (`text-heading`, `text-body`, `text-subtle`). Don't collapse the extras — create them with `color-mix()` so they still adapt per context:
799
+
800
+ ```css
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
+
589
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.
590
807
 
591
808
  ## Component Development
@@ -622,6 +839,88 @@ export default Hero
622
839
  - `Component.className` — adds classes to the runtime's wrapper. Use for section-level spacing, borders, overflow. Set `py-[var(--section-padding-y)]` for consistent spacing from the theme variable, or override for specific sections (e.g., hero needs extra top padding). The component's own JSX handles inner layout only (`max-w-7xl mx-auto px-6`).
623
840
  - `Component.as` — changes the wrapper element. Use `'nav'` for headers, `'footer'` for footers, `'div'` when `<section>` isn't semantically appropriate.
624
841
 
842
+ **Layout components** (Header, Footer) typically need `Component.className = 'p-0'` to suppress the runtime's default section padding, since they control their own padding. Also set `Component.as = 'header'` or `'footer'` for semantic HTML:
843
+
844
+ ```jsx
845
+ function Header({ content, block }) { /* ... */ }
846
+ Header.className = 'p-0'
847
+ Header.as = 'header'
848
+ export default Header
849
+ ```
850
+
851
+ ### Content Patterns for Header and Footer
852
+
853
+ Header and Footer are the hardest components to content-model because they combine several content categories. Use different parts of the content shape for each role:
854
+
855
+ **Header** — title for logo, list for nav links, standalone link for CTA, tagged YAML for metadata:
856
+
857
+ ````markdown
858
+ ---
859
+ type: Header
860
+ ---
861
+
862
+ # Acme Inc
863
+
864
+ - ![](lu-search) [How It Works](/how-it-works)
865
+ - ![](lu-users) [For Teams](/for-teams)
866
+ - ![](lu-book) [Docs](/docs)
867
+
868
+ [Get Started](/docs/quickstart)
869
+
870
+ ```yaml:config
871
+ github: https://github.com/acme
872
+ version: v2.1.0
873
+ ```
874
+ ````
875
+
876
+ ```jsx
877
+ function Header({ content, block }) {
878
+ const logo = content.title // "Acme Inc"
879
+ const navItems = content.lists[0] || [] // [{icons, links}, ...]
880
+ const cta = content.links[0] // {href, label}
881
+ const config = content.data?.config // {github, version}
882
+ // ...
883
+ }
884
+ ```
885
+
886
+ **Footer** — paragraph for tagline, nested list for grouped columns, tagged YAML for legal:
887
+
888
+ ````markdown
889
+ ---
890
+ type: Footer
891
+ ---
892
+
893
+ Build something great.
894
+
895
+ - Product
896
+ - [Features](/features)
897
+ - [Pricing](/pricing)
898
+ - Developers
899
+ - [Docs](/docs)
900
+ - [GitHub](https://github.com/acme){target=_blank}
901
+ - Community
902
+ - [Discord](#)
903
+ - [Blog](/blog)
904
+
905
+ ```yaml:legal
906
+ copyright: © 2025 Acme Inc
907
+ ```
908
+ ````
909
+
910
+ ```jsx
911
+ function Footer({ content, block }) {
912
+ const tagline = content.paragraphs[0] // "Build something great."
913
+ const columns = content.lists[0] || [] // [{paragraphs, lists}, ...]
914
+ const legal = content.data?.legal // {copyright}
915
+
916
+ // Each column: group.paragraphs[0] = label, group.lists[0] = links
917
+ columns.map(group => ({
918
+ label: group.paragraphs[0],
919
+ links: group.lists[0]?.map(item => item.links[0])
920
+ }))
921
+ }
922
+ ```
923
+
625
924
  ### meta.js Structure
626
925
 
627
926
  ```javascript
@@ -629,7 +928,7 @@ export default {
629
928
  title: 'Feature Grid',
630
929
  description: 'Grid of feature cards with icons',
631
930
  category: 'marketing',
632
- // hidden: true, // Hide from content authors
931
+ // hidden: true, // Exclude from export (internal/helper component)
633
932
  // background: 'self', // Component renders its own background
634
933
  // inset: true, // Available for @ComponentName references in markdown
635
934
  // visuals: 1, // Expects 1 visual (image, video, or inset)
@@ -667,11 +966,13 @@ Content fields (`title`, `pretitle`, `paragraphs[]`, list item text) are **HTML
667
966
  **Rendering text** (`@uniweb/kit`):
668
967
 
669
968
  ```jsx
670
- import { H2, P, Span } from '@uniweb/kit'
969
+ import { H1, H2, P, Span } from '@uniweb/kit'
671
970
 
672
- <H2 text={content.title} className="text-heading text-3xl font-bold" />
673
- <P text={content.paragraphs[0]} className="text-body" />
674
- <P text={content.paragraphs} /> // array each string becomes its own <p>
971
+ <H1 text={content.title} className="text-heading text-5xl font-bold" />
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>
675
976
  <Span text={listItem.paragraphs[0]} className="text-subtle" />
676
977
  ```
677
978
 
@@ -702,15 +1003,29 @@ import { Section, Render } from '@uniweb/kit'
702
1003
 
703
1004
  **Other primitives** (`@uniweb/kit`): `Link`, `Image`, `Icon`, `Media`, `Asset`, `SafeHtml`, `SocialIcon`, `FileLogo`, `cn()`
704
1005
 
1006
+ `Link` props: `to` (or `href`), `target`, `reload`, `download`, `className`, `children`:
1007
+
1008
+ ```jsx
1009
+ <Link to="/about">About</Link> // SPA navigation via React Router
1010
+ <Link to="page:about">About</Link> // Resolves page ID to route
1011
+ <Link reload href={localeUrl}>ES</Link> // Full page reload, prepends basePath
1012
+ // External URLs auto-get target="_blank" and rel="noopener noreferrer"
1013
+ ```
1014
+
705
1015
  **Other styled** (`@uniweb/kit`): `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
706
1016
 
707
1017
  **Hooks:**
708
1018
  - `useScrolled(threshold)` → boolean for scroll-based header styling
709
1019
  - `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
710
1020
  - `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
711
- - `useActiveRoute()` → `{ route, isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
1021
+ - `useActiveRoute()` → `{ route, rootSegment, isActive(page), isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
712
1022
  - `useGridLayout(columns, { gap })` → responsive grid class string
713
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
714
1029
 
715
1030
  **Utilities:** `cn()` (Tailwind class merge), `filterSocialLinks(links)`, `getSocialPlatform(url)`
716
1031
 
@@ -737,7 +1052,8 @@ Only folders with `meta.js` in `sections/` (or `components/` for older foundatio
737
1052
  ### Website and Page APIs
738
1053
 
739
1054
  ```jsx
740
- const { website } = useWebsite()
1055
+ const { website } = useWebsite() // or block.website
1056
+ const page = website.activePage // or block.page
741
1057
 
742
1058
  // Navigation
743
1059
  const pages = website.getPageHierarchy({ for: 'header' }) // or 'footer'
@@ -748,6 +1064,28 @@ website.hasMultipleLocales()
748
1064
  website.getLocales() // [{ code, label, isDefault }]
749
1065
  website.getActiveLocale() // 'en'
750
1066
  website.getLocaleUrl('es')
1067
+
1068
+ // Core properties
1069
+ website.name // Site name from site.yml
1070
+ website.basePath // Deployment base path (e.g., '/docs/')
1071
+
1072
+ // Route detection
1073
+ const { isActive, isActiveOrAncestor } = useActiveRoute()
1074
+ isActive(page) // Exact match
1075
+ isActiveOrAncestor(page) // Ancestor match (for parent highlighting in nav)
1076
+
1077
+ // Appearance (light/dark mode)
1078
+ const { scheme, toggle, canToggle } = useAppearance()
1079
+
1080
+ // Page properties
1081
+ page.title // Page title
1082
+ page.label // Short label for nav (falls back to title)
1083
+ page.route // Route path
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
751
1089
  ```
752
1090
 
753
1091
  ### Insets and the Visual Component
@@ -776,17 +1114,20 @@ function SplitContent({ content, block }) {
776
1114
  - `block.getInset(refId)` — lookup by refId (used by sequential renderers)
777
1115
  - `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
778
1116
 
779
- Inset components declare `inset: true` in meta.js. Use `hidden: true` for inset-only components:
1117
+ **SSG and hooks:** Inset components that use React hooks (useState, useEffect) will trigger prerender warnings during `pnpm build`. This is expected — the SSG pipeline cannot render hooks due to dual React instances in the build. The warnings are informational; the page renders correctly client-side. If you see `"Skipped SSG for /..."` or `"Invalid hook call"`, this is the cause.
1118
+
1119
+ Inset components declare `inset: true` in meta.js:
780
1120
 
781
1121
  ```js
782
1122
  // sections/insets/NetworkDiagram/meta.js
783
1123
  export default {
784
1124
  inset: true,
785
- hidden: true,
786
1125
  params: { variant: { type: 'select', options: ['full', 'compact'], default: 'full' } },
787
1126
  }
788
1127
  ```
789
1128
 
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
+
790
1131
  ### Dispatcher Pattern
791
1132
 
792
1133
  One section type with a `variant` param replaces multiple near-duplicates. Instead of `HeroLeft`, `HeroCentered`, `HeroSplit` — one `Hero` with `variant: left | centered | split`:
@@ -965,11 +1306,19 @@ Semantic color tokens (`text-heading`, `bg-section`, `bg-primary`, etc.) come fr
965
1306
 
966
1307
  **"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"` (or `"default": "file:../../foundations/default"` for multi-site).
967
1308
 
968
- **Component not appearing** — Verify `meta.js` exists and doesn't have `hidden: true`. Rebuild: `cd foundation && pnpm build`.
1309
+ **Component not appearing** — Verify `meta.js` exists. Check for `hidden: true` (means component is excluded from export — only use for internal helpers). Rebuild: `cd foundation && pnpm build`.
969
1310
 
970
1311
  **Styles not applying** — Verify `@source` in `styles.css` includes your component paths. Check custom colors match `@theme` definitions.
971
1312
 
972
- **Prerender warnings about hooks/useState** — During `pnpm build`, you may see `Warning: Failed to render /: Cannot read properties of null (reading 'useState')` for pages. This is a known limitation of the SSG pipeline (dual React instances in development). The site works correctly client-side — these warnings only affect the static HTML preview, not functionality.
1313
+ **Prerender warnings about hooks/useState** — Components with React hooks (useState/useEffect) especially insets will show SSG warnings during `pnpm build`. This is expected and harmless; see the note in the Insets section above.
1314
+
1315
+ **Content not appearing as expected?** In dev mode, open the browser console and inspect the parsed content shape your component receives:
1316
+
1317
+ ```js
1318
+ globalThis.uniweb.activeWebsite.activePage.bodyBlocks[0].parsedContent
1319
+ ```
1320
+
1321
+ Compare with the Content Shape table above to identify mapping issues (e.g., headings becoming items instead of title, links inline in paragraphs instead of in `links[]`).
973
1322
 
974
1323
  ## Further Documentation
975
1324
 
@@ -1,8 +1,10 @@
1
- ## AI Assistance
1
+ ## Developer Guide
2
2
 
3
- This project includes an [AGENTS.md](./AGENTS.md) file with detailed instructions for AI coding assistants (Claude Code, Cursor, Copilot, etc.).
3
+ [AGENTS.md](./AGENTS.md) is a comprehensive guide to building with Uniweb content authoring, component development, theming, configuration, and the kit API. Despite the name, it's written for developers and AI assistants alike. Read it to get productive fast.
4
4
 
5
- ### Example Prompts
5
+ ### AI Prompts
6
+
7
+ AI coding assistants (Claude Code, Cursor, Copilot, etc.) read AGENTS.md automatically. Some prompts to try:
6
8
 
7
9
  **Converting an existing design:**
8
10
  ```
@@ -1,5 +1,6 @@
1
1
  ## Learn More
2
2
 
3
- - [Uniweb Documentation](https://github.com/uniweb/cli)
4
- - [@uniweb/kit Components](https://www.npmjs.com/package/@uniweb/kit)
3
+ - [AGENTS.md](./AGENTS.md) — comprehensive guide to building with Uniweb
4
+ - [Uniweb Documentation](https://github.com/uniweb/docs) — full reference docs
5
+ - [Uniweb CLI](https://github.com/uniweb/cli) — project scaffolding and build tools
5
6
  - [Tailwind CSS v4](https://tailwindcss.com)
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Add Command
3
3
  *
4
- * Adds foundations, sites, extensions, or co-located projects to an existing workspace.
4
+ * Adds foundations, sites, extensions, section types, or co-located projects to an existing workspace.
5
5
  *
6
6
  * Usage:
7
7
  * uniweb add project [name] [--from <template>]
8
8
  * uniweb add foundation [name] [--from <template>] [--path <dir>] [--project <name>]
9
9
  * uniweb add site [name] [--from <template>] [--foundation <name>] [--path <dir>] [--project <name>]
10
10
  * uniweb add extension [name] [--from <template>] [--site <name>] [--path <dir>]
11
+ * uniweb add section <name> [--foundation <name>]
11
12
  */
12
13
 
13
14
  import { existsSync } from 'node:fs'
14
- import { readFile, writeFile } from 'node:fs/promises'
15
+ import { readFile, writeFile, mkdir } from 'node:fs/promises'
15
16
  import { join, relative } from 'node:path'
16
17
  import prompts from 'prompts'
17
18
  import yaml from 'js-yaml'
@@ -123,9 +124,10 @@ export async function add(rawArgs) {
123
124
  { label: 'foundation', description: 'Component library' },
124
125
  { label: 'site', description: 'Content site' },
125
126
  { label: 'extension', description: 'Additional component package' },
127
+ { label: 'section', description: 'Section type in a foundation' },
126
128
  ]))
127
129
  log('')
128
- log(`Usage: ${prefix} add <project|foundation|site|extension> [name]`)
130
+ log(`Usage: ${prefix} add <project|foundation|site|extension|section> [name]`)
129
131
  process.exit(1)
130
132
  }
131
133
 
@@ -138,6 +140,7 @@ export async function add(rawArgs) {
138
140
  { title: 'Foundation', value: 'foundation', description: 'Component library' },
139
141
  { title: 'Site', value: 'site', description: 'Content site' },
140
142
  { title: 'Extension', value: 'extension', description: 'Additional component package' },
143
+ { title: 'Section', value: 'section', description: 'Section type in a foundation' },
141
144
  ],
142
145
  }, {
143
146
  onCancel: () => {
@@ -169,9 +172,12 @@ export async function add(rawArgs) {
169
172
  case 'extension':
170
173
  await addExtension(rootDir, projectName, parsed, pm)
171
174
  break
175
+ case 'section':
176
+ await addSection(rootDir, parsed)
177
+ break
172
178
  default:
173
179
  error(`Unknown subcommand: ${parsed.subcommand}`)
174
- log(`Valid subcommands: project, foundation, site, extension`)
180
+ log(`Valid subcommands: project, foundation, site, extension, section`)
175
181
  process.exit(1)
176
182
  }
177
183
  }
@@ -875,6 +881,138 @@ async function wireExtensionToSite(rootDir, siteName, extensionName, extensionPa
875
881
  }
876
882
  }
877
883
 
884
+ /**
885
+ * Add a section type to a foundation
886
+ */
887
+ async function addSection(rootDir, opts) {
888
+ let name = opts.name
889
+
890
+ // Interactive name prompt when not provided
891
+ if (!name) {
892
+ if (isNonInteractive(process.argv)) {
893
+ error(`Missing section name.\n`)
894
+ log(`Usage: ${getCliPrefix()} add section <Name>`)
895
+ log(`\nSection names use PascalCase: Hero, FeatureGrid, CallToAction`)
896
+ process.exit(1)
897
+ }
898
+
899
+ const response = await prompts({
900
+ type: 'text',
901
+ name: 'name',
902
+ message: 'Section name (PascalCase):',
903
+ validate: (value) => /^[A-Z][a-zA-Z0-9]*$/.test(value) || 'Use PascalCase: Hero, FeatureGrid, CallToAction',
904
+ }, {
905
+ onCancel: () => {
906
+ log('\nCancelled.')
907
+ process.exit(0)
908
+ },
909
+ })
910
+ name = response.name
911
+ }
912
+
913
+ // Validate PascalCase
914
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
915
+ error(`Section name must be PascalCase (e.g., Hero, FeatureGrid, CallToAction).`)
916
+ process.exit(1)
917
+ }
918
+
919
+ // Find the foundation
920
+ const foundations = await discoverFoundations(rootDir)
921
+ let foundation
922
+
923
+ if (foundations.length === 0) {
924
+ error('No foundation found in this workspace.')
925
+ log(`Create one first: ${getCliPrefix()} add foundation`)
926
+ process.exit(1)
927
+ } else if (foundations.length === 1) {
928
+ foundation = foundations[0]
929
+ } else if (opts.foundation) {
930
+ foundation = foundations.find(f => f.name === opts.foundation)
931
+ if (!foundation) {
932
+ error(`Foundation '${opts.foundation}' not found.`)
933
+ log(`Available: ${foundations.map(f => f.name).join(', ')}`)
934
+ process.exit(1)
935
+ }
936
+ } else if (isNonInteractive(process.argv)) {
937
+ error(`Multiple foundations found. Specify which to use:\n`)
938
+ log(formatOptions(foundations.map(f => ({ label: f.name, description: f.path }))))
939
+ log('')
940
+ log(`Usage: ${getCliPrefix()} add section ${name} --foundation <name>`)
941
+ process.exit(1)
942
+ } else {
943
+ const response = await prompts({
944
+ type: 'select',
945
+ name: 'foundation',
946
+ message: 'Which foundation?',
947
+ choices: foundations.map(f => ({ title: f.name, description: f.path, value: f })),
948
+ }, {
949
+ onCancel: () => {
950
+ log('\nCancelled.')
951
+ process.exit(0)
952
+ },
953
+ })
954
+ foundation = response.foundation
955
+ }
956
+
957
+ // Resolve sections directory
958
+ const sectionsDir = join(rootDir, foundation.path, 'src', 'sections')
959
+ const sectionDir = join(sectionsDir, name)
960
+
961
+ if (existsSync(sectionDir)) {
962
+ error(`Section '${name}' already exists at ${foundation.path}/src/sections/${name}/`)
963
+ process.exit(1)
964
+ }
965
+
966
+ // Create section directory and files
967
+ await mkdir(sectionDir, { recursive: true })
968
+
969
+ const componentContent = `import { H2, P, Link, cn } from '@uniweb/kit'
970
+
971
+ export default function ${name}({ content, params }) {
972
+ const { title, paragraphs = [], links = [] } = content || {}
973
+
974
+ return (
975
+ <div className="max-w-4xl mx-auto px-6">
976
+ {title && <H2 text={title} className="text-heading text-3xl font-bold" />}
977
+ <P text={paragraphs} className="text-body mt-4" />
978
+ {links.length > 0 && (
979
+ <div className="mt-6 flex gap-3 flex-wrap">
980
+ {links.map((link, i) => (
981
+ <Link key={i} to={link.href} className={cn(
982
+ 'px-5 py-2.5 rounded-lg font-medium transition-colors',
983
+ i === 0
984
+ ? 'bg-primary text-primary-foreground hover:bg-primary-hover'
985
+ : 'bg-secondary text-secondary-foreground hover:bg-secondary-hover'
986
+ )}>
987
+ {link.label}
988
+ </Link>
989
+ ))}
990
+ </div>
991
+ )}
992
+ </div>
993
+ )
994
+ }
995
+ `
996
+
997
+ const metaContent = `export default {
998
+ title: '${name}',
999
+ description: '',
1000
+ params: {},
1001
+ }
1002
+ `
1003
+
1004
+ await writeFile(join(sectionDir, 'index.jsx'), componentContent)
1005
+ await writeFile(join(sectionDir, 'meta.js'), metaContent)
1006
+
1007
+ success(`Created section '${name}' at ${foundation.path}/src/sections/${name}/`)
1008
+ log(` ${colors.dim}index.jsx${colors.reset} — component (customize the JSX)`)
1009
+ log(` ${colors.dim}meta.js${colors.reset} — metadata (add content expectations, params, presets)`)
1010
+ if (foundations.length === 1) {
1011
+ log('')
1012
+ log(`${colors.dim}The dev server will pick it up automatically.${colors.reset}`)
1013
+ }
1014
+ }
1015
+
878
1016
  /**
879
1017
  * Show help for the add command
880
1018
  */
@@ -882,13 +1020,14 @@ function showAddHelp() {
882
1020
  log(`
883
1021
  ${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
884
1022
 
885
- Add projects, foundations, sites, or extensions to your workspace.
1023
+ Add projects, foundations, sites, extensions, or section types to your workspace.
886
1024
 
887
1025
  ${colors.bright}Usage:${colors.reset}
888
1026
  uniweb add project [name] [options]
889
1027
  uniweb add foundation [name] [options]
890
1028
  uniweb add site [name] [options]
891
1029
  uniweb add extension <name> [options]
1030
+ uniweb add section <name> [options]
892
1031
 
893
1032
  ${colors.bright}Common Options:${colors.reset}
894
1033
  --from <template> Apply content from a template after scaffolding
@@ -904,6 +1043,9 @@ ${colors.bright}Site Options:${colors.reset}
904
1043
  ${colors.bright}Extension Options:${colors.reset}
905
1044
  --site <name> Site to wire extension URL into
906
1045
 
1046
+ ${colors.bright}Section Options:${colors.reset}
1047
+ --foundation <n> Foundation to add section to (prompted if multiple exist)
1048
+
907
1049
  ${colors.bright}Examples:${colors.reset}
908
1050
  uniweb add project docs # Create docs/foundation/ + docs/site/
909
1051
  uniweb add project docs --from academic # Co-located pair + academic content
@@ -912,6 +1054,8 @@ ${colors.bright}Examples:${colors.reset}
912
1054
  uniweb add site # Create ./site/ at root
913
1055
  uniweb add site blog --foundation marketing # Create ./blog/ wired to marketing
914
1056
  uniweb add extension effects --site site # Create ./extensions/effects/
1057
+ uniweb add section Hero # Create Hero section type
1058
+ uniweb add section Hero --foundation ui # Target specific foundation
915
1059
  uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
916
1060
  uniweb add site --project docs # Create ./docs/site/ (co-located)
917
1061
  `)
package/src/index.js CHANGED
@@ -583,6 +583,7 @@ ${colors.bright}Add Subcommands:${colors.reset}
583
583
  add foundation [name] Add a foundation (--from, --path, --project)
584
584
  add site [name] Add a site (--from, --foundation, --path, --project)
585
585
  add extension <name> Add an extension (--from, --site, --path)
586
+ add section <name> Add a section type to a foundation (--foundation)
586
587
 
587
588
  ${colors.bright}Global Options:${colors.reset}
588
589
  --non-interactive Fail with usage info instead of prompting
@@ -1,37 +1,25 @@
1
- import React from 'react'
2
1
  import { H1, H2, P, Link, cn } from '@uniweb/kit'
3
2
 
4
3
  /**
5
4
  * Section Component
6
5
  *
7
6
  * A versatile content section that handles headings, text, and links.
8
- * This is the default component for rendering markdown content.
7
+ * Uses semantic tokens so it adapts to any theme context automatically.
9
8
  */
10
- function Section({ content, params }) {
11
- // Content fields: title, pretitle, subtitle, paragraphs, links, imgs, items
9
+ export default function Section({ content, params }) {
12
10
  const { title, pretitle, subtitle, paragraphs = [], links = [], imgs = [] } = content || {}
13
11
 
14
12
  const {
15
- theme = 'light',
16
13
  align = 'center',
17
14
  width = 'default',
18
15
  } = params || {}
19
16
 
20
- // Theme styles
21
- const themes = {
22
- light: 'bg-white text-gray-900',
23
- dark: 'bg-gray-900 text-white',
24
- primary: 'bg-primary text-white',
25
- }
26
-
27
- // Alignment styles
28
17
  const alignments = {
29
18
  left: 'text-left',
30
19
  center: 'text-center',
31
20
  right: 'text-right',
32
21
  }
33
22
 
34
- // Width styles
35
23
  const widths = {
36
24
  narrow: 'max-w-2xl',
37
25
  default: 'max-w-4xl',
@@ -40,58 +28,47 @@ function Section({ content, params }) {
40
28
  }
41
29
 
42
30
  return (
43
- <section className={cn('py-16 px-6', themes[theme])}>
44
- <div className={cn('mx-auto', widths[width], alignments[align])}>
45
- {/* Pretitle / Eyebrow */}
31
+ <div className={cn('py-16 px-6', alignments[align])}>
32
+ <div className={cn('mx-auto', widths[width])}>
46
33
  {pretitle && (
47
- <p className="text-sm font-medium text-primary mb-4 uppercase tracking-wide">
34
+ <p className="text-sm font-medium text-link mb-4 uppercase tracking-wide">
48
35
  {pretitle}
49
36
  </p>
50
37
  )}
51
38
 
52
- {/* Title */}
53
39
  {title && (
54
40
  <H1
55
41
  text={title}
56
- className="text-3xl sm:text-4xl font-bold mb-4"
42
+ className="text-heading text-3xl sm:text-4xl font-bold mb-4"
57
43
  />
58
44
  )}
59
45
 
60
- {/* Subtitle */}
61
46
  {subtitle && (
62
47
  <H2
63
48
  text={subtitle}
64
- className={cn(
65
- 'text-xl mb-6',
66
- theme === 'light' ? 'text-gray-600' : 'text-gray-300'
67
- )}
49
+ className="text-body text-xl mb-6"
68
50
  />
69
51
  )}
70
52
 
71
- {/* Paragraphs */}
72
53
  {paragraphs.map((para, index) => (
73
54
  <P
74
55
  key={index}
75
56
  text={para}
76
- className={cn(
77
- 'text-lg mb-4 leading-relaxed',
78
- theme === 'light' ? 'text-gray-700' : 'text-gray-300'
79
- )}
57
+ className="text-body text-lg mb-4 leading-relaxed"
80
58
  />
81
59
  ))}
82
60
 
83
- {/* Links */}
84
61
  {links.length > 0 && (
85
- <div className={cn('mt-8 flex gap-4 flex-wrap', alignments[align] === 'text-center' && 'justify-center')}>
62
+ <div className={cn('mt-8 flex gap-4 flex-wrap', align === 'center' && 'justify-center')}>
86
63
  {links.map((link, index) => (
87
64
  <Link
88
65
  key={index}
89
- href={link.href}
66
+ to={link.href}
90
67
  className={cn(
91
68
  'inline-flex items-center px-6 py-3 font-medium rounded-lg transition-colors',
92
69
  index === 0
93
- ? 'bg-primary-600 text-white hover:bg-primary-700'
94
- : 'border border-current hover:bg-gray-100'
70
+ ? 'bg-primary text-primary-foreground hover:bg-primary-hover'
71
+ : 'bg-secondary text-secondary-foreground hover:bg-secondary-hover'
95
72
  )}
96
73
  >
97
74
  {link.label}
@@ -100,7 +77,6 @@ function Section({ content, params }) {
100
77
  </div>
101
78
  )}
102
79
 
103
- {/* Images */}
104
80
  {imgs.length > 0 && (
105
81
  <div className="mt-8">
106
82
  {imgs.map((img, index) => (
@@ -114,8 +90,6 @@ function Section({ content, params }) {
114
90
  </div>
115
91
  )}
116
92
  </div>
117
- </section>
93
+ </div>
118
94
  )
119
95
  }
120
-
121
- export default Section
@@ -1,8 +1,3 @@
1
- /**
2
- * Section Component Metadata (v2)
3
- *
4
- * A versatile content section for headings, text, and links.
5
- */
6
1
  export default {
7
2
  title: 'Section',
8
3
  description: 'A versatile content section for headings, text, and links',
@@ -19,12 +14,6 @@ export default {
19
14
  },
20
15
 
21
16
  params: {
22
- theme: {
23
- type: 'select',
24
- label: 'Theme',
25
- options: ['light', 'dark', 'primary'],
26
- default: 'light',
27
- },
28
17
  align: {
29
18
  type: 'select',
30
19
  label: 'Alignment',
@@ -47,15 +36,11 @@ export default {
47
36
  presets: {
48
37
  default: {
49
38
  label: 'Centered',
50
- params: { theme: 'light', align: 'center' },
51
- },
52
- dark: {
53
- label: 'Dark Theme',
54
- params: { theme: 'dark', align: 'center' },
39
+ params: { align: 'center' },
55
40
  },
56
41
  left: {
57
42
  label: 'Left Aligned',
58
- params: { theme: 'light', align: 'left' },
43
+ params: { align: 'left' },
59
44
  },
60
45
  },
61
46
  }
@@ -8,7 +8,7 @@ align: center
8
8
 
9
9
  ## Your Uniweb project is ready
10
10
 
11
- This is a minimal starting point for your Uniweb project. Edit the content in `site/pages/` and customize the components in `foundation/src/components/`.
11
+ This is a minimal starting point for your Uniweb project. Edit the content in `site/pages/` and build section types in `foundation/src/sections/`.
12
12
 
13
13
  [About](/about)
14
14
  [Documentation](https://github.com/uniweb)
@@ -51,25 +51,25 @@ colors:
51
51
  # Text: body, heading, subtle
52
52
  # Border: border, ring
53
53
  # Links: link, link-hover
54
- # Actions: primary, primary-foreground, primary-hover
55
- # secondary, secondary-foreground, secondary-hover
54
+ # Actions: primary, primary-foreground, primary-hover, primary-border
55
+ # secondary, secondary-foreground, secondary-hover, secondary-border
56
56
  # Status: success, warning, error, info (+ -subtle variants)
57
57
 
58
58
  # contexts:
59
59
  # light:
60
60
  # section: white
61
61
  # medium:
62
- # section: 'var(--neutral-100)'
62
+ # section: neutral-100
63
63
  # dark:
64
64
  # heading: white
65
65
 
66
66
  # ─── Inline Text Styles ───────────────────────────────────────────────────────
67
- # Named styles for inline text in markdown: [text]{emphasis}
67
+ # Named styles for inline text in markdown: [text]{accent}
68
68
  # Each name maps to CSS properties.
69
- # Defaults: emphasis (colored + bold), muted (subtle).
69
+ # Defaults: accent (colored + bold), muted (subtle).
70
70
 
71
71
  # inline:
72
- # emphasis:
72
+ # accent:
73
73
  # color: 'var(--link)'
74
74
  # font-weight: '600'
75
75
  # muted:
@@ -10,7 +10,7 @@ A website built with [Uniweb](https://github.com/uniweb/cli) — a component web
10
10
  {{projectName}}/
11
11
  ├── foundation/ # React component library
12
12
  │ ├── src/
13
- │ │ ├── components/ # Your components
13
+ │ │ ├── sections/ # Section types (selectable by content authors)
14
14
  │ │ └── styles.css # Tailwind CSS v4 theme
15
15
  │ └── vite.config.js # defineFoundationConfig()
16
16
 
@@ -22,7 +22,7 @@ A website built with [Uniweb](https://github.com/uniweb/cli) — a component web
22
22
  │ ├── site.yml # Site configuration
23
23
  │ └── vite.config.js # defineSiteConfig()
24
24
 
25
- └── AGENTS.md # AI assistant instructions
25
+ └── AGENTS.md # Developer guide (human + AI)
26
26
  ```
27
27
 
28
28
  ## Content Authoring
@@ -46,27 +46,25 @@ Your content here.
46
46
 
47
47
  ## Component Development
48
48
 
49
- Components live in `foundation/src/components/`:
49
+ Section types live in `foundation/src/sections/`. Each is a folder with an `index.jsx` and a `meta.js`:
50
50
 
51
51
  ```jsx
52
- // foundation/src/components/Hero/index.jsx
53
- import { H1, P, cn } from '@uniweb/kit'
52
+ // foundation/src/sections/Hero/index.jsx
53
+ import { H1, P } from '@uniweb/kit'
54
54
 
55
- export function Hero({ content, params }) {
56
- const { title } = content.main?.header || {}
57
- const { theme = 'light' } = params
55
+ export default function Hero({ content }) {
56
+ const { title, paragraphs = [] } = content || {}
58
57
 
59
58
  return (
60
- <section className={cn('py-20', theme === 'dark' && 'bg-gray-900')}>
61
- <H1 text={title} />
62
- </section>
59
+ <div className="max-w-4xl mx-auto px-6">
60
+ <H1 text={title} className="text-heading text-4xl font-bold" />
61
+ <P text={paragraphs} className="text-body mt-4" />
62
+ </div>
63
63
  )
64
64
  }
65
-
66
- export default Hero
67
65
  ```
68
66
 
69
- Exposed components (selectable via `type:` in frontmatter) need a `meta.js` file.
67
+ Components receive parsed content from markdown. Classes like `text-heading` and `text-body` are semantic tokens — the site controls their actual colors through theming, so the same component adapts to any context automatically.
70
68
 
71
69
  {{> components-docs}}
72
70