uniweb 0.2.42 → 0.2.43
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.hbs +516 -24
- package/src/commands/docs.js +53 -1
- package/src/commands/i18n.js +97 -17
- package/src/templates/processor.js +8 -9
- package/src/utils/workspace.js +189 -0
- package/templates/_shared/package.json.hbs +2 -1
- package/templates/multi/foundations/default/package.json.hbs +0 -1
- package/templates/multi/foundations/default/src/_entry.generated.js +1 -0
- package/templates/multi/foundations/default/src/styles.css +1 -0
- package/templates/multi/package.json.hbs +2 -1
- package/templates/multi/sites/main/package.json.hbs +1 -1
- package/templates/multi/template.json +2 -1
- package/templates/single/foundation/package.json.hbs +0 -1
- package/templates/single/foundation/src/_entry.generated.js +1 -0
- package/templates/single/foundation/src/styles.css +1 -0
- package/templates/single/site/package.json.hbs +1 -1
- package/templates/template/template/AGENTS.md.hbs +1 -70
- package/templates/template/template/foundation/package.json.hbs +0 -1
- package/templates/template/template/foundation/src/_entry.generated.js +2 -1
- package/templates/template/template/foundation/src/styles.css +1 -0
- package/templates/template/template/package.json.hbs +2 -1
- package/templates/template/template/site/package.json.hbs +1 -1
- package/templates/multi/AGENTS.md.hbs +0 -1
- package/templates/multi/pnpm-workspace.yaml +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"prompts": "^2.4.2",
|
|
39
39
|
"tar": "^7.0.0",
|
|
40
|
-
"@uniweb/build": "0.1.
|
|
41
|
-
"@uniweb/
|
|
42
|
-
"@uniweb/
|
|
43
|
-
"@uniweb/
|
|
40
|
+
"@uniweb/build": "0.1.24",
|
|
41
|
+
"@uniweb/kit": "0.1.6",
|
|
42
|
+
"@uniweb/runtime": "0.2.12",
|
|
43
|
+
"@uniweb/core": "0.1.11"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/partials/agents-md.hbs
CHANGED
|
@@ -54,8 +54,9 @@ ls foundation/src/components/*/meta.js
|
|
|
54
54
|
|
|
55
55
|
1. **`meta.js`** - Defines the component's interface:
|
|
56
56
|
- `title`, `description` - What the component does
|
|
57
|
-
- `
|
|
58
|
-
- `
|
|
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
|
|
59
60
|
- `hidden: true` - Component exists but isn't selectable from frontmatter
|
|
60
61
|
|
|
61
62
|
2. **`index.jsx`** - The React implementation
|
|
@@ -156,33 +157,400 @@ index: getting-started # Or just name the index
|
|
|
156
157
|
|
|
157
158
|
### Props Interface
|
|
158
159
|
|
|
160
|
+
The runtime provides **guarantees** for component props. No defensive null checks needed:
|
|
161
|
+
|
|
159
162
|
```jsx
|
|
160
|
-
function MyComponent({ content, params, block
|
|
161
|
-
|
|
162
|
-
const {
|
|
163
|
-
const
|
|
164
|
-
//
|
|
163
|
+
function MyComponent({ content, params, block }) {
|
|
164
|
+
// Runtime guarantees these always exist:
|
|
165
|
+
const { title, pretitle, subtitle } = content.main.header
|
|
166
|
+
const { paragraphs, links, imgs } = content.main.body
|
|
167
|
+
const items = content.items // Always an array (may be empty)
|
|
168
|
+
|
|
169
|
+
// params already has defaults from meta.js merged in:
|
|
170
|
+
const { theme, layout } = params
|
|
171
|
+
|
|
172
|
+
// Access website and page via block or useWebsite() hook:
|
|
173
|
+
const { website } = useWebsite() // or use block.website, block.page
|
|
165
174
|
}
|
|
166
175
|
```
|
|
167
176
|
|
|
177
|
+
**Guaranteed content shape:**
|
|
178
|
+
```js
|
|
179
|
+
content = {
|
|
180
|
+
main: {
|
|
181
|
+
header: { title: '', pretitle: '', subtitle: '' },
|
|
182
|
+
body: { paragraphs: [], links: [], imgs: [], lists: [], icons: [] },
|
|
183
|
+
},
|
|
184
|
+
items: [],
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Best practice:** Keep defaults in `meta.js`, not in component code. This ensures:
|
|
189
|
+
- Documentation consistency (defaults defined once)
|
|
190
|
+
- Cleaner, more readable component code
|
|
191
|
+
- Easier maintenance (change defaults without touching JSX)
|
|
192
|
+
|
|
168
193
|
### Using @uniweb/kit
|
|
169
194
|
|
|
195
|
+
The kit is split into two paths: primitives (no Tailwind dependency) and styled components.
|
|
196
|
+
|
|
197
|
+
**Primitives** (`@uniweb/kit`):
|
|
170
198
|
```jsx
|
|
171
|
-
import {
|
|
199
|
+
import {
|
|
200
|
+
// Typography
|
|
201
|
+
H1, H2, H3, P, Span, Text,
|
|
202
|
+
// Navigation & Media
|
|
203
|
+
Link, Image, Icon, Media, Asset,
|
|
204
|
+
// Utilities
|
|
205
|
+
cn, FileLogo, MediaIcon, SocialIcon,
|
|
206
|
+
// Hooks
|
|
207
|
+
useScrolled, useMobileMenu, useAccordion, useActiveRoute,
|
|
208
|
+
useGridLayout, useTheme,
|
|
209
|
+
} from '@uniweb/kit'
|
|
172
210
|
```
|
|
173
211
|
|
|
174
|
-
-
|
|
212
|
+
**Pre-styled components** (`@uniweb/kit/styled`):
|
|
213
|
+
```jsx
|
|
214
|
+
import {
|
|
215
|
+
// Layout
|
|
216
|
+
SidebarLayout,
|
|
217
|
+
// Content rendering
|
|
218
|
+
Section, Render,
|
|
219
|
+
Code, Alert, Table, Details, Divider,
|
|
220
|
+
// UI
|
|
221
|
+
Disclaimer,
|
|
222
|
+
Media, // styled version with play button facade
|
|
223
|
+
Asset, // styled version with card preview
|
|
224
|
+
} from '@uniweb/kit/styled'
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Templates include an `@source` directive for kit components by default:
|
|
228
|
+
|
|
229
|
+
```css
|
|
230
|
+
/* foundation/src/styles.css */
|
|
231
|
+
@import "tailwindcss";
|
|
232
|
+
@source "./components/**/*.jsx";
|
|
233
|
+
@source "../node_modules/@uniweb/kit/src/**/*.jsx";
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Primitives** (from `@uniweb/kit`):
|
|
237
|
+
- `H1`-`H6`, `P`, `Span`, `Text` - Typography (handles arrays, filters empty)
|
|
175
238
|
- `Link` - Smart routing
|
|
176
|
-
- `Image` - Optimized images
|
|
177
|
-
- `
|
|
239
|
+
- `Image` - Optimized images with size presets
|
|
240
|
+
- `Icon` - SVG icon loader
|
|
241
|
+
- `Media` - Video player (YouTube, Vimeo, local) - plain version
|
|
242
|
+
- `Asset` - File download link - plain version
|
|
243
|
+
- `FileLogo`, `MediaIcon` - File type icons
|
|
244
|
+
- `SocialIcon` - SVG icons for 15+ social platforms
|
|
245
|
+
|
|
246
|
+
**Styled components** (from `@uniweb/kit/styled`):
|
|
247
|
+
- `SidebarLayout` - Layout with left/right sidebars (see Custom Layouts below)
|
|
248
|
+
- `Section` - Rich content section with width/padding/columns
|
|
249
|
+
- `Render` - ProseMirror content renderer
|
|
250
|
+
- `Code`, `Alert`, `Table`, `Details`, `Divider` - Content block renderers
|
|
251
|
+
- `Disclaimer` - Modal dialog for legal text
|
|
252
|
+
- `Media` - Video with styled play button facade
|
|
253
|
+
- `Asset` - File card with preview thumbnail and hover overlay
|
|
254
|
+
|
|
255
|
+
**Hooks:**
|
|
256
|
+
- `useScrolled(threshold)` - Scroll detection (returns boolean)
|
|
257
|
+
- `useMobileMenu()` - Mobile menu state with auto-close on route change
|
|
258
|
+
- `useAccordion({ multiple, defaultOpen })` - Expand/collapse state
|
|
259
|
+
- `useActiveRoute()` - SSG-safe route detection for navigation highlighting
|
|
260
|
+
- `useGridLayout(columns, { gap })` - Responsive grid classes
|
|
261
|
+
- `useTheme(name, overrides)` - Standardized theme classes
|
|
262
|
+
|
|
263
|
+
**Utilities:**
|
|
178
264
|
- `cn()` - Tailwind class merging
|
|
265
|
+
- `getSocialPlatform(url)` - Detect platform from URL
|
|
266
|
+
- `isSocialLink(url)` - Check if URL is a social platform
|
|
267
|
+
- `filterSocialLinks(links)` - Filter array to social links only
|
|
268
|
+
- `getGridClasses()` / `getThemeClasses()` - Non-hook versions
|
|
269
|
+
|
|
270
|
+
### Kit Hook Examples
|
|
271
|
+
|
|
272
|
+
**Scroll-based header styling:**
|
|
273
|
+
```jsx
|
|
274
|
+
import { useScrolled, cn } from '@uniweb/kit'
|
|
275
|
+
|
|
276
|
+
function Header() {
|
|
277
|
+
const scrolled = useScrolled(20) // threshold in pixels
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<header className={cn(
|
|
281
|
+
'fixed top-0 transition-all',
|
|
282
|
+
scrolled ? 'bg-white shadow-lg' : 'bg-transparent'
|
|
283
|
+
)}>
|
|
284
|
+
...
|
|
285
|
+
</header>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Mobile menu with auto-close:**
|
|
291
|
+
```jsx
|
|
292
|
+
import { useMobileMenu } from '@uniweb/kit'
|
|
293
|
+
|
|
294
|
+
function Header() {
|
|
295
|
+
const { isOpen, toggle, close } = useMobileMenu()
|
|
296
|
+
|
|
297
|
+
// Menu automatically closes on route change
|
|
298
|
+
return (
|
|
299
|
+
<>
|
|
300
|
+
<button onClick={toggle}>Menu</button>
|
|
301
|
+
{isOpen && (
|
|
302
|
+
<nav>
|
|
303
|
+
<Link href="/about" onClick={close}>About</Link>
|
|
304
|
+
</nav>
|
|
305
|
+
)}
|
|
306
|
+
</>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Active route highlighting:**
|
|
312
|
+
```jsx
|
|
313
|
+
import { useActiveRoute, cn } from '@uniweb/kit'
|
|
314
|
+
|
|
315
|
+
function Navigation({ pages }) {
|
|
316
|
+
const { isActiveOrAncestor } = useActiveRoute()
|
|
317
|
+
|
|
318
|
+
return pages.map(page => (
|
|
319
|
+
<Link
|
|
320
|
+
key={page.route}
|
|
321
|
+
href={page.navigableRoute}
|
|
322
|
+
className={cn(
|
|
323
|
+
'nav-link',
|
|
324
|
+
isActiveOrAncestor(page) && 'text-primary font-bold'
|
|
325
|
+
)}
|
|
326
|
+
>
|
|
327
|
+
{page.label}
|
|
328
|
+
</Link>
|
|
329
|
+
))
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Accordion/FAQ:**
|
|
334
|
+
```jsx
|
|
335
|
+
import { useAccordion } from '@uniweb/kit'
|
|
336
|
+
|
|
337
|
+
function FAQ({ items }) {
|
|
338
|
+
const { isOpen, toggle } = useAccordion({ expandFirst: true })
|
|
339
|
+
|
|
340
|
+
return items.map((item, i) => (
|
|
341
|
+
<div key={i}>
|
|
342
|
+
<button onClick={() => toggle(i)}>{item.question}</button>
|
|
343
|
+
{isOpen(i) && <p>{item.answer}</p>}
|
|
344
|
+
</div>
|
|
345
|
+
))
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Responsive grid:**
|
|
350
|
+
```jsx
|
|
351
|
+
import { useGridLayout } from '@uniweb/kit'
|
|
352
|
+
|
|
353
|
+
function Features({ items, params }) {
|
|
354
|
+
const gridClass = useGridLayout(params.columns, { gap: 8 })
|
|
355
|
+
// Returns: "grid gap-8 sm:grid-cols-2 lg:grid-cols-3" (for columns=3)
|
|
356
|
+
|
|
357
|
+
return <div className={gridClass}>{items.map(...)}</div>
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Social icons:**
|
|
362
|
+
```jsx
|
|
363
|
+
import { SocialIcon, filterSocialLinks, Link } from '@uniweb/kit'
|
|
364
|
+
|
|
365
|
+
function Footer({ links }) {
|
|
366
|
+
const socialLinks = filterSocialLinks(links)
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div className="flex gap-4">
|
|
370
|
+
{socialLinks.map((link, i) => (
|
|
371
|
+
<Link key={i} href={link.href}>
|
|
372
|
+
<SocialIcon url={link.href} className="w-5 h-5" />
|
|
373
|
+
</Link>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Why use kit hooks:**
|
|
381
|
+
- Eliminates boilerplate (scroll handlers, route effects, state management)
|
|
382
|
+
- Consistent behavior across components
|
|
383
|
+
- SSG-safe (works during static generation)
|
|
384
|
+
- Auto-close menus on navigation (better UX)
|
|
385
|
+
|
|
386
|
+
### Custom Layouts
|
|
387
|
+
|
|
388
|
+
Foundations can provide a custom site Layout component via `src/exports.js`. The kit includes `SidebarLayout` for common sidebar layouts:
|
|
389
|
+
|
|
390
|
+
```jsx
|
|
391
|
+
// foundation/src/exports.js
|
|
392
|
+
import { SidebarLayout } from '@uniweb/kit/styled'
|
|
393
|
+
|
|
394
|
+
export default {
|
|
395
|
+
Layout: SidebarLayout,
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**SidebarLayout features:**
|
|
400
|
+
- Optional left and/or right sidebars (show whichever panels have content)
|
|
401
|
+
- Desktop: sidebars appear inline at configurable breakpoints
|
|
402
|
+
- Mobile: left panel in slide-out drawer with FAB, right panel hidden
|
|
403
|
+
- Sticky header and sidebar options
|
|
404
|
+
- Auto-close drawer on route change
|
|
405
|
+
|
|
406
|
+
**Configuration props:**
|
|
407
|
+
```jsx
|
|
408
|
+
import { SidebarLayout } from '@uniweb/kit/styled'
|
|
409
|
+
|
|
410
|
+
function CustomLayout(props) {
|
|
411
|
+
return (
|
|
412
|
+
<SidebarLayout
|
|
413
|
+
{...props}
|
|
414
|
+
leftWidth="w-64" // Left sidebar width
|
|
415
|
+
rightWidth="w-64" // Right sidebar width
|
|
416
|
+
drawerWidth="w-72" // Mobile drawer width
|
|
417
|
+
leftBreakpoint="md" // Show left at md+
|
|
418
|
+
rightBreakpoint="xl" // Show right at xl+
|
|
419
|
+
stickyHeader={true} // Sticky header
|
|
420
|
+
stickySidebar={true} // Sticky sidebars
|
|
421
|
+
maxWidth="max-w-7xl" // Content area max width
|
|
422
|
+
/>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export default { Layout: CustomLayout }
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Layout receives pre-rendered areas:**
|
|
430
|
+
- `header` - From `@header/` sections
|
|
431
|
+
- `body` - Page content sections
|
|
432
|
+
- `footer` - From `@footer/` sections
|
|
433
|
+
- `left` / `leftPanel` - From `@left/` sections (navigation)
|
|
434
|
+
- `right` / `rightPanel` - From `@right/` sections (TOC, contextual info)
|
|
435
|
+
|
|
436
|
+
**When to use SidebarLayout vs custom:**
|
|
437
|
+
- Use `SidebarLayout` for docs, dashboards, admin panels, or any sidebar site
|
|
438
|
+
- Build custom Layout when you need complex responsive behavior or prose styling
|
|
439
|
+
|
|
440
|
+
### Website and Page APIs
|
|
441
|
+
|
|
442
|
+
Access `website` via `useWebsite()` hook or `block.website`. Access `page` via `block.page`:
|
|
443
|
+
|
|
444
|
+
**Page hierarchy for navigation:**
|
|
445
|
+
```jsx
|
|
446
|
+
import { useWebsite, Link } from '@uniweb/kit'
|
|
447
|
+
|
|
448
|
+
function Header({ content, params, block }) {
|
|
449
|
+
const { website } = useWebsite()
|
|
450
|
+
|
|
451
|
+
// Get pages for header navigation (respects hideInHeader)
|
|
452
|
+
const pages = website.getPageHierarchy({ for: 'header' })
|
|
453
|
+
// Returns: [{ route, navigableRoute, title, label, hasContent, children }]
|
|
454
|
+
|
|
455
|
+
return pages.map(page => (
|
|
456
|
+
<Link href={page.navigableRoute}>{page.label}</Link>
|
|
457
|
+
))
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function Footer({ content, params }) {
|
|
461
|
+
const { website } = useWebsite()
|
|
462
|
+
|
|
463
|
+
// Get pages for footer (respects hideInFooter)
|
|
464
|
+
const pages = website.getPageHierarchy({ for: 'footer' })
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Key page info properties:**
|
|
469
|
+
- `page.route` - The page's URL path
|
|
470
|
+
- `page.navigableRoute` - URL to use for links (may differ if page has no content)
|
|
471
|
+
- `page.label` - Short navigation label (falls back to title)
|
|
472
|
+
- `page.hasContent` - Whether page has renderable sections
|
|
473
|
+
- `page.children` - Nested child pages (if any)
|
|
474
|
+
|
|
475
|
+
**Locale handling (for multilingual sites):**
|
|
476
|
+
```jsx
|
|
477
|
+
import { useWebsite } from '@uniweb/kit'
|
|
478
|
+
|
|
479
|
+
function Header({ content, params }) {
|
|
480
|
+
const { website } = useWebsite()
|
|
481
|
+
|
|
482
|
+
if (website.hasMultipleLocales()) {
|
|
483
|
+
const locales = website.getLocales() // [{code, label, isDefault}]
|
|
484
|
+
const active = website.getActiveLocale() // 'en'
|
|
485
|
+
|
|
486
|
+
return locales.map(locale => (
|
|
487
|
+
<a
|
|
488
|
+
key={locale.code}
|
|
489
|
+
href={website.getLocaleUrl(locale.code)}
|
|
490
|
+
className={locale.code === active ? 'font-bold' : ''}
|
|
491
|
+
>
|
|
492
|
+
{locale.label}
|
|
493
|
+
</a>
|
|
494
|
+
))
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Active route detection (SSG-safe):**
|
|
500
|
+
```jsx
|
|
501
|
+
import { useActiveRoute } from '@uniweb/kit'
|
|
502
|
+
|
|
503
|
+
function Navigation({ pages }) {
|
|
504
|
+
const { route, isActiveOrAncestor } = useActiveRoute()
|
|
505
|
+
|
|
506
|
+
// route: current normalized route (e.g., 'docs/getting-started')
|
|
507
|
+
// isActiveOrAncestor(page): true if page matches or is parent of current route
|
|
508
|
+
}
|
|
509
|
+
```
|
|
179
510
|
|
|
180
511
|
### Creating a New Component
|
|
181
512
|
|
|
182
513
|
1. Create `foundation/src/components/NewComponent/index.jsx`
|
|
183
514
|
2. Create `foundation/src/components/NewComponent/meta.js`
|
|
184
|
-
3.
|
|
185
|
-
|
|
515
|
+
3. Rebuild: `cd foundation && pnpm build`
|
|
516
|
+
|
|
517
|
+
### Component Organization
|
|
518
|
+
|
|
519
|
+
By default, exposed components (those with `meta.js`) live in `src/components/`:
|
|
520
|
+
|
|
521
|
+
```
|
|
522
|
+
foundation/src/
|
|
523
|
+
├── components/ # Exposed components (auto-discovered)
|
|
524
|
+
│ ├── Hero/
|
|
525
|
+
│ │ ├── index.jsx
|
|
526
|
+
│ │ └── meta.js # Makes this component available to content
|
|
527
|
+
│ └── Features/
|
|
528
|
+
│ ├── index.jsx
|
|
529
|
+
│ └── meta.js
|
|
530
|
+
├── shared/ # Internal components (no meta.js)
|
|
531
|
+
│ ├── Button/
|
|
532
|
+
│ └── Card/
|
|
533
|
+
└── styles.css
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Discovery rule:** A component is exposed if it has a `meta.js` file. Components without `meta.js` are internal helpers.
|
|
537
|
+
|
|
538
|
+
**For complex projects**, you can organize exposed components in subdirectories:
|
|
539
|
+
|
|
540
|
+
```js
|
|
541
|
+
// vite.config.js
|
|
542
|
+
import { defineFoundationConfig } from '@uniweb/build'
|
|
543
|
+
|
|
544
|
+
export default defineFoundationConfig({
|
|
545
|
+
components: [
|
|
546
|
+
'components', // src/components/*/meta.js
|
|
547
|
+
'components/marketing', // src/components/marketing/*/meta.js
|
|
548
|
+
'components/docs', // src/components/docs/*/meta.js
|
|
549
|
+
]
|
|
550
|
+
})
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
This allows organizing exposed components by category while keeping internal components separate.
|
|
186
554
|
|
|
187
555
|
### meta.js Structure
|
|
188
556
|
|
|
@@ -190,26 +558,150 @@ import { H1, P, Link, Image, Section, cn } from '@uniweb/kit'
|
|
|
190
558
|
export default {
|
|
191
559
|
title: 'Component Name',
|
|
192
560
|
description: 'What it does',
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
561
|
+
category: 'marketing', // For grouping in editors
|
|
562
|
+
// hidden: true, // Uncomment to hide from content authors
|
|
563
|
+
|
|
564
|
+
// Document expected content (for editors/validation)
|
|
565
|
+
content: {
|
|
566
|
+
pretitle: 'Eyebrow text',
|
|
567
|
+
title: 'Headline',
|
|
568
|
+
paragraphs: 'Description [1-2]',
|
|
569
|
+
links: 'CTA buttons [1-2]',
|
|
198
570
|
},
|
|
199
|
-
|
|
571
|
+
|
|
572
|
+
// Configurable parameters with defaults
|
|
573
|
+
params: {
|
|
200
574
|
theme: {
|
|
201
575
|
type: 'select',
|
|
202
576
|
label: 'Theme',
|
|
203
|
-
options: [
|
|
204
|
-
|
|
205
|
-
{ value: 'dark', label: 'Dark' },
|
|
206
|
-
],
|
|
207
|
-
default: 'light',
|
|
577
|
+
options: ['light', 'dark', 'gradient'],
|
|
578
|
+
default: 'light', // Runtime applies this if not set
|
|
208
579
|
},
|
|
580
|
+
columns: {
|
|
581
|
+
type: 'number',
|
|
582
|
+
label: 'Columns',
|
|
583
|
+
default: 3,
|
|
584
|
+
},
|
|
585
|
+
showIcon: {
|
|
586
|
+
type: 'boolean',
|
|
587
|
+
label: 'Show Icon',
|
|
588
|
+
default: true,
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
// Named presets (combinations of params)
|
|
593
|
+
presets: {
|
|
594
|
+
default: { label: 'Standard', params: { theme: 'light', columns: 3 } },
|
|
595
|
+
dark: { label: 'Dark Mode', params: { theme: 'dark', columns: 3 } },
|
|
596
|
+
compact: { label: 'Compact', params: { theme: 'light', columns: 4 } },
|
|
209
597
|
},
|
|
598
|
+
|
|
599
|
+
// Static capabilities for cross-block coordination (optional)
|
|
600
|
+
context: {
|
|
601
|
+
allowTranslucentTop: true, // Header can overlay this section
|
|
602
|
+
},
|
|
603
|
+
|
|
604
|
+
// Initial values for mutable block state (optional)
|
|
605
|
+
initialState: {
|
|
606
|
+
expanded: false,
|
|
607
|
+
},
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Key principle:** All defaults belong in `meta.js`. Component code should never have inline defaults like `theme || 'light'` or `columns ?? 3`.
|
|
612
|
+
|
|
613
|
+
### Cross-Block Communication
|
|
614
|
+
|
|
615
|
+
Components can read information from neighboring blocks via `block.getNextBlockInfo()` or `block.page.getFirstBodyBlockInfo()`. This enables adaptive behavior like headers that become translucent over hero sections.
|
|
616
|
+
|
|
617
|
+
**Block info structure:**
|
|
618
|
+
```js
|
|
619
|
+
{
|
|
620
|
+
type: 'Hero', // Component type
|
|
621
|
+
theme: 'dark', // Theme setting
|
|
622
|
+
state: { ... }, // Dynamic state (mutable at runtime)
|
|
623
|
+
context: { ... }, // Static context (from meta.js, immutable)
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Key distinction:**
|
|
628
|
+
- **`context`** — Static capabilities per component type. Defined in meta.js. "All Hero components support translucent navbar overlay."
|
|
629
|
+
- **`state`** — Dynamic values per block instance. Can change via `useBlockState`. "This accordion has item 2 open."
|
|
630
|
+
|
|
631
|
+
**Example: Header adapting to first section:**
|
|
632
|
+
```jsx
|
|
633
|
+
function Header({ content, params, block }) {
|
|
634
|
+
const firstBodyInfo = block.page.getFirstBodyBlockInfo()
|
|
635
|
+
|
|
636
|
+
// Use context (static) to check capability
|
|
637
|
+
const allowTranslucentTop = firstBodyInfo?.context?.allowTranslucentTop || false
|
|
638
|
+
|
|
639
|
+
// Use theme (from params) for color adaptation
|
|
640
|
+
const isDarkTheme = firstBodyInfo?.theme === 'dark'
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<header className={cn(
|
|
644
|
+
allowTranslucentTop ? 'absolute bg-transparent' : 'relative bg-white',
|
|
645
|
+
isDarkTheme ? 'text-white' : 'text-gray-900'
|
|
646
|
+
)}>
|
|
647
|
+
...
|
|
648
|
+
</header>
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Hero declaring its context:**
|
|
654
|
+
```javascript
|
|
655
|
+
// Hero/meta.js
|
|
656
|
+
export default {
|
|
657
|
+
title: 'Hero Banner',
|
|
658
|
+
context: {
|
|
659
|
+
allowTranslucentTop: true, // Header can overlay this section
|
|
660
|
+
},
|
|
661
|
+
params: { ... },
|
|
210
662
|
}
|
|
211
663
|
```
|
|
212
664
|
|
|
665
|
+
### Block State
|
|
666
|
+
|
|
667
|
+
Block state persists across renders and SPA page navigations. Use for UI state like accordion open/closed, tabs, form input.
|
|
668
|
+
|
|
669
|
+
```jsx
|
|
670
|
+
import { useState } from 'react'
|
|
671
|
+
|
|
672
|
+
function Accordion({ content, params, block }) {
|
|
673
|
+
// Bridge pattern: pass useState to block
|
|
674
|
+
const [state, setState] = block.useBlockState(useState)
|
|
675
|
+
|
|
676
|
+
const toggle = (index) => {
|
|
677
|
+
setState({ ...state, openItem: state.openItem === index ? null : index })
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return content.items.map((item, i) => (
|
|
681
|
+
<div key={i}>
|
|
682
|
+
<button onClick={() => toggle(i)}>{item.header.title}</button>
|
|
683
|
+
{state.openItem === i && <p>{item.body.paragraphs[0]}</p>}
|
|
684
|
+
</div>
|
|
685
|
+
))
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
**Initial state in meta.js:**
|
|
690
|
+
```javascript
|
|
691
|
+
// Accordion/meta.js
|
|
692
|
+
export default {
|
|
693
|
+
title: 'Accordion',
|
|
694
|
+
initialState: {
|
|
695
|
+
openItem: null,
|
|
696
|
+
},
|
|
697
|
+
params: { ... },
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**When to use block state vs React state:**
|
|
702
|
+
- **Block state** — UI state that should persist across SPA navigation (accordion position, form input)
|
|
703
|
+
- **React state** — Temporary state that should reset on navigation (hover effects, animations)
|
|
704
|
+
|
|
213
705
|
### Parameter Philosophy
|
|
214
706
|
|
|
215
707
|
Design parameters that describe **intent**, not implementation:
|