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 +5 -5
- package/partials/agents.md +368 -19
- package/partials/ai-assistance.hbs +5 -3
- package/partials/learn-more.hbs +3 -2
- package/src/commands/add.js +149 -5
- package/src/index.js +1 -0
- package/starter/foundation/src/sections/Section/index.jsx +13 -39
- package/starter/foundation/src/sections/Section/meta.js +2 -17
- package/starter/site/pages/home/1-welcome.md.hbs +1 -1
- package/templates/site/theme.yml +6 -6
- package/templates/workspace/README.md.hbs +12 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.8.
|
|
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.
|
|
45
|
-
"@uniweb/kit": "0.7.
|
|
46
|
-
"@uniweb/core": "0.5.
|
|
47
|
-
"@uniweb/runtime": "0.6.
|
|
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
|
}
|
package/partials/agents.md
CHANGED
|
@@ -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
|
-
###
|
|
119
|
+
### V1.0.0 IS OUT ← pretitle (small label above the title)
|
|
88
120
|
|
|
89
|
-
#
|
|
121
|
+
# Build the system. ← title (the big headline)
|
|
90
122
|
|
|
91
|
-
##
|
|
123
|
+
## Not every page. ← subtitle
|
|
92
124
|
|
|
93
125
|
Description paragraph.
|
|
94
126
|
|
|
@@ -97,15 +129,17 @@ Description paragraph.
|
|
|
97
129
|

|
|
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
|
+
 │ 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
|
+
 │ 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
|
+
-  [Home](/)
|
|
414
|
+
-  [Docs](/docs)
|
|
415
|
+
-  [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:
|
|
481
|
-
primary-hover:
|
|
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
|
-
|
|
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
|
+
-  [How It Works](/how-it-works)
|
|
865
|
+
-  [For Teams](/for-teams)
|
|
866
|
+
-  [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, //
|
|
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
|
-
<
|
|
673
|
-
<
|
|
674
|
-
<
|
|
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
|
|
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
|
|
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** —
|
|
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
|
-
##
|
|
1
|
+
## Developer Guide
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
###
|
|
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
|
```
|
package/partials/learn-more.hbs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
## Learn More
|
|
2
2
|
|
|
3
|
-
- [
|
|
4
|
-
- [
|
|
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)
|
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
|
|
@@ -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
|
-
*
|
|
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
|
-
<
|
|
44
|
-
<div className={cn('mx-auto', widths[width]
|
|
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-
|
|
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=
|
|
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=
|
|
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',
|
|
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
|
-
|
|
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
|
|
94
|
-
: '
|
|
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
|
-
</
|
|
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: {
|
|
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: {
|
|
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
|
|
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)
|
package/templates/site/theme.yml
CHANGED
|
@@ -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:
|
|
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]{
|
|
67
|
+
# Named styles for inline text in markdown: [text]{accent}
|
|
68
68
|
# Each name maps to CSS properties.
|
|
69
|
-
# Defaults:
|
|
69
|
+
# Defaults: accent (colored + bold), muted (subtle).
|
|
70
70
|
|
|
71
71
|
# inline:
|
|
72
|
-
#
|
|
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
|
-
│ │ ├──
|
|
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 #
|
|
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
|
-
|
|
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/
|
|
53
|
-
import { H1, P
|
|
52
|
+
// foundation/src/sections/Hero/index.jsx
|
|
53
|
+
import { H1, P } from '@uniweb/kit'
|
|
54
54
|
|
|
55
|
-
export function Hero({ content
|
|
56
|
-
const { title } = content
|
|
57
|
-
const { theme = 'light' } = params
|
|
55
|
+
export default function Hero({ content }) {
|
|
56
|
+
const { title, paragraphs = [] } = content || {}
|
|
58
57
|
|
|
59
58
|
return (
|
|
60
|
-
<
|
|
61
|
-
<H1 text={title} />
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|