uniweb 0.5.13 → 0.5.14
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/README.md +2 -2
- package/package.json +3 -3
- package/partials/agents-md.hbs +192 -805
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ my-project/
|
|
|
64
64
|
│ ├── pages/ # File-based routing
|
|
65
65
|
│ │ └── home/
|
|
66
66
|
│ │ ├── page.yml # Page metadata
|
|
67
|
-
│ │ └──
|
|
67
|
+
│ │ └── hero.md # Section content
|
|
68
68
|
│ ├── locales/ # i18n (hash-based translations)
|
|
69
69
|
│ ├── main.js # Entry point (~6 lines)
|
|
70
70
|
│ ├── vite.config.js # 3-line config
|
|
@@ -169,7 +169,7 @@ The `meta.js` file defines what content and parameters a component accepts. The
|
|
|
169
169
|
|
|
170
170
|
### Your First Content Change
|
|
171
171
|
|
|
172
|
-
Open `site/pages/home/
|
|
172
|
+
Open `site/pages/home/hero.md` and edit the headline:
|
|
173
173
|
|
|
174
174
|
```markdown
|
|
175
175
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"js-yaml": "^4.1.0",
|
|
41
41
|
"prompts": "^2.4.2",
|
|
42
42
|
"tar": "^7.0.0",
|
|
43
|
-
"@uniweb/build": "0.4.
|
|
43
|
+
"@uniweb/build": "0.4.9",
|
|
44
|
+
"@uniweb/runtime": "0.5.11",
|
|
44
45
|
"@uniweb/core": "0.3.10",
|
|
45
|
-
"@uniweb/runtime": "0.5.10",
|
|
46
46
|
"@uniweb/kit": "0.4.11"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/partials/agents-md.hbs
CHANGED
|
@@ -1,983 +1,370 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Guidance for AI agents working with this Uniweb project. For deeper reference, see the [developer guides](./guides/developers/) and [content author guides](./guides/authors/).
|
|
4
4
|
|
|
5
5
|
## Project Structure
|
|
6
6
|
|
|
7
|
-
Uniweb projects have two structures. A single project can be converted to multi-site:
|
|
8
|
-
|
|
9
|
-
**Single (one foundation, one site):**
|
|
10
7
|
```
|
|
11
8
|
project/
|
|
12
|
-
├── foundation/ #
|
|
9
|
+
├── foundation/ # React component library
|
|
13
10
|
├── site/ # Content (markdown pages)
|
|
14
11
|
└── pnpm-workspace.yaml
|
|
15
12
|
```
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
project/
|
|
20
|
-
├── foundations/ # Purpose-built component libraries
|
|
21
|
-
│ └── marketing/
|
|
22
|
-
├── sites/ # Content sites
|
|
23
|
-
│ └── my-org/
|
|
24
|
-
└── pnpm-workspace.yaml
|
|
25
|
-
```
|
|
14
|
+
Multi-site variant uses `foundations/` and `sites/` (plural) folders.
|
|
26
15
|
|
|
27
|
-
- **Foundation**: React components.
|
|
28
|
-
- **Site**: Markdown content. Each section references
|
|
16
|
+
- **Foundation**: React components. Those with `meta.js` are *exposed* — selectable by content authors via `type:` in frontmatter.
|
|
17
|
+
- **Site**: Markdown content + configuration. Each section file references an exposed component.
|
|
29
18
|
|
|
30
19
|
## Commands
|
|
31
20
|
|
|
32
21
|
```bash
|
|
33
22
|
pnpm install # Install dependencies
|
|
34
|
-
pnpm dev # Start dev server
|
|
23
|
+
pnpm dev # Start dev server
|
|
35
24
|
pnpm build # Build for production
|
|
36
25
|
```
|
|
37
26
|
|
|
38
|
-
Multi-site also supports:
|
|
39
|
-
```bash
|
|
40
|
-
pnpm dev:all # Start all sites in parallel
|
|
41
|
-
pnpm build:all # Build all sites
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Discovering Components
|
|
45
|
-
|
|
46
|
-
Exposed components live in `foundation/src/components/` (or `foundations/*/src/components/`).
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
# List exposed components (those with meta.js)
|
|
50
|
-
ls foundation/src/components/*/meta.js
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
**Understanding a component:**
|
|
54
|
-
|
|
55
|
-
1. **`meta.js`** - Defines the component's interface:
|
|
56
|
-
- `title`, `description` - What the component does
|
|
57
|
-
- `content` - What content it expects (title, paragraphs, links, items, etc.)
|
|
58
|
-
- `params` - Configurable parameters with types and defaults
|
|
59
|
-
- `presets` - Named combinations of param values
|
|
60
|
-
- `hidden: true` - Component exists but isn't selectable from frontmatter
|
|
61
|
-
|
|
62
|
-
2. **`index.jsx`** - The React implementation
|
|
63
|
-
|
|
64
|
-
3. **Existing content** - See how the component is used in `site/pages/`
|
|
65
|
-
|
|
66
|
-
**Note:** Components without `meta.js` are internal helpers used by other components.
|
|
67
|
-
|
|
68
27
|
## Content Authoring
|
|
69
28
|
|
|
70
29
|
### Section Format
|
|
71
30
|
|
|
72
|
-
Each
|
|
31
|
+
Each `.md` file is a section. Frontmatter on top, content below:
|
|
73
32
|
|
|
74
33
|
```markdown
|
|
75
34
|
---
|
|
76
|
-
type:
|
|
77
|
-
theme: dark
|
|
35
|
+
type: Hero
|
|
36
|
+
theme: dark
|
|
78
37
|
---
|
|
79
38
|
|
|
80
|
-
### Eyebrow Text
|
|
39
|
+
### Eyebrow Text ← pretitle (heading before a more important one)
|
|
81
40
|
|
|
82
|
-
# Main Headline
|
|
41
|
+
# Main Headline ← title
|
|
83
42
|
|
|
84
|
-
## Subtitle
|
|
43
|
+
## Subtitle ← subtitle
|
|
85
44
|
|
|
86
45
|
Description paragraph.
|
|
87
46
|
|
|
88
|
-
[Call to Action](
|
|
47
|
+
[Call to Action](/link)
|
|
89
48
|
|
|
90
|
-

|
|
49
|
+

|
|
91
50
|
```
|
|
92
51
|
|
|
93
|
-
### Content
|
|
52
|
+
### Content Shape
|
|
94
53
|
|
|
95
|
-
The semantic parser extracts markdown into a
|
|
54
|
+
The semantic parser extracts markdown into a flat, guaranteed structure. No null checks needed — empty strings/arrays if content is absent:
|
|
96
55
|
|
|
97
56
|
```js
|
|
98
|
-
{
|
|
99
|
-
// Headers (from headings)
|
|
57
|
+
content = {
|
|
100
58
|
title: '', // Main heading
|
|
101
59
|
pretitle: '', // Heading before main title (auto-detected)
|
|
102
|
-
subtitle: '', // Heading after
|
|
103
|
-
subtitle2: '', // Third heading
|
|
104
|
-
|
|
105
|
-
// Body content
|
|
60
|
+
subtitle: '', // Heading after title
|
|
61
|
+
subtitle2: '', // Third-level heading
|
|
106
62
|
paragraphs: [], // Text blocks
|
|
107
|
-
links: [], //
|
|
108
|
-
imgs: [], //
|
|
109
|
-
icons: [], //
|
|
110
|
-
videos: [], //
|
|
63
|
+
links: [], // { href, label, role } — standalone links become buttons
|
|
64
|
+
imgs: [], // { src, alt, role }
|
|
65
|
+
icons: [], // { library, name, role }
|
|
66
|
+
videos: [], // Video embeds
|
|
111
67
|
lists: [], // Bullet/ordered lists
|
|
112
68
|
quotes: [], // Blockquotes
|
|
113
|
-
data: {}, //
|
|
114
|
-
headings: [], // Overflow headings after
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
items: [], // Created by headings after content
|
|
118
|
-
|
|
119
|
-
// Document-order rendering
|
|
120
|
-
sequence: [], // All elements in original order
|
|
69
|
+
data: {}, // From tagged code blocks (```yaml:tagname)
|
|
70
|
+
headings: [], // Overflow headings after subtitle2
|
|
71
|
+
items: [], // Each has the same flat structure — from headings after body content
|
|
72
|
+
sequence: [], // All elements in document order
|
|
121
73
|
}
|
|
122
74
|
```
|
|
123
75
|
|
|
124
|
-
**
|
|
125
|
-
- `#` in markdown doesn't always become `<h1>` — the component decides
|
|
126
|
-
- A pretitle is auto-detected when a heading precedes a more important one (H3→H1, H2→H1)
|
|
127
|
-
- Items are created when any heading appears after body content
|
|
76
|
+
**Items** are repeating content groups (cards, features, FAQ entries). Created when a heading appears after body content:
|
|
128
77
|
|
|
129
|
-
|
|
78
|
+
```markdown
|
|
79
|
+
# Our Features ← title
|
|
130
80
|
|
|
131
|
-
|
|
81
|
+
We built this for you. ← paragraph
|
|
132
82
|
|
|
133
|
-
|
|
134
|
-
[
|
|
135
|
-
{role=banner width=1200 loading=lazy}
|
|
136
|
-
```
|
|
83
|
+
### Fast ← items[0].title
|
|
84
|
+
Lightning quick. ← items[0].paragraphs[0]
|
|
137
85
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
[Learn more](/about) <!-- Standard link -->
|
|
141
|
-
[Get Started](button:/signup) <!-- Button (prefix) -->
|
|
142
|
-
[Get Started](/signup){.button variant=primary} <!-- Button (class) -->
|
|
143
|
-
[Download](./report.pdf){download} <!-- Download link -->
|
|
86
|
+
### Secure ← items[1].title
|
|
87
|
+
Enterprise-grade. ← items[1].paragraphs[0]
|
|
144
88
|
```
|
|
145
89
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
-
|
|
149
|
-
|
|
90
|
+
### Icons
|
|
91
|
+
|
|
92
|
+
Use image syntax with library prefix: ``. Supported libraries: `lu` (Lucide), `hi2` (Heroicons), `fi` (Feather), `pi` (Phosphor), `tb` (Tabler), `bs` (Bootstrap), `md` (Material), `fa6` (Font Awesome 6), and others. Browse at [react-icons.github.io/react-icons](https://react-icons.github.io/react-icons/).
|
|
93
|
+
|
|
94
|
+
Custom SVGs: `{role=icon}`
|
|
95
|
+
|
|
96
|
+
### Links and Media Attributes
|
|
150
97
|
|
|
151
|
-
**Media classification by role:**
|
|
152
98
|
```markdown
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{role=video} <!-- videos array -->
|
|
99
|
+
[text](url){target=_blank} <!-- Open in new tab -->
|
|
100
|
+
[text](./file.pdf){download} <!-- Download -->
|
|
101
|
+
{role=banner} <!-- Role determines array: imgs, icons, or videos -->
|
|
157
102
|
```
|
|
158
103
|
|
|
159
|
-
|
|
104
|
+
Standalone links (alone on a line) become buttons. Inline links stay as text links.
|
|
160
105
|
|
|
161
106
|
### Structured Data
|
|
162
107
|
|
|
163
|
-
Tagged code blocks pass structured data
|
|
108
|
+
Tagged code blocks pass structured data via `content.data`:
|
|
164
109
|
|
|
165
110
|
````markdown
|
|
166
111
|
```yaml:form
|
|
167
112
|
fields:
|
|
168
113
|
- name: email
|
|
169
114
|
type: email
|
|
170
|
-
required: true
|
|
171
115
|
submitLabel: Send
|
|
172
116
|
```
|
|
173
117
|
````
|
|
174
118
|
|
|
175
|
-
Access
|
|
119
|
+
Access: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
|
|
120
|
+
|
|
121
|
+
### Section Backgrounds
|
|
176
122
|
|
|
177
|
-
|
|
123
|
+
Set `background` in frontmatter — the runtime renders it automatically:
|
|
178
124
|
|
|
179
|
-
|
|
125
|
+
```yaml
|
|
126
|
+
---
|
|
127
|
+
type: Hero
|
|
128
|
+
theme: dark
|
|
129
|
+
background: /images/hero.jpg # Simple: URL (image or video auto-detected)
|
|
130
|
+
---
|
|
131
|
+
```
|
|
180
132
|
|
|
181
|
-
|
|
133
|
+
Full syntax supports `image`, `video`, `gradient`, `color` modes plus overlays:
|
|
182
134
|
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
|
|
135
|
+
```yaml
|
|
136
|
+
background:
|
|
137
|
+
image: { src: /img.jpg, position: center top }
|
|
138
|
+
overlay: { enabled: true, type: dark, opacity: 0.5 }
|
|
186
139
|
```
|
|
187
140
|
|
|
188
|
-
|
|
141
|
+
Components that render their own background declare `background: 'self'` in `meta.js`.
|
|
189
142
|
|
|
190
143
|
### Page Organization
|
|
191
144
|
|
|
192
145
|
```
|
|
193
|
-
site/pages/
|
|
194
|
-
├── @header/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
146
|
+
site/pages/
|
|
147
|
+
├── @header/ # Rendered on every page
|
|
148
|
+
│ └── header.md # type: Header
|
|
149
|
+
├── @footer/ # Rendered on every page
|
|
150
|
+
│ └── footer.md # type: Footer
|
|
151
|
+
└── home/
|
|
152
|
+
├── page.yml # title, description, order
|
|
153
|
+
├── hero.md # Single section — no prefix needed
|
|
154
|
+
└── (or for multi-section pages:)
|
|
155
|
+
├── 1-hero.md # Numeric prefix sets order
|
|
156
|
+
├── 2-features.md
|
|
157
|
+
└── 3-cta.md
|
|
199
158
|
```
|
|
200
159
|
|
|
201
|
-
|
|
160
|
+
Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`.
|
|
161
|
+
|
|
162
|
+
**page.yml:**
|
|
202
163
|
```yaml
|
|
203
164
|
title: About Us
|
|
204
165
|
description: Learn about our company
|
|
205
|
-
order: 2
|
|
206
|
-
|
|
207
|
-
# Section ordering (optional)
|
|
208
|
-
sections:
|
|
209
|
-
- hero
|
|
210
|
-
- features
|
|
211
|
-
- pricing
|
|
166
|
+
order: 2 # Navigation sort order
|
|
167
|
+
index: getting-started # Which child page is the index
|
|
212
168
|
```
|
|
213
169
|
|
|
214
|
-
**
|
|
215
|
-
- Default: `.md` files discovered and sorted by numeric prefix (`1-`, `2-`, `3-`)
|
|
216
|
-
- Decimals to insert: `2.5-` sorts between `2-` and `3-` (no renumbering needed)
|
|
217
|
-
- Explicit: Use `sections: [hero, features]` — no prefixes needed, easy reordering
|
|
218
|
-
- Empty: `sections: []` — pure route with no content
|
|
219
|
-
- Wildcard: `sections: *` — explicitly use default behavior
|
|
220
|
-
|
|
221
|
-
**Section hierarchy:** For subsections, use explicit nesting in `page.yml`:
|
|
170
|
+
**site.yml:**
|
|
222
171
|
```yaml
|
|
223
|
-
|
|
224
|
-
- hero
|
|
225
|
-
- features:
|
|
226
|
-
- logocloud
|
|
227
|
-
- stats
|
|
228
|
-
```
|
|
229
|
-
This is clearer than prefix notation (`1,1-`, `1,2-`) and easier to restructure.
|
|
230
|
-
|
|
231
|
-
**Page ordering:** Parent controls which page is the index for its route:
|
|
232
|
-
```yaml
|
|
233
|
-
# In site.yml (top-level pages)
|
|
234
|
-
pages: [home, about, docs] # First is homepage at /
|
|
235
|
-
index: home # Or just name the homepage
|
|
236
|
-
|
|
237
|
-
# In page.yml (child pages)
|
|
238
|
-
pages: [getting-started, installation] # First is index for this route
|
|
239
|
-
index: getting-started # Or just name the index
|
|
240
|
-
```
|
|
241
|
-
- `pages: [a, b, c]` — First gets parent route (becomes index)
|
|
242
|
-
- `index: name` — Just set index, auto-discover rest
|
|
243
|
-
- Omit both — Sort by `order` prop, lowest becomes index
|
|
244
|
-
|
|
245
|
-
## Component Development
|
|
246
|
-
|
|
247
|
-
### Props Interface
|
|
248
|
-
|
|
249
|
-
The runtime provides **guarantees** for component props. No defensive null checks needed:
|
|
250
|
-
|
|
251
|
-
```jsx
|
|
252
|
-
function MyComponent({ content, params, block }) {
|
|
253
|
-
// Runtime guarantees these always exist (flat API):
|
|
254
|
-
const { title, pretitle, subtitle, paragraphs, links, imgs, items } = content
|
|
255
|
-
|
|
256
|
-
// params already has defaults from meta.js merged in:
|
|
257
|
-
const { theme, layout } = params
|
|
258
|
-
|
|
259
|
-
// Access website and page via block or useWebsite() hook:
|
|
260
|
-
const { website } = useWebsite() // or use block.website, block.page
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**Guaranteed content shape:**
|
|
265
|
-
```js
|
|
266
|
-
content = {
|
|
267
|
-
title: '',
|
|
268
|
-
pretitle: '',
|
|
269
|
-
subtitle: '',
|
|
270
|
-
subtitle2: '',
|
|
271
|
-
paragraphs: [],
|
|
272
|
-
links: [],
|
|
273
|
-
imgs: [],
|
|
274
|
-
icons: [],
|
|
275
|
-
videos: [],
|
|
276
|
-
lists: [],
|
|
277
|
-
quotes: [],
|
|
278
|
-
data: {},
|
|
279
|
-
headings: [],
|
|
280
|
-
items: [], // Each item has the same flat structure
|
|
281
|
-
sequence: [], // For document-order rendering
|
|
282
|
-
}
|
|
172
|
+
index: home # Which page folder is the homepage
|
|
283
173
|
```
|
|
284
174
|
|
|
285
|
-
|
|
286
|
-
- Documentation consistency (defaults defined once)
|
|
287
|
-
- Cleaner, more readable component code
|
|
288
|
-
- Easier maintenance (change defaults without touching JSX)
|
|
175
|
+
Use `index:` rather than `pages: [...]` — listing pages explicitly hides auto-discovered ones.
|
|
289
176
|
|
|
290
|
-
|
|
177
|
+
## Semantic Theming
|
|
291
178
|
|
|
292
|
-
|
|
179
|
+
CCA separates theme from code. Components use **semantic CSS tokens** instead of hardcoded colors. The runtime applies a context class (`context-light`, `context-medium`, `context-dark`) to each section based on `theme:` frontmatter.
|
|
293
180
|
|
|
294
|
-
**Primitives** (`@uniweb/kit`):
|
|
295
181
|
```jsx
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
H1, H2, H3, P, Span, Text,
|
|
299
|
-
// Navigation & Media
|
|
300
|
-
Link, Image, Icon, Media, Asset,
|
|
301
|
-
// Utilities
|
|
302
|
-
cn, FileLogo, MediaIcon, SocialIcon,
|
|
303
|
-
// Hooks
|
|
304
|
-
useScrolled, useMobileMenu, useAccordion, useActiveRoute,
|
|
305
|
-
useGridLayout, useTheme,
|
|
306
|
-
} from '@uniweb/kit'
|
|
307
|
-
```
|
|
182
|
+
// ❌ Hardcoded — breaks in dark context, locked to one palette
|
|
183
|
+
<h2 className="text-slate-900">...</h2>
|
|
308
184
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
import {
|
|
312
|
-
// Layout
|
|
313
|
-
SidebarLayout,
|
|
314
|
-
// Content rendering
|
|
315
|
-
Section, Render,
|
|
316
|
-
Code, Alert, Table, Details, Divider,
|
|
317
|
-
// UI
|
|
318
|
-
Disclaimer,
|
|
319
|
-
Media, // styled version with play button facade
|
|
320
|
-
Asset, // styled version with card preview
|
|
321
|
-
} from '@uniweb/kit/styled'
|
|
185
|
+
// ✅ Semantic — adapts to any context and brand automatically
|
|
186
|
+
<h2 className="text-heading">...</h2>
|
|
322
187
|
```
|
|
323
188
|
|
|
324
|
-
|
|
189
|
+
**Core tokens** (available as Tailwind classes):
|
|
325
190
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
191
|
+
| Token | Purpose |
|
|
192
|
+
|-------|---------|
|
|
193
|
+
| `text-heading` | Headings |
|
|
194
|
+
| `text-body` | Body text |
|
|
195
|
+
| `text-muted` | Secondary text |
|
|
196
|
+
| `bg-surface` | Section background |
|
|
197
|
+
| `bg-surface-subtle` | Slightly offset surface |
|
|
198
|
+
| `border-edge` | Borders |
|
|
199
|
+
| `border-edge-muted` | Subtle borders |
|
|
200
|
+
| `text-link` | Link color |
|
|
201
|
+
| `text-on-primary` | Text on primary-colored backgrounds |
|
|
202
|
+
| `bg-primary` | Brand primary color |
|
|
332
203
|
|
|
333
|
-
**
|
|
334
|
-
- `H1`-`H6`, `P`, `Span`, `Text` - Typography (handles arrays, filters empty)
|
|
335
|
-
- `Link` - Smart routing
|
|
336
|
-
- `Image` - Optimized images with size presets
|
|
337
|
-
- `Icon` - SVG icon loader
|
|
338
|
-
- `Media` - Video player (YouTube, Vimeo, local) - plain version
|
|
339
|
-
- `Asset` - File download link - plain version
|
|
340
|
-
- `FileLogo`, `MediaIcon` - File type icons
|
|
341
|
-
- `SocialIcon` - SVG icons for 15+ social platforms
|
|
342
|
-
|
|
343
|
-
**Styled components** (from `@uniweb/kit/styled`):
|
|
344
|
-
- `SidebarLayout` - Layout with left/right sidebars (see Custom Layouts below)
|
|
345
|
-
- `Section` - Rich content section with width/padding/columns
|
|
346
|
-
- `Render` - ProseMirror content renderer
|
|
347
|
-
- `Code`, `Alert`, `Table`, `Details`, `Divider` - Content block renderers
|
|
348
|
-
- `Disclaimer` - Modal dialog for legal text
|
|
349
|
-
- `Media` - Video with styled play button facade
|
|
350
|
-
- `Asset` - File card with preview thumbnail and hover overlay
|
|
351
|
-
|
|
352
|
-
**Hooks:**
|
|
353
|
-
- `useScrolled(threshold)` - Scroll detection (returns boolean)
|
|
354
|
-
- `useMobileMenu()` - Mobile menu state with auto-close on route change
|
|
355
|
-
- `useAccordion({ multiple, defaultOpen })` - Expand/collapse state
|
|
356
|
-
- `useActiveRoute()` - SSG-safe route detection for navigation highlighting
|
|
357
|
-
- `useGridLayout(columns, { gap })` - Responsive grid classes
|
|
358
|
-
- `useTheme(name, overrides)` - Standardized theme classes
|
|
359
|
-
|
|
360
|
-
**Utilities:**
|
|
361
|
-
- `cn()` - Tailwind class merging
|
|
362
|
-
- `getSocialPlatform(url)` - Detect platform from URL
|
|
363
|
-
- `isSocialLink(url)` - Check if URL is a social platform
|
|
364
|
-
- `filterSocialLinks(links)` - Filter array to social links only
|
|
365
|
-
- `getGridClasses()` / `getThemeClasses()` - Non-hook versions
|
|
366
|
-
|
|
367
|
-
### Kit Hook Examples
|
|
368
|
-
|
|
369
|
-
**Scroll-based header styling:**
|
|
370
|
-
```jsx
|
|
371
|
-
import { useScrolled, cn } from '@uniweb/kit'
|
|
372
|
-
|
|
373
|
-
function Header() {
|
|
374
|
-
const scrolled = useScrolled(20) // threshold in pixels
|
|
375
|
-
|
|
376
|
-
return (
|
|
377
|
-
<header className={cn(
|
|
378
|
-
'fixed top-0 transition-all',
|
|
379
|
-
scrolled ? 'bg-white shadow-lg' : 'bg-transparent'
|
|
380
|
-
)}>
|
|
381
|
-
...
|
|
382
|
-
</header>
|
|
383
|
-
)
|
|
384
|
-
}
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
**Mobile menu with auto-close:**
|
|
388
|
-
```jsx
|
|
389
|
-
import { useMobileMenu } from '@uniweb/kit'
|
|
390
|
-
|
|
391
|
-
function Header() {
|
|
392
|
-
const { isOpen, toggle, close } = useMobileMenu()
|
|
393
|
-
|
|
394
|
-
// Menu automatically closes on route change
|
|
395
|
-
return (
|
|
396
|
-
<>
|
|
397
|
-
<button onClick={toggle}>Menu</button>
|
|
398
|
-
{isOpen && (
|
|
399
|
-
<nav>
|
|
400
|
-
<Link href="/about" onClick={close}>About</Link>
|
|
401
|
-
</nav>
|
|
402
|
-
)}
|
|
403
|
-
</>
|
|
404
|
-
)
|
|
405
|
-
}
|
|
406
|
-
```
|
|
204
|
+
**Content authors control context** in frontmatter:
|
|
407
205
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const { isActiveOrAncestor } = useActiveRoute()
|
|
414
|
-
|
|
415
|
-
return pages.map(page => (
|
|
416
|
-
<Link
|
|
417
|
-
key={page.route}
|
|
418
|
-
href={page.navigableRoute}
|
|
419
|
-
className={cn(
|
|
420
|
-
'nav-link',
|
|
421
|
-
isActiveOrAncestor(page) && 'text-primary font-bold'
|
|
422
|
-
)}
|
|
423
|
-
>
|
|
424
|
-
{page.label}
|
|
425
|
-
</Link>
|
|
426
|
-
))
|
|
427
|
-
}
|
|
206
|
+
```markdown
|
|
207
|
+
---
|
|
208
|
+
type: Testimonial
|
|
209
|
+
theme: dark ← sets context-dark, all tokens resolve to dark values
|
|
210
|
+
---
|
|
428
211
|
```
|
|
429
212
|
|
|
430
|
-
**
|
|
431
|
-
```jsx
|
|
432
|
-
import { useAccordion } from '@uniweb/kit'
|
|
213
|
+
**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.
|
|
433
214
|
|
|
434
|
-
|
|
435
|
-
const { isOpen, toggle } = useAccordion({ expandFirst: true })
|
|
215
|
+
**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. See the [Thinking in Contexts](./guides/developers/thinking-in-contexts.md) guide.
|
|
436
216
|
|
|
437
|
-
|
|
438
|
-
<div key={i}>
|
|
439
|
-
<button onClick={() => toggle(i)}>{item.question}</button>
|
|
440
|
-
{isOpen(i) && <p>{item.answer}</p>}
|
|
441
|
-
</div>
|
|
442
|
-
))
|
|
443
|
-
}
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
**Responsive grid:**
|
|
447
|
-
```jsx
|
|
448
|
-
import { useGridLayout } from '@uniweb/kit'
|
|
449
|
-
|
|
450
|
-
function Features({ items, params }) {
|
|
451
|
-
const gridClass = useGridLayout(params.columns, { gap: 8 })
|
|
452
|
-
// Returns: "grid gap-8 sm:grid-cols-2 lg:grid-cols-3" (for columns=3)
|
|
217
|
+
## Component Development
|
|
453
218
|
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
```
|
|
219
|
+
### Props Interface
|
|
457
220
|
|
|
458
|
-
**Social icons:**
|
|
459
221
|
```jsx
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
return (
|
|
466
|
-
<div className="flex gap-4">
|
|
467
|
-
{socialLinks.map((link, i) => (
|
|
468
|
-
<Link key={i} href={link.href}>
|
|
469
|
-
<SocialIcon url={link.href} className="w-5 h-5" />
|
|
470
|
-
</Link>
|
|
471
|
-
))}
|
|
472
|
-
</div>
|
|
473
|
-
)
|
|
222
|
+
function MyComponent({ content, params, block }) {
|
|
223
|
+
const { title, paragraphs, links, items } = content // Guaranteed shape
|
|
224
|
+
const { theme, columns } = params // Defaults from meta.js
|
|
225
|
+
const { website } = useWebsite() // Or block.website
|
|
474
226
|
}
|
|
475
227
|
```
|
|
476
228
|
|
|
477
|
-
|
|
478
|
-
- Eliminates boilerplate (scroll handlers, route effects, state management)
|
|
479
|
-
- Consistent behavior across components
|
|
480
|
-
- SSG-safe (works during static generation)
|
|
481
|
-
- Auto-close menus on navigation (better UX)
|
|
482
|
-
|
|
483
|
-
### Custom Layouts
|
|
484
|
-
|
|
485
|
-
Foundations can provide a custom site Layout component via `src/exports.js`. The kit includes `SidebarLayout` for common sidebar layouts:
|
|
486
|
-
|
|
487
|
-
```jsx
|
|
488
|
-
// foundation/src/exports.js
|
|
489
|
-
import { SidebarLayout } from '@uniweb/kit/styled'
|
|
229
|
+
### meta.js Structure
|
|
490
230
|
|
|
231
|
+
```javascript
|
|
491
232
|
export default {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
- Optional left and/or right sidebars (show whichever panels have content)
|
|
498
|
-
- Desktop: sidebars appear inline at configurable breakpoints
|
|
499
|
-
- Mobile: left panel in slide-out drawer with FAB, right panel hidden
|
|
500
|
-
- Sticky header and sidebar options
|
|
501
|
-
- Auto-close drawer on route change
|
|
502
|
-
|
|
503
|
-
**Configuration props:**
|
|
504
|
-
```jsx
|
|
505
|
-
import { SidebarLayout } from '@uniweb/kit/styled'
|
|
506
|
-
|
|
507
|
-
function CustomLayout(props) {
|
|
508
|
-
return (
|
|
509
|
-
<SidebarLayout
|
|
510
|
-
{...props}
|
|
511
|
-
leftWidth="w-64" // Left sidebar width
|
|
512
|
-
rightWidth="w-64" // Right sidebar width
|
|
513
|
-
drawerWidth="w-72" // Mobile drawer width
|
|
514
|
-
leftBreakpoint="md" // Show left at md+
|
|
515
|
-
rightBreakpoint="xl" // Show right at xl+
|
|
516
|
-
stickyHeader={true} // Sticky header
|
|
517
|
-
stickySidebar={true} // Sticky sidebars
|
|
518
|
-
maxWidth="max-w-7xl" // Content area max width
|
|
519
|
-
/>
|
|
520
|
-
)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
export default { Layout: CustomLayout }
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
**Layout receives pre-rendered areas:**
|
|
527
|
-
- `header` - From `@header/` sections
|
|
528
|
-
- `body` - Page content sections
|
|
529
|
-
- `footer` - From `@footer/` sections
|
|
530
|
-
- `left` / `leftPanel` - From `@left/` sections (navigation)
|
|
531
|
-
- `right` / `rightPanel` - From `@right/` sections (TOC, contextual info)
|
|
532
|
-
|
|
533
|
-
**When to use SidebarLayout vs custom:**
|
|
534
|
-
- Use `SidebarLayout` for docs, dashboards, admin panels, or any sidebar site
|
|
535
|
-
- Build custom Layout when you need complex responsive behavior or prose styling
|
|
233
|
+
title: 'Feature Grid',
|
|
234
|
+
description: 'Grid of feature cards with icons',
|
|
235
|
+
category: 'marketing',
|
|
236
|
+
// hidden: true, // Hide from content authors
|
|
237
|
+
// background: 'self', // Component renders its own background
|
|
536
238
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
```jsx
|
|
543
|
-
import { useWebsite, Link } from '@uniweb/kit'
|
|
544
|
-
|
|
545
|
-
function Header({ content, params, block }) {
|
|
546
|
-
const { website } = useWebsite()
|
|
547
|
-
|
|
548
|
-
// Get pages for header navigation (respects hideInHeader)
|
|
549
|
-
const pages = website.getPageHierarchy({ for: 'header' })
|
|
550
|
-
// Returns: [{ route, navigableRoute, title, label, hasContent, children }]
|
|
239
|
+
content: {
|
|
240
|
+
title: 'Section heading',
|
|
241
|
+
paragraphs: 'Introduction [0-1]',
|
|
242
|
+
items: 'Feature cards with icon, title, description',
|
|
243
|
+
},
|
|
551
244
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
245
|
+
params: {
|
|
246
|
+
columns: { type: 'number', default: 3 },
|
|
247
|
+
theme: { type: 'select', options: ['light', 'dark'], default: 'light' },
|
|
248
|
+
},
|
|
556
249
|
|
|
557
|
-
|
|
558
|
-
|
|
250
|
+
presets: {
|
|
251
|
+
default: { label: 'Standard', params: { columns: 3 } },
|
|
252
|
+
compact: { label: 'Compact', params: { columns: 4 } },
|
|
253
|
+
},
|
|
559
254
|
|
|
560
|
-
//
|
|
561
|
-
|
|
255
|
+
// Static capabilities for cross-block coordination
|
|
256
|
+
context: {
|
|
257
|
+
allowTranslucentTop: true, // Header can overlay this section
|
|
258
|
+
},
|
|
562
259
|
}
|
|
563
260
|
```
|
|
564
261
|
|
|
565
|
-
|
|
566
|
-
- `page.route` - The page's URL path
|
|
567
|
-
- `page.navigableRoute` - URL to use for links (may differ if page has no content)
|
|
568
|
-
- `page.label` - Short navigation label (falls back to title)
|
|
569
|
-
- `page.hasContent` - Whether page has renderable sections
|
|
570
|
-
- `page.children` - Nested child pages (if any)
|
|
262
|
+
All defaults belong in `meta.js`, not inline in component code.
|
|
571
263
|
|
|
572
|
-
|
|
573
|
-
```jsx
|
|
574
|
-
import { useWebsite } from '@uniweb/kit'
|
|
575
|
-
|
|
576
|
-
function Header({ content, params }) {
|
|
577
|
-
const { website } = useWebsite()
|
|
578
|
-
|
|
579
|
-
if (website.hasMultipleLocales()) {
|
|
580
|
-
const locales = website.getLocales() // [{code, label, isDefault}]
|
|
581
|
-
const active = website.getActiveLocale() // 'en'
|
|
582
|
-
|
|
583
|
-
return locales.map(locale => (
|
|
584
|
-
<a
|
|
585
|
-
key={locale.code}
|
|
586
|
-
href={website.getLocaleUrl(locale.code)}
|
|
587
|
-
className={locale.code === active ? 'font-bold' : ''}
|
|
588
|
-
>
|
|
589
|
-
{locale.label}
|
|
590
|
-
</a>
|
|
591
|
-
))
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
```
|
|
264
|
+
### @uniweb/kit
|
|
595
265
|
|
|
596
|
-
**
|
|
597
|
-
```jsx
|
|
598
|
-
import { useActiveRoute } from '@uniweb/kit'
|
|
599
|
-
|
|
600
|
-
function Navigation({ pages }) {
|
|
601
|
-
const { route, isActiveOrAncestor } = useActiveRoute()
|
|
266
|
+
**Primitives** (`@uniweb/kit`): `H1`–`H6`, `P`, `Span`, `Text`, `Link`, `Image`, `Icon`, `Media`, `Asset`, `SocialIcon`, `FileLogo`, `cn()`
|
|
602
267
|
|
|
603
|
-
|
|
604
|
-
// isActiveOrAncestor(page): true if page matches or is parent of current route
|
|
605
|
-
}
|
|
606
|
-
```
|
|
268
|
+
**Styled** (`@uniweb/kit/styled`): `Section`, `Render`, `SidebarLayout`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
607
269
|
|
|
608
|
-
|
|
270
|
+
**Hooks:**
|
|
271
|
+
- `useScrolled(threshold)` → boolean for scroll-based header styling
|
|
272
|
+
- `useMobileMenu()` → `{ isOpen, toggle, close }` with auto-close on navigation
|
|
273
|
+
- `useAccordion({ multiple, defaultOpen })` → `{ isOpen, toggle }` for expand/collapse
|
|
274
|
+
- `useActiveRoute()` → `{ route, isActiveOrAncestor(page) }` for nav highlighting (SSG-safe)
|
|
275
|
+
- `useGridLayout(columns, { gap })` → responsive grid class string
|
|
276
|
+
- `useTheme(name)` → standardized theme classes
|
|
609
277
|
|
|
610
|
-
|
|
611
|
-
2. Create `foundation/src/components/NewComponent/meta.js`
|
|
612
|
-
3. Rebuild: `cd foundation && pnpm build`
|
|
278
|
+
**Utilities:** `cn()` (Tailwind class merge), `filterSocialLinks(links)`, `getSocialPlatform(url)`
|
|
613
279
|
|
|
614
280
|
### Component Organization
|
|
615
281
|
|
|
616
|
-
By default, exposed components (those with `meta.js`) live in `src/components/`:
|
|
617
|
-
|
|
618
282
|
```
|
|
619
283
|
foundation/src/
|
|
620
|
-
├── components/ # Exposed
|
|
284
|
+
├── components/ # Exposed (auto-discovered via meta.js)
|
|
621
285
|
│ ├── Hero/
|
|
622
286
|
│ │ ├── index.jsx
|
|
623
|
-
│ │ └── meta.js
|
|
287
|
+
│ │ └── meta.js
|
|
624
288
|
│ └── Features/
|
|
625
289
|
│ ├── index.jsx
|
|
626
290
|
│ └── meta.js
|
|
627
|
-
├── shared/ # Internal
|
|
628
|
-
│ ├── Button
|
|
629
|
-
│ └── Card
|
|
291
|
+
├── shared/ # Internal helpers (no meta.js, not selectable)
|
|
292
|
+
│ ├── Button.jsx
|
|
293
|
+
│ └── Card.jsx
|
|
630
294
|
└── styles.css
|
|
631
295
|
```
|
|
632
296
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
**For complex projects**, you can organize exposed components in subdirectories:
|
|
636
|
-
|
|
637
|
-
```js
|
|
638
|
-
// vite.config.js
|
|
639
|
-
import { defineFoundationConfig } from '@uniweb/build'
|
|
640
|
-
|
|
641
|
-
export default defineFoundationConfig({
|
|
642
|
-
components: [
|
|
643
|
-
'components', // src/components/*/meta.js
|
|
644
|
-
'components/marketing', // src/components/marketing/*/meta.js
|
|
645
|
-
'components/docs', // src/components/docs/*/meta.js
|
|
646
|
-
]
|
|
647
|
-
})
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
This allows organizing exposed components by category while keeping internal components separate.
|
|
651
|
-
|
|
652
|
-
### meta.js Structure
|
|
653
|
-
|
|
654
|
-
```javascript
|
|
655
|
-
export default {
|
|
656
|
-
title: 'Component Name',
|
|
657
|
-
description: 'What it does',
|
|
658
|
-
category: 'marketing', // For grouping in editors
|
|
659
|
-
// hidden: true, // Uncomment to hide from content authors
|
|
660
|
-
|
|
661
|
-
// Document expected content (for editors/validation)
|
|
662
|
-
content: {
|
|
663
|
-
pretitle: 'Eyebrow text',
|
|
664
|
-
title: 'Headline',
|
|
665
|
-
paragraphs: 'Description [1-2]',
|
|
666
|
-
links: 'CTA buttons [1-2]',
|
|
667
|
-
},
|
|
297
|
+
Components without `meta.js` are internal — imported by exposed components but invisible to content authors.
|
|
668
298
|
|
|
669
|
-
|
|
670
|
-
params: {
|
|
671
|
-
theme: {
|
|
672
|
-
type: 'select',
|
|
673
|
-
label: 'Theme',
|
|
674
|
-
options: ['light', 'dark', 'gradient'],
|
|
675
|
-
default: 'light', // Runtime applies this if not set
|
|
676
|
-
},
|
|
677
|
-
columns: {
|
|
678
|
-
type: 'number',
|
|
679
|
-
label: 'Columns',
|
|
680
|
-
default: 3,
|
|
681
|
-
},
|
|
682
|
-
showIcon: {
|
|
683
|
-
type: 'boolean',
|
|
684
|
-
label: 'Show Icon',
|
|
685
|
-
default: true,
|
|
686
|
-
},
|
|
687
|
-
},
|
|
299
|
+
### Website and Page APIs
|
|
688
300
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
default: { label: 'Standard', params: { theme: 'light', columns: 3 } },
|
|
692
|
-
dark: { label: 'Dark Mode', params: { theme: 'dark', columns: 3 } },
|
|
693
|
-
compact: { label: 'Compact', params: { theme: 'light', columns: 4 } },
|
|
694
|
-
},
|
|
301
|
+
```jsx
|
|
302
|
+
const { website } = useWebsite()
|
|
695
303
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
},
|
|
304
|
+
// Navigation
|
|
305
|
+
const pages = website.getPageHierarchy({ for: 'header' }) // or 'footer'
|
|
306
|
+
// → [{ route, navigableRoute, label, hasContent, children }]
|
|
700
307
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
308
|
+
// Locale
|
|
309
|
+
website.hasMultipleLocales()
|
|
310
|
+
website.getLocales() // [{ code, label, isDefault }]
|
|
311
|
+
website.getActiveLocale() // 'en'
|
|
312
|
+
website.getLocaleUrl('es')
|
|
706
313
|
```
|
|
707
314
|
|
|
708
|
-
**Key principle:** All defaults belong in `meta.js`. Component code should never have inline defaults like `theme || 'light'` or `columns ?? 3`.
|
|
709
|
-
|
|
710
315
|
### Cross-Block Communication
|
|
711
316
|
|
|
712
|
-
Components
|
|
713
|
-
|
|
714
|
-
**Block info structure:**
|
|
715
|
-
```js
|
|
716
|
-
{
|
|
717
|
-
type: 'Hero', // Component type
|
|
718
|
-
theme: 'dark', // Theme setting
|
|
719
|
-
state: { ... }, // Dynamic state (mutable at runtime)
|
|
720
|
-
context: { ... }, // Static context (from meta.js, immutable)
|
|
721
|
-
}
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
**Key distinction:**
|
|
725
|
-
- **`context`** — Static capabilities per component type. Defined in meta.js. "All Hero components support translucent navbar overlay."
|
|
726
|
-
- **`state`** — Dynamic values per block instance. Can change via `useBlockState`. "This accordion has item 2 open."
|
|
317
|
+
Components read neighboring blocks for adaptive behavior (e.g., translucent header over hero):
|
|
727
318
|
|
|
728
|
-
**Example: Header adapting to first section:**
|
|
729
319
|
```jsx
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// Use context (static) to check capability
|
|
734
|
-
const allowTranslucentTop = firstBodyInfo?.context?.allowTranslucentTop || false
|
|
735
|
-
|
|
736
|
-
// Use theme (from params) for color adaptation
|
|
737
|
-
const isDarkTheme = firstBodyInfo?.theme === 'dark'
|
|
738
|
-
|
|
739
|
-
return (
|
|
740
|
-
<header className={cn(
|
|
741
|
-
allowTranslucentTop ? 'absolute bg-transparent' : 'relative bg-white',
|
|
742
|
-
isDarkTheme ? 'text-white' : 'text-gray-900'
|
|
743
|
-
)}>
|
|
744
|
-
...
|
|
745
|
-
</header>
|
|
746
|
-
)
|
|
747
|
-
}
|
|
748
|
-
```
|
|
320
|
+
const firstBody = block.page.getFirstBodyBlockInfo()
|
|
321
|
+
// → { type, theme, context: { allowTranslucentTop }, state }
|
|
749
322
|
|
|
750
|
-
|
|
751
|
-
```javascript
|
|
752
|
-
// Hero/meta.js
|
|
753
|
-
export default {
|
|
754
|
-
title: 'Hero Banner',
|
|
755
|
-
context: {
|
|
756
|
-
allowTranslucentTop: true, // Header can overlay this section
|
|
757
|
-
},
|
|
758
|
-
params: { ... },
|
|
759
|
-
}
|
|
323
|
+
// context = static (from meta.js), state = dynamic (from useBlockState)
|
|
760
324
|
```
|
|
761
325
|
|
|
762
|
-
###
|
|
326
|
+
### Custom Layouts
|
|
763
327
|
|
|
764
|
-
|
|
328
|
+
Export a Layout from `foundation/src/exports.js`. Kit provides `SidebarLayout` for sidebar-based sites (docs, dashboards):
|
|
765
329
|
|
|
766
330
|
```jsx
|
|
767
|
-
import {
|
|
768
|
-
|
|
769
|
-
function Accordion({ content, params, block }) {
|
|
770
|
-
// Bridge pattern: pass useState to block
|
|
771
|
-
const [state, setState] = block.useBlockState(useState)
|
|
772
|
-
|
|
773
|
-
const toggle = (index) => {
|
|
774
|
-
setState({ ...state, openItem: state.openItem === index ? null : index })
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return content.items.map((item, i) => (
|
|
778
|
-
<div key={i}>
|
|
779
|
-
<button onClick={() => toggle(i)}>{item.title}</button>
|
|
780
|
-
{state.openItem === i && <p>{item.paragraphs[0]}</p>}
|
|
781
|
-
</div>
|
|
782
|
-
))
|
|
783
|
-
}
|
|
331
|
+
import { SidebarLayout } from '@uniweb/kit/styled'
|
|
332
|
+
export default { Layout: SidebarLayout }
|
|
784
333
|
```
|
|
785
334
|
|
|
786
|
-
|
|
787
|
-
```javascript
|
|
788
|
-
// Accordion/meta.js
|
|
789
|
-
export default {
|
|
790
|
-
title: 'Accordion',
|
|
791
|
-
initialState: {
|
|
792
|
-
openItem: null,
|
|
793
|
-
},
|
|
794
|
-
params: { ... },
|
|
795
|
-
}
|
|
796
|
-
```
|
|
335
|
+
Layout receives pre-rendered areas: `header`, `body`, `footer`, `left`/`leftPanel`, `right`/`rightPanel`.
|
|
797
336
|
|
|
798
|
-
|
|
799
|
-
- **Block state** — UI state that should persist across SPA navigation (accordion position, form input)
|
|
800
|
-
- **React state** — Temporary state that should reset on navigation (hover effects, animations)
|
|
337
|
+
## Converting Existing Designs
|
|
801
338
|
|
|
802
|
-
|
|
339
|
+
When given a monolithic React file (AI-generated or hand-built), don't port line-by-line. Decompose into CCA architecture:
|
|
803
340
|
|
|
804
|
-
|
|
341
|
+
1. **Name by purpose** — `Institutions` → `Testimonial`, `WorkModes` → `FeatureColumns`. Components render a *kind* of content, not specific content.
|
|
342
|
+
2. **Separate content from code** — Hardcoded strings → markdown. Layout/styling stays in JSX. Content authors edit words without touching code.
|
|
343
|
+
3. **Use semantic tokens** — Replace `text-slate-900` with `text-heading`, `bg-white` with `bg-surface`. Component works in any context and any brand.
|
|
344
|
+
4. **Shared UI → `shared/`** — Buttons, badges, cards become internal components (no `meta.js`).
|
|
805
345
|
|
|
806
|
-
|
|
807
|
-
|------|-----|
|
|
808
|
-
| `theme: "dark"` | `backgroundColor: "#1a1a1a"` |
|
|
809
|
-
| `layout: "split"` | `gridTemplateColumns: "1fr 1fr"` |
|
|
810
|
-
| `size: "large"` | `fontSize: "2rem"` |
|
|
346
|
+
You don't have to convert everything at once. Each section is independent — one can use hardcoded content while another reads from markdown. See the [Converting Existing Designs](./guides/developers/converting-existing-designs.md) guide for the full walkthrough.
|
|
811
347
|
|
|
812
348
|
## Tailwind CSS v4
|
|
813
349
|
|
|
814
|
-
Theme
|
|
350
|
+
Theme defined in `foundation/src/styles.css`:
|
|
815
351
|
|
|
816
352
|
```css
|
|
817
353
|
@import "tailwindcss";
|
|
818
354
|
@source "./components/**/*.{js,jsx}";
|
|
819
|
-
|
|
820
|
-
@theme {
|
|
821
|
-
--color-primary: #3b82f6;
|
|
822
|
-
}
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
Use with: `bg-primary`, `text-primary`, `bg-primary/10` (10% opacity)
|
|
826
|
-
|
|
827
|
-
## Converting an Existing Design
|
|
828
|
-
|
|
829
|
-
When given a monolithic React/JSX file (e.g., a landing page built with an AI tool) to convert into a Uniweb project, follow this approach.
|
|
830
|
-
|
|
831
|
-
### Mindset
|
|
832
|
-
|
|
833
|
-
The goal is a page that **renders identically** to the original, but the implementation logic changes. You are not porting code line-by-line — you are decomposing a monolithic design into the Uniweb content/component architecture.
|
|
834
|
-
|
|
835
|
-
**Don't preserve source component names.** A monolithic file might have components named after specific content (e.g., `Institutions`, `ProximifyCTA`). Uniweb foundation components are **generic rendering agencies named for purpose**, not for the content they happen to display. A section showing an institutional quote is rendered by a component like `Testimonial` or `Quote` — something reusable for any quote, not tied to one client.
|
|
836
|
-
|
|
837
|
-
### Step 1: Identify Sections
|
|
838
|
-
|
|
839
|
-
Each visually distinct block in the design becomes:
|
|
840
|
-
- An **exposed component** in `foundation/src/components/` (with `meta.js`)
|
|
841
|
-
- A **markdown file** in `site/pages/` (with `type:` referencing that component)
|
|
842
|
-
|
|
843
|
-
Look for natural section boundaries — full-width blocks separated by spacing, background changes, or horizontal rules.
|
|
844
|
-
|
|
845
|
-
### Step 2: Separate Content from Code
|
|
846
|
-
|
|
847
|
-
For each section, ask: **what is content and what is design?**
|
|
848
|
-
|
|
849
|
-
| In the JSX source | In Uniweb |
|
|
850
|
-
|---|---|
|
|
851
|
-
| Hardcoded heading text | `# Heading` in markdown → `content.title` |
|
|
852
|
-
| Hardcoded paragraph text | Paragraph in markdown → `content.paragraphs[]` |
|
|
853
|
-
| Hardcoded link/button text and URLs | `[Label](url)` in markdown → `content.links[]` |
|
|
854
|
-
| Hardcoded images | `` in markdown → `content.imgs[]` |
|
|
855
|
-
| Repeating card/item structures | H3 groups in markdown → `content.items[]` |
|
|
856
|
-
| Icon references (e.g., `lucide-react`) | `` in markdown → `content.icons[]` |
|
|
857
|
-
| Layout, spacing, colors, animations | Component JSX + Tailwind classes |
|
|
858
|
-
| Visual elements with no content (diagrams, decorations) | Component JSX only — no markdown equivalent |
|
|
859
|
-
|
|
860
|
-
### Step 3: Name Components for Purpose
|
|
861
|
-
|
|
862
|
-
Choose names that describe **what the component does**, not what content it currently shows:
|
|
863
|
-
|
|
864
|
-
| Source name | Better Uniweb name | Why |
|
|
865
|
-
|---|---|---|
|
|
866
|
-
| `ProximifyCTA` | `CallToAction` | Renders any CTA, not just one brand's |
|
|
867
|
-
| `Institutions` | `Testimonial` or `Quote` | Renders any testimonial |
|
|
868
|
-
| `WorkModes` | `FeatureColumns` | Renders any set of features in columns |
|
|
869
|
-
| `TheModel` | `SplitContent` | Text on one side, visual on the other |
|
|
870
|
-
|
|
871
|
-
### Step 4: Design Params Beyond the Original
|
|
872
|
-
|
|
873
|
-
Exposed components should define **params in `meta.js`** that make them flexible, even if the original design only uses one variant. For example:
|
|
874
|
-
|
|
875
|
-
```javascript
|
|
876
|
-
// A component that's always "dark" in the source design
|
|
877
|
-
// should still support theme switching
|
|
878
|
-
params: {
|
|
879
|
-
theme: {
|
|
880
|
-
type: 'select',
|
|
881
|
-
options: ['light', 'dark'],
|
|
882
|
-
default: 'dark', // Matches the original design
|
|
883
|
-
},
|
|
884
|
-
align: {
|
|
885
|
-
type: 'select',
|
|
886
|
-
options: ['left', 'center'],
|
|
887
|
-
default: 'center',
|
|
888
|
-
},
|
|
889
|
-
}
|
|
890
|
-
```
|
|
891
|
-
|
|
892
|
-
The original design becomes the **default preset**. But a content author can reconfigure.
|
|
893
|
-
|
|
894
|
-
### Step 5: Create Internal Components
|
|
895
|
-
|
|
896
|
-
Not every React component in the source becomes an exposed Uniweb component. Shared UI elements like buttons, cards, and badges are **internal components** — they live in `foundation/src/shared/` (or similar) with **no `meta.js`**:
|
|
897
|
-
|
|
898
|
-
```
|
|
899
|
-
foundation/src/
|
|
900
|
-
├── components/ # Exposed (with meta.js) — selectable from markdown
|
|
901
|
-
│ ├── Hero/
|
|
902
|
-
│ ├── FeatureGrid/
|
|
903
|
-
│ └── CallToAction/
|
|
904
|
-
├── shared/ # Internal (no meta.js) — used by exposed components
|
|
905
|
-
│ ├── Button.jsx
|
|
906
|
-
│ ├── Badge.jsx
|
|
907
|
-
│ └── SectionWrapper.jsx
|
|
908
|
-
└── styles.css
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
Internal components are imported by exposed components but are invisible to content authors. This is normal and encouraged — it keeps the content interface clean.
|
|
912
|
-
|
|
913
|
-
### Step 6: Map Special Sections
|
|
914
|
-
|
|
915
|
-
| Source pattern | Uniweb equivalent |
|
|
916
|
-
|---|---|
|
|
917
|
-
| Navigation / navbar | `@header/` special page with a Header component |
|
|
918
|
-
| Footer | `@footer/` special page with a Footer component |
|
|
919
|
-
| Sticky/fixed elements | Header component using `useScrolled()` hook |
|
|
920
|
-
| Mobile menu toggle | Header component using `useMobileMenu()` hook |
|
|
921
|
-
|
|
922
|
-
### Step 7: Extract Design Tokens
|
|
923
|
-
|
|
924
|
-
Colors, fonts, and spacing from the source become Tailwind theme variables:
|
|
925
|
-
|
|
926
|
-
```css
|
|
927
|
-
/* foundation/src/styles.css */
|
|
928
|
-
@import "tailwindcss";
|
|
929
|
-
@source "./components/**/*.jsx";
|
|
930
|
-
@source "./shared/**/*.jsx";
|
|
931
355
|
@source "../node_modules/@uniweb/kit/src/**/*.jsx";
|
|
932
356
|
|
|
933
357
|
@theme {
|
|
934
|
-
--color-primary: #
|
|
935
|
-
--color-accent: #10b981; /* From the source's accent color */
|
|
358
|
+
--color-primary: #3b82f6;
|
|
936
359
|
}
|
|
937
360
|
```
|
|
938
361
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
Given a file with `Nav`, `Hero`, `Features`, `Testimonial`, `CTA`, `Footer`:
|
|
942
|
-
|
|
943
|
-
**Foundation components created:**
|
|
944
|
-
- `Hero/` — full-width hero with heading, text, buttons
|
|
945
|
-
- `FeatureGrid/` — grid of feature cards with icons
|
|
946
|
-
- `Testimonial/` — quote with attribution
|
|
947
|
-
- `CallToAction/` — dark-background CTA section
|
|
948
|
-
- `Header/` — navigation bar (for `@header`)
|
|
949
|
-
- `Footer/` — site footer (for `@footer`)
|
|
950
|
-
|
|
951
|
-
**Internal helpers:**
|
|
952
|
-
- `shared/Button.jsx` — reusable button styles
|
|
953
|
-
- `shared/SectionWrapper.jsx` — consistent section padding/width
|
|
954
|
-
|
|
955
|
-
**Site content:**
|
|
956
|
-
```
|
|
957
|
-
site/pages/
|
|
958
|
-
├── @header/
|
|
959
|
-
│ └── 1-nav.md # type: Header
|
|
960
|
-
├── @footer/
|
|
961
|
-
│ └── 1-footer.md # type: Footer (links as markdown)
|
|
962
|
-
└── home/
|
|
963
|
-
├── page.yml
|
|
964
|
-
├── 1-hero.md # type: Hero
|
|
965
|
-
├── 2-features.md # type: FeatureGrid (items from H3 groups)
|
|
966
|
-
├── 3-testimonial.md # type: Testimonial
|
|
967
|
-
└── 4-cta.md # type: CallToAction
|
|
968
|
-
```
|
|
362
|
+
Use with: `bg-primary`, `text-primary`, `bg-primary/10`
|
|
969
363
|
|
|
970
364
|
## Troubleshooting
|
|
971
365
|
|
|
972
|
-
**"Could not load foundation"**
|
|
973
|
-
- Single: Check `site/package.json` has `"foundation": "file:../foundation"`
|
|
974
|
-
- Multi: Check `sites/*/package.json` has `"default": "file:../../foundations/default"`
|
|
366
|
+
**"Could not load foundation"** — Check `site/package.json` has `"foundation": "file:../foundation"` (or `"default": "file:../../foundations/default"` for multi-site).
|
|
975
367
|
|
|
976
|
-
**Component not appearing**
|
|
977
|
-
1. Verify `meta.js` exists and doesn't have `hidden: true`
|
|
978
|
-
2. Check it's exported from `foundation/src/index.js`
|
|
979
|
-
3. Rebuild: `cd foundation && pnpm build`
|
|
368
|
+
**Component not appearing** — Verify `meta.js` exists and doesn't have `hidden: true`. Rebuild: `cd foundation && pnpm build`.
|
|
980
369
|
|
|
981
|
-
**Styles not applying**
|
|
982
|
-
1. Verify `@source` in styles.css includes your component path
|
|
983
|
-
2. Check custom color names match `@theme` definitions
|
|
370
|
+
**Styles not applying** — Verify `@source` in `styles.css` includes your component paths. Check custom colors match `@theme` definitions.
|