uniweb 0.2.42 → 0.2.44

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.
Files changed (30) hide show
  1. package/README.md +46 -0
  2. package/package.json +5 -5
  3. package/partials/agents-md.hbs +516 -24
  4. package/partials/config-reference.hbs +67 -0
  5. package/src/commands/docs.js +300 -16
  6. package/src/commands/i18n.js +97 -17
  7. package/src/index.js +6 -0
  8. package/src/templates/processor.js +8 -9
  9. package/src/utils/workspace.js +189 -0
  10. package/templates/_shared/package.json.hbs +2 -1
  11. package/templates/multi/foundations/default/package.json.hbs +0 -1
  12. package/templates/multi/foundations/default/src/_entry.generated.js +1 -0
  13. package/templates/multi/foundations/default/src/styles.css +1 -0
  14. package/templates/multi/package.json.hbs +2 -1
  15. package/templates/multi/sites/main/package.json.hbs +1 -1
  16. package/templates/multi/template/.vscode/settings.json +6 -0
  17. package/templates/multi/template.json +2 -1
  18. package/templates/single/foundation/package.json.hbs +0 -1
  19. package/templates/single/foundation/src/_entry.generated.js +1 -0
  20. package/templates/single/foundation/src/styles.css +1 -0
  21. package/templates/single/site/package.json.hbs +1 -1
  22. package/templates/single/template/.vscode/settings.json +6 -0
  23. package/templates/template/template/AGENTS.md.hbs +1 -70
  24. package/templates/template/template/foundation/package.json.hbs +0 -1
  25. package/templates/template/template/foundation/src/_entry.generated.js +2 -1
  26. package/templates/template/template/foundation/src/styles.css +1 -0
  27. package/templates/template/template/package.json.hbs +2 -1
  28. package/templates/template/template/site/package.json.hbs +1 -1
  29. package/templates/multi/AGENTS.md.hbs +0 -1
  30. package/templates/multi/pnpm-workspace.yaml +0 -5
package/README.md CHANGED
@@ -125,6 +125,52 @@ export function Hero({ content, params }) {
125
125
 
126
126
  Standard React. Standard Tailwind. The `{ content, params }` interface is only for _exposed_ components—the ones content creators select in markdown frontmatter. Internal components use regular React props.
127
127
 
128
+ ## Next Steps
129
+
130
+ After creating your project:
131
+
132
+ 1. **Explore the structure** — Browse `site/pages/` to see how content is organized. Each page folder contains `page.yml` (metadata) and `.md` files (sections).
133
+
134
+ 2. **Generate component docs** — Run `pnpm uniweb docs` to create `COMPONENTS.md` with all available components, their parameters, and presets.
135
+
136
+ 3. **Learn the configuration** — Run `uniweb docs site` or `uniweb docs page` for quick reference on configuration options.
137
+
138
+ 4. **Create a component** — Add a folder in `foundation/src/components/`, create `index.jsx` and `meta.js`, then rebuild. See the [meta.js guide](https://github.com/uniweb/cli/blob/main/docs/meta/README.md) for the full schema.
139
+
140
+ The `meta.js` file defines what content and parameters a component accepts. The runtime uses this metadata to apply defaults and guarantee content structure—no defensive null checks needed in your component code.
141
+
142
+ ### Your First Content Change
143
+
144
+ Open `site/pages/home/1-hero.md` and edit the headline:
145
+
146
+ ```markdown
147
+ ---
148
+ type: Hero
149
+ ---
150
+
151
+ # Your New Headline Here
152
+
153
+ Updated description text.
154
+
155
+ [Get Started](/about)
156
+ ```
157
+
158
+ Save and see the change instantly in your browser.
159
+
160
+ ### Your First Component Change
161
+
162
+ Open `foundation/src/components/Hero/index.jsx`. The component receives parsed content:
163
+
164
+ ```jsx
165
+ export function Hero({ content, params }) {
166
+ const { title } = content.main.header // "Your New Headline Here"
167
+ const { paragraphs } = content.main.body // ["Updated description text."]
168
+ // Edit the JSX below...
169
+ }
170
+ ```
171
+
172
+ Change the JSX, save, and the dev server hot-reloads your changes.
173
+
128
174
  ## Foundations Are Portable
129
175
 
130
176
  The `foundation/` folder ships with your project as a convenience, but a foundation is a self-contained artifact with no dependency on any specific site. Sites reference foundations by configuration, not by folder proximity.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
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.23",
41
- "@uniweb/core": "0.1.10",
42
- "@uniweb/kit": "0.1.5",
43
- "@uniweb/runtime": "0.2.11"
40
+ "@uniweb/build": "0.1.25",
41
+ "@uniweb/runtime": "0.2.12",
42
+ "@uniweb/core": "0.1.11",
43
+ "@uniweb/kit": "0.1.7"
44
44
  }
45
45
  }
@@ -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
- - `elements` - What content it expects (title, paragraphs, links, items, etc.)
58
- - `properties` - Configurable parameters (theme, layout, etc.)
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, website }) {
161
- const { title, pretitle } = content.main?.header || {}
162
- const { paragraphs = [], links = [] } = content.main?.body || {}
163
- const items = content.items || []
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 { H1, P, Link, Image, Section, cn } from '@uniweb/kit'
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
- - `H1, H2, H3, P, Span` - Typography (handles arrays, filters empty)
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
- - `Section` - Layout wrapper
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. Export from `foundation/src/index.js`
185
- 4. Rebuild: `cd foundation && pnpm build`
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
- // hidden: true, // Uncomment to hide from content authors
194
- elements: {
195
- title: { label: 'Headline', required: true },
196
- paragraphs: { label: 'Description' },
197
- links: { label: 'Call to Action' },
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
- properties: {
571
+
572
+ // Configurable parameters with defaults
573
+ params: {
200
574
  theme: {
201
575
  type: 'select',
202
576
  label: 'Theme',
203
- options: [
204
- { value: 'light', label: 'Light' },
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: