uniweb 0.8.6 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/partials/agents.md +258 -7
- package/src/commands/add.js +149 -5
- package/src/index.js +1 -0
- 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/build": "0.8.
|
|
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`):
|
|
@@ -84,11 +114,11 @@ type: Hero
|
|
|
84
114
|
theme: dark
|
|
85
115
|
---
|
|
86
116
|
|
|
87
|
-
###
|
|
117
|
+
### V1.0.0 IS OUT ← pretitle (small label above the title)
|
|
88
118
|
|
|
89
|
-
#
|
|
119
|
+
# Build the system. ← title (the big headline)
|
|
90
120
|
|
|
91
|
-
##
|
|
121
|
+
## Not every page. ← subtitle
|
|
92
122
|
|
|
93
123
|
Description paragraph.
|
|
94
124
|
|
|
@@ -97,6 +127,8 @@ Description paragraph.
|
|
|
97
127
|

|
|
98
128
|
```
|
|
99
129
|
|
|
130
|
+
Content authors don't need to understand *why* `###` means pretitle — just that putting a smaller heading before the main heading creates a small label above it. Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing.
|
|
131
|
+
|
|
100
132
|
### Content Shape
|
|
101
133
|
|
|
102
134
|
The semantic parser extracts markdown into a flat, guaranteed structure. No null checks needed — empty strings/arrays if content is absent:
|
|
@@ -140,6 +172,28 @@ Enterprise-grade. ← items[1].paragraphs[0]
|
|
|
140
172
|
|
|
141
173
|
Each item has the same content shape as the top level — `title`, `paragraphs`, `icons`, `links`, `lists`, etc. are all available per item.
|
|
142
174
|
|
|
175
|
+
**Complete example — markdown and resulting content shape side by side:**
|
|
176
|
+
|
|
177
|
+
```markdown
|
|
178
|
+
### Eyebrow │ content.pretitle = "Eyebrow"
|
|
179
|
+
# Our Features │ content.title = "Our Features"
|
|
180
|
+
## Build better products │ content.subtitle = "Build better products"
|
|
181
|
+
│
|
|
182
|
+
We help teams ship faster. │ content.paragraphs[0] = "We help teams..."
|
|
183
|
+
│
|
|
184
|
+
[Get Started](/start) │ content.links[0] = { href: "/start", label: "Get Started" }
|
|
185
|
+
│
|
|
186
|
+
### Fast │ content.items[0].title = "Fast"
|
|
187
|
+
 │ 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
|
+
|
|
143
197
|
**Lists** contain bullet or ordered list items. Each list item is an object with the same content shape — not a plain string:
|
|
144
198
|
|
|
145
199
|
```markdown
|
|
@@ -210,6 +264,22 @@ Inset components must declare `inset: true` in their `meta.js`. They render at t
|
|
|
210
264
|
|
|
211
265
|
Standalone links (alone on a line) become buttons. Inline links stay as text links.
|
|
212
266
|
|
|
267
|
+
**Standalone links** — paragraphs that contain *only* links (no other text) are promoted to `content.links[]`. This works for single links and for multiple links sharing a paragraph:
|
|
268
|
+
|
|
269
|
+
```markdown
|
|
270
|
+
[Primary](/start) ← standalone → content.links[0]
|
|
271
|
+
|
|
272
|
+
[Secondary](/learn) ← standalone → content.links[1]
|
|
273
|
+
|
|
274
|
+
[One](/a) [Two](/b) ← links-only paragraph → content.links[0], content.links[1]
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Links mixed with non-link text stay as inline `<a>` tags within `content.paragraphs[]`:
|
|
278
|
+
|
|
279
|
+
```markdown
|
|
280
|
+
Check out [this](/a) and [that](/b). ← inline links in paragraph text, NOT in content.links[]
|
|
281
|
+
```
|
|
282
|
+
|
|
213
283
|
### Structured Data
|
|
214
284
|
|
|
215
285
|
Tagged code blocks pass structured data via `content.data`:
|
|
@@ -239,6 +309,46 @@ const data = useData()
|
|
|
239
309
|
|
|
240
310
|
Access: `content.data?.before`, `content.data?.after` → raw code strings.
|
|
241
311
|
|
|
312
|
+
### Lists as Navigation Menus
|
|
313
|
+
|
|
314
|
+
Markdown lists are ideal for navigation, menus, and grouped link structures. Each list item is a full content object with `paragraphs`, `links`, `icons`, and nested `lists`.
|
|
315
|
+
|
|
316
|
+
**Header nav — flat list with icons and links:**
|
|
317
|
+
|
|
318
|
+
```markdown
|
|
319
|
+
-  [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
|
+
|
|
242
352
|
### Section Backgrounds
|
|
243
353
|
|
|
244
354
|
Set `background` in frontmatter — the runtime renders it automatically. The string form auto-detects the type:
|
|
@@ -411,7 +521,12 @@ CCA separates theme from code. Components use **semantic CSS tokens** instead of
|
|
|
411
521
|
| `text-link` | Link color |
|
|
412
522
|
| `bg-primary` | Primary action background |
|
|
413
523
|
| `text-primary-foreground` | Text on primary background |
|
|
524
|
+
| `hover:bg-primary-hover` | Primary hover state |
|
|
525
|
+
| `border-primary-border` | Primary border (transparent by default) |
|
|
414
526
|
| `bg-secondary` | Secondary action background |
|
|
527
|
+
| `text-secondary-foreground` | Text on secondary background |
|
|
528
|
+
| `hover:bg-secondary-hover` | Secondary hover state |
|
|
529
|
+
| `border-secondary-border` | Secondary border |
|
|
415
530
|
| `text-success` / `bg-success-subtle` | Status: success |
|
|
416
531
|
| `text-error` / `bg-error-subtle` | Status: error |
|
|
417
532
|
| `text-warning` / `bg-warning-subtle` | Status: warning |
|
|
@@ -477,8 +592,8 @@ theme: dark
|
|
|
477
592
|
```yaml
|
|
478
593
|
theme:
|
|
479
594
|
mode: light
|
|
480
|
-
primary:
|
|
481
|
-
primary-hover:
|
|
595
|
+
primary: neutral-900 # Dark buttons in a light section
|
|
596
|
+
primary-hover: neutral-800
|
|
482
597
|
```
|
|
483
598
|
|
|
484
599
|
Any semantic token (`section`, `heading`, `body`, `primary`, `link`, etc.) can be overridden this way. The overrides are applied as inline CSS custom properties on the section wrapper — components don't need to know about them.
|
|
@@ -586,6 +701,14 @@ These compose with semantic tokens — they adapt per context because they refer
|
|
|
586
701
|
|
|
587
702
|
**The priority:** Design quality > portability > configurability. It's better to ship a foundation with beautiful, detailed design that's less configurable than to ship a generic one that looks flat. A foundation that looks great for one site is more valuable than one that looks mediocre for any site.
|
|
588
703
|
|
|
704
|
+
**Text tones beyond the 3-token set.** Source designs often have 4+ text tones (primary, secondary, tertiary, disabled). Uniweb provides 3 (`text-heading`, `text-body`, `text-subtle`). Don't collapse the extras — create them with `color-mix()` so they still adapt per context:
|
|
705
|
+
|
|
706
|
+
```css
|
|
707
|
+
/* foundation/src/styles.css */
|
|
708
|
+
.text-tertiary { color: color-mix(in oklch, var(--body), var(--subtle) 50%); }
|
|
709
|
+
.text-disabled { color: color-mix(in oklch, var(--subtle), transparent 40%); }
|
|
710
|
+
```
|
|
711
|
+
|
|
589
712
|
**When migrating from an existing design**, map every visual detail — not just the ones that have a semantic token. Shadow systems, border hierarchies, custom hover effects, accent tints: create CSS classes or Tailwind utilities in `styles.css` for anything the original has that tokens don't cover. Use palette shades directly (`var(--primary-300)`, `bg-neutral-200`) for fine-grained color control beyond the semantic tokens.
|
|
590
713
|
|
|
591
714
|
## Component Development
|
|
@@ -622,6 +745,88 @@ export default Hero
|
|
|
622
745
|
- `Component.className` — adds classes to the runtime's wrapper. Use for section-level spacing, borders, overflow. Set `py-[var(--section-padding-y)]` for consistent spacing from the theme variable, or override for specific sections (e.g., hero needs extra top padding). The component's own JSX handles inner layout only (`max-w-7xl mx-auto px-6`).
|
|
623
746
|
- `Component.as` — changes the wrapper element. Use `'nav'` for headers, `'footer'` for footers, `'div'` when `<section>` isn't semantically appropriate.
|
|
624
747
|
|
|
748
|
+
**Layout components** (Header, Footer) typically need `Component.className = 'p-0'` to suppress the runtime's default section padding, since they control their own padding. Also set `Component.as = 'header'` or `'footer'` for semantic HTML:
|
|
749
|
+
|
|
750
|
+
```jsx
|
|
751
|
+
function Header({ content, block }) { /* ... */ }
|
|
752
|
+
Header.className = 'p-0'
|
|
753
|
+
Header.as = 'header'
|
|
754
|
+
export default Header
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Content Patterns for Header and Footer
|
|
758
|
+
|
|
759
|
+
Header and Footer are the hardest components to content-model because they combine several content categories. Use different parts of the content shape for each role:
|
|
760
|
+
|
|
761
|
+
**Header** — title for logo, list for nav links, standalone link for CTA, tagged YAML for metadata:
|
|
762
|
+
|
|
763
|
+
````markdown
|
|
764
|
+
---
|
|
765
|
+
type: Header
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
# Acme Inc
|
|
769
|
+
|
|
770
|
+
-  [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
|
+
|
|
625
830
|
### meta.js Structure
|
|
626
831
|
|
|
627
832
|
```javascript
|
|
@@ -702,15 +907,29 @@ import { Section, Render } from '@uniweb/kit'
|
|
|
702
907
|
|
|
703
908
|
**Other primitives** (`@uniweb/kit`): `Link`, `Image`, `Icon`, `Media`, `Asset`, `SafeHtml`, `SocialIcon`, `FileLogo`, `cn()`
|
|
704
909
|
|
|
910
|
+
`Link` props: `to` (or `href`), `target`, `reload`, `download`, `className`, `children`:
|
|
911
|
+
|
|
912
|
+
```jsx
|
|
913
|
+
<Link to="/about">About</Link> // SPA navigation via React Router
|
|
914
|
+
<Link to="page:about">About</Link> // Resolves page ID to route
|
|
915
|
+
<Link reload href={localeUrl}>ES</Link> // Full page reload, prepends basePath
|
|
916
|
+
// External URLs auto-get target="_blank" and rel="noopener noreferrer"
|
|
917
|
+
```
|
|
918
|
+
|
|
705
919
|
**Other styled** (`@uniweb/kit`): `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
706
920
|
|
|
707
921
|
**Hooks:**
|
|
708
922
|
- `useScrolled(threshold)` → boolean for scroll-based header styling
|
|
709
923
|
- `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
|
|
710
924
|
- `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
|
|
711
|
-
- `useActiveRoute()` → `{ route, isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
925
|
+
- `useActiveRoute()` → `{ route, rootSegment, isActive(page), isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
712
926
|
- `useGridLayout(columns, { gap })` → responsive grid class string
|
|
713
927
|
- `useTheme(name)` → standardized theme classes
|
|
928
|
+
- `useAppearance()` → `{ scheme, toggle, canToggle, setScheme, schemes }` — light/dark mode control with localStorage persistence
|
|
929
|
+
- `useRouting()` → `{ useLocation, useParams, useNavigate, Link, isRoutingAvailable }` — SSG-safe routing access (returns no-op fallbacks during prerender)
|
|
930
|
+
- `useWebsite()` → `{ website, localize, makeHref, getLanguage, getLanguages, getRoutingComponents }` — primary runtime hook
|
|
931
|
+
- `useThemeData()` → Theme instance for programmatic color access (`getColor(name, shade)`, `getPalette(name)`)
|
|
932
|
+
- `useColorContext(block)` → `'light' | 'medium' | 'dark'` — current section's color context
|
|
714
933
|
|
|
715
934
|
**Utilities:** `cn()` (Tailwind class merge), `filterSocialLinks(links)`, `getSocialPlatform(url)`
|
|
716
935
|
|
|
@@ -748,6 +967,28 @@ website.hasMultipleLocales()
|
|
|
748
967
|
website.getLocales() // [{ code, label, isDefault }]
|
|
749
968
|
website.getActiveLocale() // 'en'
|
|
750
969
|
website.getLocaleUrl('es')
|
|
970
|
+
|
|
971
|
+
// Core properties
|
|
972
|
+
website.name // Site name from site.yml
|
|
973
|
+
website.basePath // Deployment base path (e.g., '/docs/')
|
|
974
|
+
|
|
975
|
+
// Route detection
|
|
976
|
+
const { isActive, isActiveOrAncestor } = useActiveRoute()
|
|
977
|
+
isActive(page) // Exact match
|
|
978
|
+
isActiveOrAncestor(page) // Ancestor match (for parent highlighting in nav)
|
|
979
|
+
|
|
980
|
+
// Appearance (light/dark mode)
|
|
981
|
+
const { scheme, toggle, canToggle } = useAppearance()
|
|
982
|
+
|
|
983
|
+
// Page properties
|
|
984
|
+
page.title // Page title
|
|
985
|
+
page.label // Short label for nav (falls back to title)
|
|
986
|
+
page.route // Route path
|
|
987
|
+
page.isHidden() // Hidden from navigation
|
|
988
|
+
page.showInHeader() // Visible in header nav
|
|
989
|
+
page.showInFooter() // Visible in footer nav
|
|
990
|
+
page.hasChildren() // Has child pages
|
|
991
|
+
page.children // Array of child Page objects
|
|
751
992
|
```
|
|
752
993
|
|
|
753
994
|
### Insets and the Visual Component
|
|
@@ -776,6 +1017,8 @@ function SplitContent({ content, block }) {
|
|
|
776
1017
|
- `block.getInset(refId)` — lookup by refId (used by sequential renderers)
|
|
777
1018
|
- `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
|
|
778
1019
|
|
|
1020
|
+
**SSG and hooks:** Inset components that use React hooks (useState, useEffect) will trigger prerender warnings during `pnpm build`. This is expected — the SSG pipeline cannot render hooks due to dual React instances in the build. The warnings are informational; the page renders correctly client-side. If you see `"Skipped SSG for /..."` or `"Invalid hook call"`, this is the cause.
|
|
1021
|
+
|
|
779
1022
|
Inset components declare `inset: true` in meta.js. Use `hidden: true` for inset-only components:
|
|
780
1023
|
|
|
781
1024
|
```js
|
|
@@ -969,7 +1212,15 @@ Semantic color tokens (`text-heading`, `bg-section`, `bg-primary`, etc.) come fr
|
|
|
969
1212
|
|
|
970
1213
|
**Styles not applying** — Verify `@source` in `styles.css` includes your component paths. Check custom colors match `@theme` definitions.
|
|
971
1214
|
|
|
972
|
-
**Prerender warnings about hooks/useState** —
|
|
1215
|
+
**Prerender warnings about hooks/useState** — Components with React hooks (useState/useEffect) — especially insets — will show SSG warnings during `pnpm build`. This is expected and harmless; see the note in the Insets section above.
|
|
1216
|
+
|
|
1217
|
+
**Content not appearing as expected?** In dev mode, open the browser console and inspect the parsed content shape your component receives:
|
|
1218
|
+
|
|
1219
|
+
```js
|
|
1220
|
+
globalThis.uniweb.activeWebsite.activePage.bodyBlocks[0].parsedContent
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
Compare with the Content Shape table above to identify mapping issues (e.g., headings becoming items instead of title, links inline in paragraphs instead of in `links[]`).
|
|
973
1224
|
|
|
974
1225
|
## Further Documentation
|
|
975
1226
|
|
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
|
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
|
|