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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.2.42",
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.23",
41
- "@uniweb/core": "0.1.10",
42
- "@uniweb/kit": "0.1.5",
43
- "@uniweb/runtime": "0.2.11"
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
  }
@@ -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: