uniweb 0.8.6 → 0.8.7

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.7",
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.6",
45
+ "@uniweb/runtime": "0.6.5",
46
+ "@uniweb/kit": "0.7.4",
47
+ "@uniweb/core": "0.5.5"
48
48
  }
49
49
  }
@@ -2,6 +2,27 @@
2
2
 
3
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
4
 
5
+ ## Documentation
6
+
7
+ This project was created with [Uniweb](https://github.com/uniweb/cli). Full documentation (markdown, fetchable): https://github.com/uniweb/docs
8
+
9
+ **To read a specific page:** `https://raw.githubusercontent.com/uniweb/docs/main/{section}/{page}.md`
10
+
11
+ **By task:**
12
+
13
+ | Task | Doc page |
14
+ |------|----------|
15
+ | Writing page content | `authoring/writing-content.md` |
16
+ | Theming and styling | `authoring/theming.md` |
17
+ | Building components | `development/creating-components.md` |
18
+ | Kit API (hooks, components) | `reference/kit-reference.md` |
19
+ | Site configuration | `reference/site-configuration.md` |
20
+ | Content shape reference | `reference/content-structure.md` |
21
+ | Component metadata (meta.js) | `reference/component-metadata.md` |
22
+ | Migrating existing designs | `development/converting-existing.md` |
23
+
24
+ > **npm registry:** Use `https://registry.npmjs.org/uniweb` for package metadata — the npmjs.com website blocks automated requests.
25
+
5
26
  ## Project Structure
6
27
 
7
28
  ```
@@ -49,6 +70,15 @@ pnpm uniweb add site blog # Named → ./blog/
49
70
 
50
71
  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
72
 
73
+ ### Adding section types
74
+
75
+ ```bash
76
+ pnpm uniweb add section Hero
77
+ pnpm uniweb add section Hero --foundation ui # When multiple foundations exist
78
+ ```
79
+
80
+ 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.
81
+
52
82
  ### What the CLI generates
53
83
 
54
84
  **Foundation** (`vite.config.js`, `package.json`, `src/foundation.js`, `src/styles.css`):
@@ -84,11 +114,11 @@ type: Hero
84
114
  theme: dark
85
115
  ---
86
116
 
87
- ### Eyebrow Text ← pretitle (heading before a more important one)
117
+ ### V1.0.0 IS OUT ← pretitle (small label above the title)
88
118
 
89
- # Main Headline ← title
119
+ # Build the system. ← title (the big headline)
90
120
 
91
- ## Subtitle ← subtitle
121
+ ## Not every page. ← subtitle
92
122
 
93
123
  Description paragraph.
94
124
 
@@ -97,6 +127,8 @@ Description paragraph.
97
127
  ![Image](./image.jpg)
98
128
  ```
99
129
 
130
+ 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.
131
+
100
132
  ### Content Shape
101
133
 
102
134
  The semantic parser extracts markdown into a flat, guaranteed structure. No null checks needed — empty strings/arrays if content is absent:
@@ -140,6 +172,28 @@ Enterprise-grade. ← items[1].paragraphs[0]
140
172
 
141
173
  Each item has the same content shape as the top level — `title`, `paragraphs`, `icons`, `links`, `lists`, etc. are all available per item.
142
174
 
175
+ **Complete example — markdown and resulting content shape side by side:**
176
+
177
+ ```markdown
178
+ ### Eyebrow │ content.pretitle = "Eyebrow"
179
+ # Our Features │ content.title = "Our Features"
180
+ ## Build better products │ content.subtitle = "Build better products"
181
+
182
+ We help teams ship faster. │ content.paragraphs[0] = "We help teams..."
183
+
184
+ [Get Started](/start) │ content.links[0] = { href: "/start", label: "Get Started" }
185
+
186
+ ### Fast │ content.items[0].title = "Fast"
187
+ ![](lu-zap) │ content.items[0].icons[0] = { library: "lu", name: "zap" }
188
+ Lightning quick. │ content.items[0].paragraphs[0] = "Lightning quick."
189
+
190
+ ### Secure │ content.items[1].title = "Secure"
191
+ ![](lu-shield) │ content.items[1].icons[0] = { library: "lu", name: "shield" }
192
+ Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise-grade..."
193
+ ```
194
+
195
+ Headings before the main title become `pretitle`. Headings after the main title at a lower importance become `subtitle`. Headings that appear after body content (paragraphs, links, images) start the `items` array.
196
+
143
197
  **Lists** contain bullet or ordered list items. Each list item is an object with the same content shape — not a plain string:
144
198
 
145
199
  ```markdown
@@ -210,6 +264,22 @@ Inset components must declare `inset: true` in their `meta.js`. They render at t
210
264
 
211
265
  Standalone links (alone on a line) become buttons. Inline links stay as text links.
212
266
 
267
+ **Standalone links** — paragraphs that contain *only* links (no other text) are promoted to `content.links[]`. This works for single links and for multiple links sharing a paragraph:
268
+
269
+ ```markdown
270
+ [Primary](/start) ← standalone → content.links[0]
271
+
272
+ [Secondary](/learn) ← standalone → content.links[1]
273
+
274
+ [One](/a) [Two](/b) ← links-only paragraph → content.links[0], content.links[1]
275
+ ```
276
+
277
+ Links mixed with non-link text stay as inline `<a>` tags within `content.paragraphs[]`:
278
+
279
+ ```markdown
280
+ Check out [this](/a) and [that](/b). ← inline links in paragraph text, NOT in content.links[]
281
+ ```
282
+
213
283
  ### Structured Data
214
284
 
215
285
  Tagged code blocks pass structured data via `content.data`:
@@ -239,6 +309,46 @@ const data = useData()
239
309
 
240
310
  Access: `content.data?.before`, `content.data?.after` → raw code strings.
241
311
 
312
+ ### Lists as Navigation Menus
313
+
314
+ 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`.
315
+
316
+ **Header nav — flat list with icons and links:**
317
+
318
+ ```markdown
319
+ - ![](lu-home) [Home](/)
320
+ - ![](lu-book) [Docs](/docs)
321
+ - ![](lu-mail) [Contact](/contact)
322
+ ```
323
+
324
+ Access: `content.lists[0]` — each item has `item.links[0]` (href + label) and `item.icons[0]` (icon).
325
+
326
+ **Footer — nested list for grouped links:**
327
+
328
+ ```markdown
329
+ - Product
330
+ - [Features](/features)
331
+ - [Pricing](/pricing)
332
+ - Company
333
+ - [About](/about)
334
+ - [Careers](/careers)
335
+ ```
336
+
337
+ 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]`).
338
+
339
+ ```jsx
340
+ content.lists[0]?.map((group, i) => (
341
+ <div key={i}>
342
+ <Span text={group.paragraphs[0]} className="font-semibold text-heading" />
343
+ <ul>
344
+ {group.lists[0]?.map((subItem, j) => (
345
+ <li key={j}><Link to={subItem.links[0]?.href}>{subItem.links[0]?.label}</Link></li>
346
+ ))}
347
+ </ul>
348
+ </div>
349
+ ))
350
+ ```
351
+
242
352
  ### Section Backgrounds
243
353
 
244
354
  Set `background` in frontmatter — the runtime renders it automatically. The string form auto-detects the type:
@@ -411,7 +521,12 @@ CCA separates theme from code. Components use **semantic CSS tokens** instead of
411
521
  | `text-link` | Link color |
412
522
  | `bg-primary` | Primary action background |
413
523
  | `text-primary-foreground` | Text on primary background |
524
+ | `hover:bg-primary-hover` | Primary hover state |
525
+ | `border-primary-border` | Primary border (transparent by default) |
414
526
  | `bg-secondary` | Secondary action background |
527
+ | `text-secondary-foreground` | Text on secondary background |
528
+ | `hover:bg-secondary-hover` | Secondary hover state |
529
+ | `border-secondary-border` | Secondary border |
415
530
  | `text-success` / `bg-success-subtle` | Status: success |
416
531
  | `text-error` / `bg-error-subtle` | Status: error |
417
532
  | `text-warning` / `bg-warning-subtle` | Status: warning |
@@ -477,8 +592,8 @@ theme: dark
477
592
  ```yaml
