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 CHANGED
@@ -64,7 +64,7 @@ my-project/
64
64
  │ ├── pages/ # File-based routing
65
65
  │ │ └── home/
66
66
  │ │ ├── page.yml # Page metadata
67
- │ │ └── 1-hero.md # Section content
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/1-hero.md` and edit the headline:
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.13",
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.8",
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
  }
@@ -1,983 +1,370 @@
1
1
  # AGENTS.md
2
2
 
3
- This file provides guidance for AI assistants working with this Uniweb project.
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/ # Purpose-built component library (React)
9
+ ├── foundation/ # React component library
13
10
  ├── site/ # Content (markdown pages)
14
11
  └── pnpm-workspace.yaml
15
12
  ```
16
13
 
17
- **Multi-site (multiple foundations and sites):**
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. Components with `meta.js` are *exposed* to content authors.
28
- - **Site**: Markdown content. Each section references a component via `type:` in frontmatter.
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 (runs default/main site)
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 section is a markdown file with YAML frontmatter:
31
+ Each `.md` file is a section. Frontmatter on top, content below:
73
32
 
74
33
  ```markdown
75
34
  ---
76
- type: ComponentName # Must match an exposed component
77
- theme: dark # Parameter from meta.js properties
35
+ type: Hero
36
+ theme: dark
78
37
  ---
79
38
 
80
- ### Eyebrow Text # H3 before H1 pretitle
39
+ ### Eyebrow Text pretitle (heading before a more important one)
81
40
 
82
- # Main Headline # H1 → title
41
+ # Main Headline title
83
42
 
84
- ## Subtitle # H2 after H1 → subtitle
43
+ ## Subtitle subtitle
85
44
 
86
45
  Description paragraph.
87
46
 
88
- [Call to Action](#link)
47
+ [Call to Action](/link)
89
48
 
90
- ![Image](image.jpg)
49
+ ![Image](./image.jpg)
91
50
  ```
92
51
 
93
- ### Content Structure
52
+ ### Content Shape
94
53
 
95
- The semantic parser extracts markdown into a **flat structure**:
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 main title
103
- subtitle2: '', // Third heading level
104
-
105
- // Body content
60
+ subtitle: '', // Heading after title
61
+ subtitle2: '', // Third-level heading
106
62
  paragraphs: [], // Text blocks
