uniweb 0.8.5 → 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 +5 -5
- package/partials/agents.md +389 -30
- package/src/commands/add.js +149 -5
- package/src/index.js +1 -0
- package/starter/foundation/src/foundation.js +2 -2
- package/templates/site/theme.yml +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.8.
|
|
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/
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
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
|
}
|
package/partials/agents.md
CHANGED
|
@@ -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`):
|
|
@@ -67,8 +97,11 @@ The name is both the directory name and the package name. Use `--project <name>`
|
|
|
67
97
|
pnpm install # Install dependencies
|
|
68
98
|
pnpm dev # Start dev server
|
|
69
99
|
pnpm build # Build for production
|
|
100
|
+
pnpm preview # Preview production build (SSG + SPA)
|
|
70
101
|
```
|
|
71
102
|
|
|
103
|
+
> **npm works too.** Projects include both `pnpm-workspace.yaml` and npm workspaces. Replace `pnpm` with `npm` in any command above.
|
|
104
|
+
|
|
72
105
|
## Content Authoring
|
|
73
106
|
|
|
74
107
|
### Section Format
|
|
@@ -81,11 +114,11 @@ type: Hero
|
|
|
81
114
|
theme: dark
|
|
82
115
|
---
|
|
83
116
|
|
|
84
|
-
###
|
|
117
|
+
### V1.0.0 IS OUT ← pretitle (small label above the title)
|
|
85
118
|
|
|
86
|
-
#
|
|
119
|
+
# Build the system. ← title (the big headline)
|
|
87
120
|
|
|
88
|
-
##
|
|
121
|
+
## Not every page. ← subtitle
|
|
89
122
|
|
|
90
123
|
Description paragraph.
|
|
91
124
|
|
|
@@ -94,6 +127,8 @@ Description paragraph.
|
|
|
94
127
|