478
593
  theme:
479
594
  mode: light
480
- primary: var(--neutral-900) # Dark buttons in a light section
481
- primary-hover: var(--neutral-800)
595
+ primary: neutral-900 # Dark buttons in a light section
596
+ primary-hover: neutral-800
482
597
  ```
483
598
 
484
599
  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.
@@ -586,6 +701,14 @@ These compose with semantic tokens — they adapt per context because they refer
586
701
 
587
702
  **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
703
 
704
+ **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:
705
+
706
+ ```css
707
+ /* foundation/src/styles.css */
708
+ .text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
709
+ .text-disabled { color: color-mix(in oklch, var(--subtle), transparent 40%); }
710
+ ```
711
+
589
712
  **When migrating from an existing design**, map every visual detail — not just the ones that have a semantic token. Shadow systems, border hierarchies, custom hover effects, accent tints: create CSS classes or Tailwind utilities in `styles.css` for anything the original has that tokens don't cover. Use palette shades directly (`var(--primary-300)`, `bg-neutral-200`) for fine-grained color control beyond the semantic tokens.
590
713
 
591
714
  ## Component Development
@@ -622,6 +745,88 @@ export default Hero
622
745
  - `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
746
  - `Component.as` — changes the wrapper element. Use `'nav'` for headers, `'footer'` for footers, `'div'` when `<section>` isn't semantically appropriate.
624
747
 
748
+ **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:
749
+
750
+ ```jsx
751
+ function Header({ content, block }) { /* ... */ }
752
+ Header.className = 'p-0'
753
+ Header.as = 'header'
754
+ export default Header
755
+ ```
756
+
757
+ ### Content Patterns for Header and Footer
758
+
759
+ 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:
760
+
761
+ **Header** — title for logo, list for nav links, standalone link for CTA, tagged YAML for metadata:
762
+
763
+ ````markdown
764
+ ---
765
+ type: Header
766
+ ---
767
+
768
+ # Acme Inc
769
+
770
+ - ![](lu-search) [How It Works](/how-it-works)
771
+ - ![](lu-users) [For Teams](/for-teams)
772
+ - ![](lu-book) [Docs](/docs)
773
+
774
+ [Get Started](/docs/quickstart)
775
+
776
+ ```yaml:config
777
+ github: https://github.com/acme
778
+ version: v2.1.0
779
+ ```
780
+ ````
781
+
782
+ ```jsx
783
+ function Header({ content, block }) {
784
+ const logo = content.title // "Acme Inc"
785
+ const navItems = content.lists[0] || [] // [{icons, links}, ...]
786
+ const cta = content.links[0] // {href, label}
787
+ const config = content.data?.config // {github, version}
788
+ // ...
789
+ }
790
+ ```
791
+
792
+ **Footer** — paragraph for tagline, nested list for grouped columns, tagged YAML for legal:
793
+
794
+ ````markdown
795
+ ---
796
+ type: Footer
797
+ ---
798
+
799
+ Build something great.
800
+
801
+ - Product
802
+ - [Features](/features)
803
+ - [Pricing](/pricing)
804
+ - Developers
805
+ - [Docs](/docs)
806
+ - [GitHub](https://github.com/acme){target=_blank}
807
+ - Community
808
+ - [Discord](#)
809
+ - [Blog](/blog)
810
+
811
+ ```yaml:legal
812
+ copyright: © 2025 Acme Inc
813
+ ```
814
+ ````
815
+
816
+ ```jsx
817
+ function Footer({ content, block }) {
818
+ const tagline = content.paragraphs[0] // "Build something great."
819
+ const columns = content.lists[0] || [] // [{paragraphs, lists}, ...]
820
+ const legal = content.data?.legal // {copyright}
821
+
822
+ // Each column: group.paragraphs[0] = label, group.lists[0] = links
823
+ columns.map(group => ({
824
+ label: group.paragraphs[0],
825
+ links: group.lists[0]?.map(item => item.links[0])
826
+ }))
827
+ }
828
+ ```
829
+
625
830
  ### meta.js Structure
626
831
 
627
832
  ```javascript
@@ -702,15 +907,29 @@ import { Section, Render } from '@uniweb/kit'
702
907
 
703
908
  **Other primitives** (`@uniweb/kit`): `Link`, `Image`, `Icon`, `Media`, `Asset`, `SafeHtml`, `SocialIcon`, `FileLogo`, `cn()`
704
909
 
910
+ `Link` props: `to` (or `href`), `target`, `reload`, `download`, `className`, `children`:
911
+
912
+ ```jsx
913
+ <Link to="/about">About</Link> // SPA navigation via React Router
914
+ <Link to="page:about">About</Link> // Resolves page ID to route
915
+ <Link reload href={localeUrl}>ES</Link> // Full page reload, prepends basePath
916
+ // External URLs auto-get target="_blank" and rel="noopener noreferrer"
917
+ ```
918
+
705
919
  **Other styled** (`@uniweb/kit`): `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
706
920
 
707
921
  **Hooks:**
708
922
  - `useScrolled(threshold)` → boolean for scroll-based header styling
709
923
  - `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
710
924
  - `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
711
- - `useActiveRoute()` → `{ route, isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
925
+ - `useActiveRoute()` → `{ route, rootSegment, isActive(page), isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
712
926
  - `useGridLayout(columns, { gap })` → responsive grid class string
713
927
  - `useTheme(name)` → standardized theme classes
928
+ - `useAppearance()` → `{ scheme, toggle, canToggle, setScheme, schemes }` — light/dark mode control with localStorage persistence
929
+ - `useRouting()` → `{ useLocation, useParams, useNavigate, Link, isRoutingAvailable }` — SSG-safe routing access (returns no-op fallbacks during prerender)
930
+ - `useWebsite()` → `{ website, localize, makeHref, getLanguage, getLanguages, getRoutingComponents }` — primary runtime hook
931
+ - `useThemeData()` → Theme instance for programmatic color access (`getColor(name, shade)`, `getPalette(name)`)
932
+ - `useColorContext(block)` → `'light' | 'medium' | 'dark'` — current section's color context
714
933
 
715
934
  **Utilities:** `cn()` (Tailwind class merge), `filterSocialLinks(links)`, `getSocialPlatform(url)`
716
935
 
@@ -748,6 +967,28 @@ website.hasMultipleLocales()
748
967
  website.getLocales() // [{ code, label, isDefault }]
749
968
  website.getActiveLocale() // 'en'
750
969
  website.getLocaleUrl('es')
970
+
971
+ // Core properties
972
+ website.name // Site name from site.yml
973
+ website.basePath // Deployment base path (e.g., '/docs/')
974
+
975
+ // Route detection
976
+ const { isActive, isActiveOrAncestor } = useActiveRoute()
977
+ isActive(page) // Exact match
978
+ isActiveOrAncestor(page) // Ancestor match (for parent highlighting in nav)
979
+
980
+ // Appearance (light/dark mode)
981
+ const { scheme, toggle, canToggle } = useAppearance()
982
+
983
+ // Page properties
984
+ page.title // Page title
985
+ page.label // Short label for nav (falls back to title)
986
+ page.route // Route path
987
+ page.isHidden() // Hidden from navigation
988
+ page.showInHeader() // Visible in header nav
989
+ page.showInFooter() // Visible in footer nav
990
+ page.hasChildren() // Has child pages
991
+ page.children // Array of child Page objects
751
992
  ```
752
993
 
753
994
  ### Insets and the Visual Component
@@ -776,6 +1017,8 @@ function SplitContent({ content, block }) {
776
1017
  - `block.getInset(refId)` — lookup by refId (used by sequential renderers)
777
1018
  - `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
778
1019
 
1020
+ **SSG and hooks:** Inset components that use React hooks (useState, useEffect) will trigger prerender warnings during `pnpm build`. This is expected — the SSG pipeline cannot render hooks due to dual React instances in the build. The warnings are informational; the page renders correctly client-side. If you see `"Skipped SSG for /..."` or `"Invalid hook call"`, this is the cause.
1021
+
779
1022
  Inset components declare `inset: true` in meta.js. Use `hidden: true` for inset-only components:
780
1023
 
781
1024
  ```js
@@ -969,7 +1212,15 @@ Semantic color tokens (`text-heading`, `bg-section`, `bg-primary`, etc.) come fr
969
1212
 
970
1213
  **Styles not applying** — Verify `@source` in `styles.css` includes your component paths. Check custom colors match `@theme` definitions.
971
1214
 
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.
1215
+ **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.
1216
+
1217
+ **Content not appearing as expected?** In dev mode, open the browser console and inspect the parsed content shape your component receives:
1218
+
1219
+ ```js
1220
+ globalThis.uniweb.activeWebsite.activePage.bodyBlocks[0].parsedContent
1221
+ ```
1222
+
1223
+ 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
1224
 
974
1225
  ## Further Documentation
975
1226
 
@@ -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
@@ -51,15 +51,15 @@ 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