107
- links: [], // All links (including buttons, documents)
108
- imgs: [], // Images (role: image, banner, gallery, background)
109
- icons: [], // Icons (role: icon)
110
- videos: [], // Videos (role: video)
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: {}, // Structured data from tagged code blocks
114
- headings: [], // Overflow headings after title/subtitle/subtitle2
115
-
116
- // Child content groups
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
- **Heading interpretation is semantic, not literal:**
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
- ### Attributes on Links and Media
78
+ ```markdown
79
+ # Our Features ← title
130
80
 
131
- Both links and media support attributes using curly braces:
81
+ We built this for you. ← paragraph
132
82
 
133
- ```markdown
134
- [text](url){key=value .class #id booleanAttr}
135
- ![alt](url){role=banner width=1200 loading=lazy}
136
- ```
83
+ ### Fast ← items[0].title
84
+ Lightning quick. ← items[0].paragraphs[0]
137
85
 
138
- **Links and buttons:**
139
- ```markdown
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
- Links are unified in the `links` array with a `role` attribute:
147
- - `role: "link"` — Standard hyperlink (default)
148
- - `role: "button"` / `"button-primary"` CTA buttons
149
- - `role: "document"` — Downloadable files (auto-detected from extension)
90
+ ### Icons
91
+
92
+ Use image syntax with library prefix: `![](lu-house)`. 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: `![Logo](./logo.svg){role=icon}`
95
+
96
+ ### Links and Media Attributes
150
97
 
151
- **Media classification by role:**
152
98
  ```markdown
153
- ![Hero](./hero.jpg) <!-- imgs array (default) -->
154
- ![Hero](./hero.jpg){role=banner} <!-- imgs array with role -->
155
- ![Logo](./logo.svg){role=icon} <!-- icons array -->
156
- ![Demo](./demo.mp4){role=video} <!-- videos array -->
99
+ [text](url){target=_blank} <!-- Open in new tab -->
100
+ [text](./file.pdf){download} <!-- Download -->
101
+ ![alt](./img.jpg){role=banner} <!-- Role determines array: imgs, icons, or videos -->
157
102
  ```
158
103
 
159
- All use image syntax but `role` determines which array they go into.
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 to components via `content.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 in component: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
119
+ Access: `content.data?.form` → `{ fields: [...], submitLabel: "Send" }`
120
+
121
+ ### Section Backgrounds
176
122
 
177
- Supported formats: `json:tag-name`, `yaml:tag-name`
123
+ Set `background` in frontmatter — the runtime renders it automatically:
178
124
 
179
- ### Asset Paths
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
- Assets can use relative or absolute paths:
133
+ Full syntax supports `image`, `video`, `gradient`, `color` modes plus overlays:
182
134
 
183
- ```markdown
184
- ![Photo](./photo.jpg) <!-- Relative to markdown file -->
185
- ![Hero](/images/hero.jpg) <!-- From public/ or assets/ folder -->
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
- Build optimizes images (PNG/JPG WebP), generates content-hashed filenames, and auto-creates video posters and PDF previews when not explicitly provided.
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/ # or sites/*/pages/
194
- ├── @header/ # Rendered on all pages
195
- ├── @footer/ # Rendered on all pages
196
- └── [page-name]/
197
- ├── page.yml # title, description, order
198
- └── 1-section.md # Numbered for ordering
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
- **page.yml options:**
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 # Sort order in navigation
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
- **Section ordering:**
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
- sections:
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
- **Best practice:** Keep defaults in `meta.js`, not in component code. This ensures:
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
- ### Using @uniweb/kit
177
+ ## Semantic Theming
291
178
 
292
- The kit is split into two paths: primitives (no Tailwind dependency) and styled components.
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
- import {
297
- // Typography
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
- **Pre-styled components** (`@uniweb/kit/styled`):
310
- ```jsx
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
- Templates include an `@source` directive for kit components by default:
189
+ **Core tokens** (available as Tailwind classes):
325
190
 
326
- ```css
327
- /* foundation/src/styles.css */
328
- @import "tailwindcss";
329
- @source "./components/**/*.jsx";
330
- @source "../node_modules/@uniweb/kit/src/**/*.jsx";
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
- **Primitives** (from `@uniweb/kit`):
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
- **Active route highlighting:**
409
- ```jsx
410
- import { useActiveRoute, cn } from '@uniweb/kit'
411
-
412
- function Navigation({ pages }) {
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
- **Accordion/FAQ:**
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
- function FAQ({ items }) {
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
- return items.map((item, i) => (
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
- return <div className={gridClass}>{items.map(...)}</div>
455
- }
456
- ```
219
+ ### Props Interface
457
220
 
458
- **Social icons:**
459
221
  ```jsx
460
- import { SocialIcon, filterSocialLinks, Link } from '@uniweb/kit'
461
-
462
- function Footer({ links }) {
463
- const socialLinks = filterSocialLinks(links)
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
- **Why use kit hooks:**
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
- Layout: SidebarLayout,
493
- }
494
- ```
495
-
496
- **SidebarLayout features:**
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
- ### Website and Page APIs
538
-
539
- Access `website` via `useWebsite()` hook or `block.website`. Access `page` via `block.page`:
540
-
541
- **Page hierarchy for navigation:**
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
- return pages.map(page => (
553
- <Link href={page.navigableRoute}>{page.label}</Link>
554
- ))
555
- }
245
+ params: {
246
+ columns: { type: 'number', default: 3 },
247
+ theme: { type: 'select', options: ['light', 'dark'], default: 'light' },
248
+ },
556
249
 
557
- function Footer({ content, params }) {
558
- const { website } = useWebsite()
250
+ presets: {
251
+ default: { label: 'Standard', params: { columns: 3 } },
252
+ compact: { label: 'Compact', params: { columns: 4 } },
253
+ },
559
254
 
560
- // Get pages for footer (respects hideInFooter)
561
- const pages = website.getPageHierarchy({ for: 'footer' })
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
- **Key page info properties:**
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
- **Locale handling (for multilingual sites):**
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
- **Active route detection (SSG-safe):**
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
- // route: current normalized route (e.g., 'docs/getting-started')
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
- ### Creating a New Component
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
- 1. Create `foundation/src/components/NewComponent/index.jsx`
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 components (auto-discovered)
284
+ ├── components/ # Exposed (auto-discovered via meta.js)
621
285
  │ ├── Hero/
622
286
  │ │ ├── index.jsx
623
- │ │ └── meta.js # Makes this component available to content
287
+ │ │ └── meta.js
624
288
  │ └── Features/
625
289
  │ ├── index.jsx
626
290
  │ └── meta.js
627
- ├── shared/ # Internal components (no meta.js)
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
- **Discovery rule:** A component is exposed if it has a `meta.js` file. Components without `meta.js` are internal helpers.
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
- // Configurable parameters with defaults
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
- // Named presets (combinations of params)
690
- presets: {
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
- // Static capabilities for cross-block coordination (optional)
697
- context: {
698
- allowTranslucentTop: true, // Header can overlay this section
699
- },
304
+ // Navigation
305
+ const pages = website.getPageHierarchy({ for: 'header' }) // or 'footer'
306
+ // → [{ route, navigableRoute, label, hasContent, children }]
700
307
 
701
- // Initial values for mutable block state (optional)
702
- initialState: {
703
- expanded: false,
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 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.
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
- function Header({ content, params, block }) {
731
- const firstBodyInfo = block.page.getFirstBodyBlockInfo()
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
- **Hero declaring its context:**
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
- ### Block State
326
+ ### Custom Layouts
763
327
 
764
- Block state persists across renders and SPA page navigations. Use for UI state like accordion open/closed, tabs, form input.
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 { useState } from 'react'
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
- **Initial state in meta.js:**
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
- **When to use block state vs React state:**
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
- ### Parameter Philosophy
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
- Design parameters that describe **intent**, not implementation:
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
- | Good | Bad |
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 is defined in `foundation/src/styles.css`:
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 | `![Alt](path)` in markdown → `content.imgs[]` |
855
- | Repeating card/item structures | H3 groups in markdown → `content.items[]` |
856
- | Icon references (e.g., `lucide-react`) | `![](lu-iconname)` 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: #0f172a; /* From the source's main color */
935
- --color-accent: #10b981; /* From the source's accent color */
358
+ --color-primary: #3b82f6;
936
359
  }
937
360
  ```
938
361
 
939
- ### Example: Converting a Landing Page
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.