|
|
95
128
|
```
|
|
96
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
|
+
|
|
97
132
|
### Content Shape
|
|
98
133
|
|
|
99
134
|
The semantic parser extracts markdown into a flat, guaranteed structure. No null checks needed — empty strings/arrays if content is absent:
|
|
@@ -112,7 +147,7 @@ content = {
|
|
|
112
147
|
insets: [], // Inline @Component references — { refId }
|
|
113
148
|
lists: [], // [[{ paragraphs, links, lists, ... }]] — each list item is an object, not a string
|
|
114
149
|
quotes: [], // Blockquotes
|
|
115
|
-
data: {}, // From tagged code blocks (```yaml:tagname)
|
|
150
|
+
data: {}, // From tagged code blocks (```yaml:tagname) and (```js:tagname)
|
|
116
151
|
headings: [], // Overflow headings after subtitle2
|
|
117
152
|
items: [], // Each has the same flat structure — from headings after body content
|
|
118
153
|
sequence: [], // All elements in document order
|
|
@@ -127,12 +162,38 @@ content = {
|
|
|
127
162
|
We built this for you. ← paragraph
|
|
128
163
|
|
|
129
164
|
### Fast ← items[0].title
|
|
165
|
+
 ← items[0].icons[0]
|
|
130
166
|
Lightning quick. ← items[0].paragraphs[0]
|
|
131
167
|
|
|
132
168
|
### Secure ← items[1].title
|
|
169
|
+
 ← items[1].icons[0]
|
|
133
170
|
Enterprise-grade. ← items[1].paragraphs[0]
|
|
134
171
|
```
|
|
135
172
|
|
|
173
|
+
Each item has the same content shape as the top level — `title`, `paragraphs`, `icons`, `links`, `lists`, etc. are all available per item.
|
|
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
|
+
 │ 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
|
+
 │ 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
|
+
|
|
136
197
|
**Lists** contain bullet or ordered list items. Each list item is an object with the same content shape — not a plain string:
|
|
137
198
|
|
|
138
199
|
```markdown
|
|
@@ -186,6 +247,7 @@ The three parts carry distinct information:
|
|
|
186
247
|
{variant=compact}
|
|
187
248
|
{period=30d}
|
|
188
249
|
{position=top-right}
|
|
250
|
+
{note="Vite + React + Routing — ready to go"}
|
|
189
251
|
```
|
|
190
252
|
|
|
191
253
|
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.
|
|
@@ -198,8 +260,26 @@ Inset components must declare `inset: true` in their `meta.js`. They render at t
|
|
|
198
260
|
{role=banner} <!-- Role determines array: imgs, icons, or videos -->
|
|
199
261
|
```
|
|
200
262
|
|
|
263
|
+
**Quote values that contain spaces:** `{note="Ready to go"}` not `{note=Ready to go}`. Unquoted values end at the first space.
|
|
264
|
+
|
|
201
265
|
Standalone links (alone on a line) become buttons. Inline links stay as text links.
|
|
202
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
|
+
|
|
203
283
|
### Structured Data
|
|
204
284
|
|
|
205
285
|
Tagged code blocks pass structured data via `content.data`:
|
|
@@ -215,19 +295,73 @@ submitLabel: Send
|
|
|
215
295
|
|
|
216
296
|
Access: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
|
|
217
297
|
|
|
298
|
+
**Code blocks need tags too.** Untagged code blocks (plain ```js) are only visible to sequential-rendering components like Article or DocSection. If a component needs to access code blocks by name, tag them:
|
|
299
|
+
|
|
300
|
+
````markdown
|
|
301
|
+
```jsx:before
|
|
302
|
+
const old = fetch('/api')
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```jsx:after
|
|
306
|
+
const data = useData()
|
|
307
|
+
```
|
|
308
|
+
````
|
|
309
|
+
|
|
310
|
+
Access: `content.data?.before`, `content.data?.after` → raw code strings.
|
|
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
|
+
-  [Home](/)
|
|
320
|
+
-  [Docs](/docs)
|
|
321
|
+
-  [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
|
+
|
|
218
352
|
### Section Backgrounds
|
|
219
353
|
|
|
220
|
-
Set `background` in frontmatter — the runtime renders it automatically:
|
|
354
|
+
Set `background` in frontmatter — the runtime renders it automatically. The string form auto-detects the type:
|
|
221
355
|
|
|
222
356
|
```yaml
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
background:
|
|
227
|
-
|
|
357
|
+
background: /images/hero.jpg # Image (by extension)
|
|
358
|
+
background: /videos/hero.mp4 # Video (by extension)
|
|
359
|
+
background: linear-gradient(135deg, #667eea, #764ba2) # CSS gradient
|
|
360
|
+
background: '#1a1a2e' # Color (hex — quote in YAML)
|
|
361
|
+
background: var(--primary-900) # Color (CSS variable)
|
|
228
362
|
```
|
|
229
363
|
|
|
230
|
-
|
|
364
|
+
The object form gives more control:
|
|
231
365
|
|
|
232
366
|
```yaml
|
|
233
367
|
background:
|
|
@@ -235,6 +369,8 @@ background:
|
|
|
235
369
|
overlay: { enabled: true, type: dark, opacity: 0.5 }
|
|
236
370
|
```
|
|
237
371
|
|
|
372
|
+
Overlay shorthand — `overlay: 0.5` is equivalent to `{ enabled: true, type: dark, opacity: 0.5 }`.
|
|
373
|
+
|
|
238
374
|
Components that render their own background declare `background: 'self'` in `meta.js`.
|
|
239
375
|
|
|
240
376
|
### Page Organization
|
|
@@ -385,7 +521,12 @@ CCA separates theme from code. Components use **semantic CSS tokens** instead of
|
|
|
385
521
|
| `text-link` | Link color |
|
|
386
522
|
| `bg-primary` | Primary action background |
|
|
387
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) |
|
|
388
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 |
|
|
389
530
|
| `text-success` / `bg-success-subtle` | Status: success |
|
|
390
531
|
| `text-error` / `bg-error-subtle` | Status: error |
|
|
391
532
|
| `text-warning` / `bg-warning-subtle` | Status: warning |
|
|
@@ -417,7 +558,7 @@ The runtime does significant work that other frameworks push onto components. Un
|
|
|
417
558
|
| `isDark ? 'text-white' : 'text-gray-900'` | Just write `text-heading` — it adapts |
|
|
418
559
|
| Background rendering code | Declare `background:` in frontmatter instead |
|
|
419
560
|
| Color constants / tokens files | Colors come from `theme.yml` |
|
|
420
|
-
|
|
|
561
|
+
| Parallel color system (`--ink`, `--paper`) that duplicates what tokens already provide | Map source color roles to `theme.yml` colors/neutral. The build generates `--primary-50` through `--primary-950`, `--neutral-50` through `--neutral-950`, etc. Use palette shades directly (`var(--primary-300)`) for specific tones. Additive design classes that BUILD ON tokens are fine — a parallel system that REPLACES them bypasses context adaptation. |
|
|
421
562
|
|
|
422
563
|
**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.
|
|
423
564
|
|
|
@@ -430,6 +571,33 @@ theme: dark ← sets context-dark, all tokens resolve to dark values
|
|
|
430
571
|
---
|
|
431
572
|
```
|
|
432
573
|
|
|
574
|
+
Alternate between `light` (default), `medium`, and `dark` across sections for visual rhythm — no CSS needed. A typical marketing page:
|
|
575
|
+
|
|
576
|
+
```markdown
|
|
577
|
+
<!-- 1-hero.md -->
|
|
578
|
+
theme: dark
|
|
579
|
+
|
|
580
|
+
<!-- 2-features.md -->
|
|
581
|
+
(no theme — defaults to light)
|
|
582
|
+
|
|
583
|
+
<!-- 3-testimonials.md -->
|
|
584
|
+
theme: medium
|
|
585
|
+
|
|
586
|
+
<!-- 4-cta.md -->
|
|
587
|
+
theme: dark
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Per-section token overrides** — the object form lets authors fine-tune individual tokens for a specific section:
|
|
591
|
+
|
|
592
|
+
```yaml
|
|
593
|
+
theme:
|
|
594
|
+
mode: light
|
|
595
|
+
primary: neutral-900 # Dark buttons in a light section
|
|
596
|
+
primary-hover: neutral-800
|
|
597
|
+
```
|
|
598
|
+
|
|
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.
|
|
600
|
+
|
|
433
601
|
**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.
|
|
434
602
|
|
|
435
603
|
### theme.yml
|
|
@@ -437,18 +605,14 @@ theme: dark ← sets context-dark, all tokens resolve to dark values
|
|
|
437
605
|
```yaml
|
|
438
606
|
# site/theme.yml
|
|
439
607
|
colors:
|
|
440
|
-
primary:
|
|
441
|
-
base: '#3b82f6'
|
|
442
|
-
exactMatch: true # Use this exact hex at the 500 shade
|
|
608
|
+
primary: '#3b82f6' # Your exact hex appears at shade 500
|
|
443
609
|
secondary: '#64748b'
|
|
444
610
|
accent: '#8b5cf6'
|
|
445
|
-
neutral: stone
|
|
611
|
+
neutral: stone # Named preset: stone, zinc, gray, slate, neutral
|
|
446
612
|
|
|
447
613
|
contexts:
|
|
448
614
|
light:
|
|
449
|
-
section: '#fafaf9'
|
|
450
|
-
primary: var(--primary-500)
|
|
451
|
-
primary-hover: var(--primary-600)
|
|
615
|
+
section: '#fafaf9' # Override individual tokens per context
|
|
452
616
|
|
|
453
617
|
fonts:
|
|
454
618
|
import:
|
|
@@ -467,23 +631,86 @@ vars: # Override foundation-declared variables
|
|
|
467
631
|
|
|
468
632
|
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:`.
|
|
469
633
|
|
|
634
|
+
### How colors reach components
|
|
635
|
+
|
|
636
|
+
Your hex color → 11 shades (50–950) → semantic tokens → components.
|
|
637
|
+
|
|
638
|
+
**Shade 500 = your exact input color.** The build generates lighter shades (50–400) above it and darker shades (600–950) below it, redistributing lightness proportionally to maintain a smooth scale. Set `exactMatch: false` on a color to opt out and use fixed lightness values instead.
|
|
639
|
+
|
|
640
|
+
Semantic tokens map shades to roles. The defaults for light/medium contexts:
|
|
641
|
+
|
|
642
|
+
| Token | Shade | Purpose |
|
|
643
|
+
|-------|-------|---------|
|
|
644
|
+
| `--primary` | 600 | Button background |
|
|
645
|
+
| `--primary-hover` | 700 | Button hover |
|
|
646
|
+
| `--link` | 600 | Link color |
|
|
647
|
+
| `--ring` | 500 | Focus ring |
|
|
648
|
+
|
|
649
|
+
In dark contexts, `--primary` uses shade 500 and `--link` uses shade 400.
|
|
650
|
+
|
|
651
|
+
**Buttons and links use shade 600 — darker than your input.** This is an accessibility choice: shade 600 provides better contrast with white button text. For medium-bright brand colors like orange, buttons will be noticeably darker than the brand color.
|
|
652
|
+
|
|
653
|
+
**Recipe — brand-exact buttons:**
|
|
654
|
+
|
|
655
|
+
```yaml
|
|
656
|
+
colors:
|
|
657
|
+
primary: "#E35D25"
|
|
658
|
+
|
|
659
|
+
contexts:
|
|
660
|
+
light:
|
|
661
|
+
primary: primary-500 # Your exact color on buttons
|
|
662
|
+
primary-hover: primary-600 # Darker on hover
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
> **Contrast warning:** Bright brand colors (orange, yellow, light green) at shade 500 may not meet WCAG contrast (4.5:1) with white foreground text. Test buttons for readability — if contrast is insufficient, keep the default shade 600 mapping or darken your base color.
|
|
666
|
+
|
|
470
667
|
### Foundation variables
|
|
471
668
|
|
|
472
|
-
Foundations declare customizable layout/spacing values in `foundation.js
|
|
669
|
+
Foundations declare customizable layout/spacing values in `foundation.js`. The starter includes:
|
|
473
670
|
|
|
474
671
|
```js
|
|
475
|
-
export
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
},
|
|
672
|
+
export const vars = {
|
|
673
|
+
'header-height': { default: '4rem', description: 'Fixed header height' },
|
|
674
|
+
'max-content-width': { default: '80rem', description: 'Maximum content width' },
|
|
675
|
+
'section-padding-y': { default: 'clamp(4rem, 6vw, 7rem)', description: 'Vertical padding for sections' },
|
|
480
676
|
}
|
|
481
677
|
```
|
|
482
678
|
|
|
483
|
-
Sites override them in `theme.yml` under `vars:`. Components use them
|
|
679
|
+
Sites override them in `theme.yml` under `vars:`. Components use them via Tailwind arbitrary values or CSS: `py-[var(--section-padding-y)]`, `h-[var(--header-height)]`, etc.
|
|
680
|
+
|
|
681
|
+
The `section-padding-y` default uses `clamp()` for fluid spacing — tighter on mobile, more breathing room on large screens. Use this variable for consistent section spacing instead of hardcoding padding in each component. Sites can override to a fixed value (`section-padding-y: 3rem`) or a different clamp in `theme.yml`.
|
|
484
682
|
|
|
485
683
|
**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.
|
|
486
684
|
|
|
685
|
+
### Design richness beyond tokens
|
|
686
|
+
|
|
687
|
+
Semantic tokens handle context adaptation — the hard problem of making colors work in light, medium, and dark sections. **They are a floor, not a ceiling.** A great foundation adds its own design vocabulary on top.
|
|
688
|
+
|
|
689
|
+
The token set is deliberately small (24 tokens). It covers the dimensions that change per context. Everything that stays constant across contexts — border weights, shadow depth, radius scales, gradient angles, accent borders, glassmorphism, elevation layers — belongs in your foundation's `styles.css` or component code.
|
|
690
|
+
|
|
691
|
+
**Don't flatten a rich design to fit the token set.** If a source design has 4 border tones, create them:
|
|
692
|
+
|
|
693
|
+
```css
|
|
694
|
+
/* foundation/src/styles.css */
|
|
695
|
+
.border-subtle { border-color: color-mix(in oklch, var(--border), transparent 50%); }
|
|
696
|
+
.border-strong { border-color: color-mix(in oklch, var(--border), var(--heading) 30%); }
|
|
697
|
+
.border-accent { border-color: var(--primary-300); }
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
These compose with semantic tokens — they adapt per context because they reference `--border`, `--heading`, or palette shades. But they add design nuance the token set alone doesn't provide.
|
|
701
|
+
|
|
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.
|
|
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
|
+
|
|
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.
|
|
713
|
+
|
|
487
714
|
## Component Development
|
|
488
715
|
|
|
489
716
|
### Props Interface
|
|
@@ -509,15 +736,97 @@ function Hero({ content, params }) {
|
|
|
509
736
|
)
|
|
510
737
|
}
|
|
511
738
|
|
|
512
|
-
Hero.className = 'pt-32 md:pt-48' //
|
|
739
|
+
Hero.className = 'pt-32 md:pt-48' // Override spacing for hero (more top padding)
|
|
513
740
|
Hero.as = 'div' // Change wrapper element (default: 'section')
|
|
514
741
|
|
|
515
742
|
export default Hero
|
|
516
743
|
```
|
|
517
744
|
|
|
518
|
-
- `Component.className` — adds classes to the runtime's wrapper. Use for section-level
|
|
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`).
|
|
519
746
|
- `Component.as` — changes the wrapper element. Use `'nav'` for headers, `'footer'` for footers, `'div'` when `<section>` isn't semantically appropriate.
|
|
520
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
|
+
-  [How It Works](/how-it-works)
|
|
771
|
+
-  [For Teams](/for-teams)
|
|
772
|
+
-  [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
|
+
|
|
521
830
|
### meta.js Structure
|
|
522
831
|
|
|
523
832
|
```javascript
|
|
@@ -577,6 +886,8 @@ import { H2, P, Span } from '@uniweb/kit'
|
|
|
577
886
|
<Text text={content.title} as="h2" className="..." /> // explicit tag
|
|
578
887
|
```
|
|
579
888
|
|
|
889
|
+
These components render their own HTML tag — don't wrap them in a matching tag. `<h2><H2 text={...} /></h2>` creates a nested `<h2><h2>...</h2></h2>`, which is invalid HTML. Just use `<H2 text={...} />` directly.
|
|
890
|
+
|
|
580
891
|
Don't render content strings with `{content.paragraphs[0]}` in JSX — that shows HTML tags as visible text. Use `<P>`, `<H2>`, `<Span>`, etc. for content strings.
|
|
581
892
|
|
|
582
893
|
**Rendering full content** (`@uniweb/kit`):
|
|
@@ -596,15 +907,29 @@ import { Section, Render } from '@uniweb/kit'
|
|
|
596
907
|
|
|
597
908
|
**Other primitives** (`@uniweb/kit`): `Link`, `Image`, `Icon`, `Media`, `Asset`, `SafeHtml`, `SocialIcon`, `FileLogo`, `cn()`
|
|
598
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
|
+
|
|
599
919
|
**Other styled** (`@uniweb/kit`): `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
600
920
|
|
|
601
921
|
**Hooks:**
|
|
602
922
|
- `useScrolled(threshold)` → boolean for scroll-based header styling
|
|
603
923
|
- `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
|
|
604
924
|
- `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
|
|
605
|
-
- `useActiveRoute()` → `{ route, isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
925
|
+
- `useActiveRoute()` → `{ route, rootSegment, isActive(page), isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
606
926
|
- `useGridLayout(columns, { gap })` → responsive grid class string
|
|
607
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
|
|
608
933
|
|
|
609
934
|
**Utilities:** `cn()` (Tailwind class merge), `filterSocialLinks(links)`, `getSocialPlatform(url)`
|
|
610
935
|
|
|
@@ -642,6 +967,28 @@ website.hasMultipleLocales()
|
|
|
642
967
|
website.getLocales() // [{ code, label, isDefault }]
|
|
643
968
|
website.getActiveLocale() // 'en'
|
|
644
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
|
|
645
992
|
```
|
|
646
993
|
|
|
647
994
|
### Insets and the Visual Component
|
|
@@ -670,6 +1017,8 @@ function SplitContent({ content, block }) {
|
|
|
670
1017
|
- `block.getInset(refId)` — lookup by refId (used by sequential renderers)
|
|
671
1018
|
- `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
|
|
672
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
|
+
|
|
673
1022
|
Inset components declare `inset: true` in meta.js. Use `hidden: true` for inset-only components:
|
|
674
1023
|
|
|
675
1024
|
```js
|
|
@@ -853,7 +1202,7 @@ Foundation styles in `foundation/src/styles.css`:
|
|
|
853
1202
|
|
|
854
1203
|
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).
|
|
855
1204
|
|
|
856
|
-
**Custom CSS is
|
|
1205
|
+
**Custom CSS is expected alongside Tailwind.** Your foundation's `styles.css` is the design layer — shadow systems, border hierarchies, gradient effects, accent treatments, elevation scales, glassmorphism. If the source design has a visual detail, create a class for it. Tailwind handles layout and spacing; semantic tokens handle context adaptation; `styles.css` handles everything else that makes the design rich and distinctive.
|
|
857
1206
|
|
|
858
1207
|
## Troubleshooting
|
|
859
1208
|
|
|
@@ -863,6 +1212,16 @@ Semantic color tokens (`text-heading`, `bg-section`, `bg-primary`, etc.) come fr
|
|
|
863
1212
|
|
|
864
1213
|
**Styles not applying** — Verify `@source` in `styles.css` includes your component paths. Check custom colors match `@theme` definitions.
|
|
865
1214
|
|
|
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[]`).
|
|
1224
|
+
|
|
866
1225
|
## Further Documentation
|
|
867
1226
|
|
|
868
1227
|
Full Uniweb documentation is available at **https://github.com/uniweb/docs** — raw markdown files you can fetch directly.
|
package/src/commands/add.js
CHANGED
|
@@ -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
|
|
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
|
|
@@ -24,8 +24,8 @@ export const vars = {
|
|
|
24
24
|
description: 'Maximum content width (1280px)',
|
|
25
25
|
},
|
|
26
26
|
'section-padding-y': {
|
|
27
|
-
default: '
|
|
28
|
-
description: 'Vertical padding for sections',
|
|
27
|
+
default: 'clamp(4rem, 6vw, 7rem)',
|
|
28
|
+
description: 'Vertical padding for sections (fluid: adapts to viewport)',
|
|
29
29
|
},
|
|
30
30
|
}
|
|
31
31
|
|
package/templates/site/theme.yml
CHANGED
|
@@ -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:
|
|
62
|
+
# section: neutral-100
|
|
63
63
|
# dark:
|
|
64
64
|
# heading: white
|
|
65
65
|
|