@symbo.ls/mcp 1.0.0

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 (35) hide show
  1. package/.env.example +16 -0
  2. package/.env.railway +13 -0
  3. package/LICENSE +21 -0
  4. package/README.md +184 -0
  5. package/mcp.json +57 -0
  6. package/package.json +20 -0
  7. package/pyproject.toml +25 -0
  8. package/railway.toml +26 -0
  9. package/run.sh +17 -0
  10. package/symbols_mcp/__init__.py +1 -0
  11. package/symbols_mcp/server.py +1114 -0
  12. package/symbols_mcp/skills/ACCESSIBILITY.md +471 -0
  13. package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +70 -0
  14. package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +257 -0
  15. package/symbols_mcp/skills/BRAND_INDENTITY.md +69 -0
  16. package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +304 -0
  17. package/symbols_mcp/skills/CLAUDE.md +2158 -0
  18. package/symbols_mcp/skills/CLI_QUICK_START.md +205 -0
  19. package/symbols_mcp/skills/DESIGN_CRITIQUE.md +64 -0
  20. package/symbols_mcp/skills/DESIGN_DIRECTION.md +320 -0
  21. package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +64 -0
  22. package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +487 -0
  23. package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +136 -0
  24. package/symbols_mcp/skills/DESIGN_TO_CODE.md +64 -0
  25. package/symbols_mcp/skills/DESIGN_TREND.md +50 -0
  26. package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +236 -0
  27. package/symbols_mcp/skills/FIGMA_MATCHING.md +63 -0
  28. package/symbols_mcp/skills/GARY_TAN.md +80 -0
  29. package/symbols_mcp/skills/MARKETING_ASSETS.md +66 -0
  30. package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +614 -0
  31. package/symbols_mcp/skills/QUICKSTART.md +79 -0
  32. package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +1405 -0
  33. package/symbols_mcp/skills/THE_PRESENTATION.md +69 -0
  34. package/symbols_mcp/skills/UI_UX_PATTERNS.md +68 -0
  35. package/windsurf-mcp-config.json +18 -0
@@ -0,0 +1,2158 @@
1
+ # CLAUDE.md — Symbols / DOMQL v3 Strict Rules
2
+
3
+ ## CRITICAL: v3 Syntax Only
4
+
5
+ This project uses **DOMQL v3 syntax exclusively**. Never use v2 patterns.
6
+
7
+ | v3 (USE THIS) | v2 (NEVER USE) |
8
+ | ----------------------------- | ------------------------------- |
9
+ | `extends: 'Component'` | ~~`extend: 'Component'`~~ |
10
+ | `childExtends: 'Component'` | ~~`childExtend: 'Component'`~~ |
11
+ | Props flattened at root level | ~~`props: { ... }` wrapper~~ |
12
+ | `onClick: fn` | ~~`on: { click: fn }` wrapper~~ |
13
+ | `onRender: fn` | ~~`on: { render: fn }`~~ |
14
+
15
+ ---
16
+
17
+ ## Core Principle
18
+
19
+ **NO JavaScript imports/exports for component usage.** Components are registered once in their folders and reused through a declarative object tree. No build step, no compilation.
20
+
21
+ ---
22
+
23
+ ## Strict Rules
24
+
25
+ ### 1. Components are OBJECTS, never functions
26
+
27
+ ```js
28
+ // CORRECT
29
+ export const Header = { extends: 'Flex', padding: 'A' }
30
+
31
+ // WRONG — never do this
32
+ export const Header = (el, state) => ({ padding: 'A' })
33
+ ```
34
+
35
+ ### 2. NO imports between project files
36
+
37
+ ```js
38
+ // WRONG
39
+ import { Header } from './Header.js'
40
+ import { parseData } from '../functions/parseData.js'
41
+
42
+ // CORRECT — reference by name in tree, call functions via el.call()
43
+ { Header: {}, Button: { onClick: (el) => el.call('parseData', args) } }
44
+ ```
45
+
46
+ ### 3. ALL folders are flat — no subfolders
47
+
48
+ ```
49
+ // WRONG: components/charts/LineChart.js
50
+ // CORRECT: components/LineChart.js
51
+ ```
52
+
53
+ ### 4. PascalCase keys = child components (auto-extends)
54
+
55
+ ```js
56
+ // The key name IS the component — no extends needed when key matches
57
+ {
58
+ UpChart: {
59
+ flex: '1'
60
+ }
61
+ }
62
+ // Equivalent to: { UpChart: { extends: 'UpChart', flex: '1' } }
63
+ ```
64
+
65
+ ### 5. Props are ALWAYS flattened — no `props:` wrapper
66
+
67
+ ```js
68
+ // CORRECT
69
+ { padding: 'A', color: 'primary', id: 'main' }
70
+
71
+ // WRONG
72
+ { props: { padding: 'A', color: 'primary', id: 'main' } }
73
+ ```
74
+
75
+ ### 6. Events use `onX` prefix — no `on:` wrapper
76
+
77
+ ```js
78
+ // CORRECT
79
+ { onClick: (e, el, s) => {}, onRender: (el, s) => {}, onWheel: (e, el, s) => {} }
80
+
81
+ // WRONG
82
+ { on: { click: fn, render: fn, wheel: fn } }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Project Structure
88
+
89
+ ```
90
+ smbls/
91
+ ├── index.js # Root export (central loader)
92
+ ├── vars.js # Global variables/constants (default export)
93
+ ├── config.js # Platform configuration (default export)
94
+ ├── dependencies.js # External npm packages with fixed versions (default export)
95
+ ├── files.js # File assets (default export, managed via SDK)
96
+
97
+ ├── components/ # UI Components — PascalCase files, named exports
98
+ │ ├── index.js # export * as ComponentName from './ComponentName.js'
99
+ │ ├── Header.js
100
+ │ ├── Layout.js
101
+ │ └── ...
102
+
103
+ ├── pages/ # Pages — dash-case files, camelCase exports
104
+ │ ├── index.js # Route mapping: { '/': main, '/dashboard': dashboard }
105
+ │ ├── main.js
106
+ │ ├── dashboard.js
107
+ │ ├── add-network.js # export const addNetwork = { extends: 'Page', ... }
108
+ │ └── ...
109
+
110
+ ├── functions/ # Utility functions — camelCase, called via el.call()
111
+ │ ├── index.js # export * from './functionName.js'
112
+ │ ├── parseNetworkRow.js
113
+ │ └── ...
114
+
115
+ ├── methods/ # Element methods — called via el.methodName()
116
+ │ ├── index.js
117
+ │ └── ...
118
+
119
+ ├── state/ # State data — flat folder, default exports
120
+ │ ├── index.js # Registry importing all state files
121
+ │ └── ...
122
+
123
+ ├── designSystem/ # Design tokens — flat folder
124
+ │ ├── index.js # Token registry
125
+ │ ├── color.js
126
+ │ ├── spacing.js
127
+ │ ├── typography.js
128
+ │ ├── theme.js
129
+ │ ├── icons.js # Flat SVG icon collection (camelCase keys)
130
+ │ └── ...
131
+
132
+ └── snippets/ # Reusable data/code snippets — named exports
133
+ ├── index.js # export * from './snippetName.js'
134
+ └── ...
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Naming Conventions
140
+
141
+ | Location | Filename | Export |
142
+ | --------------- | ---------------- | -------------------------------------------- |
143
+ | `components/` | `Header.js` | `export const Header = { }` |
144
+ | `pages/` | `add-network.js` | `export const addNetwork = { }` |
145
+ | `functions/` | `parseData.js` | `export const parseData = function() { }` |
146
+ | `methods/` | `formatDate.js` | `export const formatDate = function() { }` |
147
+ | `designSystem/` | `color.js` | `export default { }` |
148
+ | `snippets/` | `mockData.js` | `export const mockData = { }` |
149
+ | `state/` | `metrics.js` | `export default { }` or `export default [ ]` |
150
+
151
+ ---
152
+
153
+ ## Component Template (v3)
154
+
155
+ ```js
156
+ export const ComponentName = {
157
+ extends: 'Flex', // v3: always "extends" (plural)
158
+ childExtends: 'ListItem', // v3: always "childExtends" (plural)
159
+
160
+ // Props flattened directly — CSS, HTML, custom
161
+ padding: 'A B',
162
+ background: 'surface',
163
+ borderRadius: 'B',
164
+ theme: 'primary',
165
+
166
+ // Events — onX prefix
167
+ onClick: (e, el, state) => {},
168
+ onRender: (el, state) => {},
169
+ onInit: async (el, state) => {},
170
+
171
+ // Conditional cases
172
+ isActive: false,
173
+ '.isActive': { background: 'primary', color: 'white' },
174
+
175
+ // Responsive
176
+ '@mobile': { padding: 'A' },
177
+ '@tablet': { padding: 'B' },
178
+
179
+ // Child components — PascalCase keys, no imports
180
+ Header: {},
181
+ Content: {
182
+ Article: { text: 'Hello' }
183
+ },
184
+ Footer: {}
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Props Anatomy (Complete Reference)
191
+
192
+ In Symbols, CSS-in-props and attr-in-props are unified alongside other properties as a single flat object. This unifies HTML, CSS, and JavaScript syntax into a single object with a flat structure.
193
+
194
+ ```js
195
+ {
196
+ // CSS properties (use design system tokens)
197
+ padding: 'C2 A',
198
+ background: 'gray',
199
+ borderRadius: 'C',
200
+
201
+ // HTML properties
202
+ id: 'some-id',
203
+ class: 'm-16',
204
+
205
+ // Component specific properties
206
+ isActive: true,
207
+ currentTime: new Date(),
208
+
209
+ // Overwrite props to specific children
210
+ Button: {
211
+ Icon: { name: 'sun' }
212
+ },
213
+
214
+ // Overwrite props to all children (single level)
215
+ childProps: {
216
+ color: 'red',
217
+ name: 'sun'
218
+ },
219
+
220
+ // Passing children as array
221
+ children: [{ state: { isActive: 'sun' } }],
222
+
223
+ // CSS selectors
224
+ ':hover': {},
225
+ '& > span': {},
226
+
227
+ // Media queries
228
+ '@tablet': {},
229
+ '@print': {},
230
+ '@tv': {},
231
+
232
+ // Global theming
233
+ '@dark': {},
234
+ '@light': {},
235
+
236
+ // Conditional cases (dot prefix for local)
237
+ isCompleted: true,
238
+ '.isCompleted': { textDecoration: 'line-through' },
239
+ '!isCompleted': { textDecoration: 'none' },
240
+
241
+ // Global cases (dollar prefix)
242
+ '$ios': { icon: 'apple' },
243
+
244
+ // Events
245
+ onRender: (element, state, context) => {},
246
+ onStateUpdate: (changes, element, state, context) => {},
247
+ onClick: (event, element, state, context) => {},
248
+ onLoad: (event, element, state, context) => {},
249
+ }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Built-in Property Categories
255
+
256
+ ### Box / Spacing Properties
257
+
258
+ | Property | Description | Example |
259
+ | -------------- | -------------------------- | ---------------------- |
260
+ | `padding` | Space inside the element | `padding: 'A1 C2'` |
261
+ | `margin` | Outer space of the element | `margin: '0 -B2'` |
262
+ | `gap` | Pace between children | `gap: 'A2'` |
263
+ | `width` | Width of the element | `width: 'F1'` |
264
+ | `height` | Height of the element | `height: 'F1'` |
265
+ | `boxSize` | Width + height shorthand | `boxSize: 'C1 E'` |
266
+ | `borderRadius` | Rounding corners | `borderRadius: 'C2'` |
267
+ | `widthRange` | min-width + max-width | `widthRange: 'A1 B2'` |
268
+ | `heightRange` | min-height + max-height | `heightRange: 'A1 B2'` |
269
+ | `minWidth` | Minimum width | `minWidth: 'F1'` |
270
+ | `maxWidth` | Maximum width | `maxWidth: 'F1'` |
271
+ | `minHeight` | Minimum height | `minHeight: 'F1'` |
272
+ | `maxHeight` | Maximum height | `maxHeight: 'F1'` |
273
+ | `aspectRatio` | Aspect ratio of the box | `aspectRatio: '1 / 2'` |
274
+
275
+ ### Flex Properties
276
+
277
+ | Property | Description | Example |
278
+ | ---------------- | ----------------------------------------- | ------------------------------ |
279
+ | `flexFlow` | CSS flexFlow shorthand | `flexFlow: 'row wrap'` |
280
+ | `flexDirection` | CSS flexDirection | `flexDirection: 'column'` |
281
+ | `flexAlign` | Shorthand for alignItems + justifyContent | `flexAlign: 'center center'` |
282
+ | `alignItems` | CSS alignItems | `alignItems: 'flex-start'` |
283
+ | `alignContent` | CSS alignContent | `alignContent: 'flex-start'` |
284
+ | `justifyContent` | CSS justifyContent | `justifyContent: 'flex-start'` |
285
+
286
+ ### Color / Theme Properties
287
+
288
+ | Property | Syntax | Example |
289
+ | ------------ | ---------------------------------------- | ------------------------------- |
290
+ | `background` | `'colorName opacity saturation'` | `background: 'oceanblue'` |
291
+ | `color` | `'colorName opacity saturation'` | `color: 'oceanblue 0.5'` |
292
+ | `border` | `'colorName size style'` | `border: 'oceanblue 1px solid'` |
293
+ | `shadow` | `'colorName x y depth offset'` | `shadow: 'black A A C'` |
294
+ | `theme` | `'themeName'` or `'themeName .modifier'` | `theme: 'primary'` |
295
+
296
+ ### Shape Properties
297
+
298
+ | Property | Description | Example |
299
+ | --------------- | ---------------------------------- | --------------------------------------------- |
300
+ | `shape` | Name from Shapes config | `shape: 'tag'` |
301
+ | `shapeModifier` | Position/direction for tooltip/tag | `shapeModifier: { position: 'block center' }` |
302
+
303
+ ### Typography Properties
304
+
305
+ | Property | Description | Example |
306
+ | ------------ | ---------------------------------------- | ------------------- |
307
+ | `fontSize` | Typography sequence unit or CSS value | `fontSize: 'B'` |
308
+ | `fontWeight` | CSS font-weight or closest config weight | `fontWeight: '500'` |
309
+
310
+ ### Animation Properties
311
+
312
+ | Property | Description | Example |
313
+ | ------------------------- | ---------------------------------- | ----------------------------------------- |
314
+ | `animation` | Bundle animation properties | `animation: 'fadeIn'` |
315
+ | `animationName` | Name from design system | `animationName: 'fadeIn'` |
316
+ | `animationDuration` | Timing sequence or CSS value | `animationDuration: 'C'` |
317
+ | `animationDelay` | Timing sequence or CSS value | `animationDelay: 'C'` |
318
+ | `animationTimingFunction` | Timing function from config or CSS | `animationTimingFunction: '...'` |
319
+ | `animationFillMode` | CSS animation-fill-mode | `animationFillMode: 'both'` |
320
+ | `animationPlayState` | CSS animation-play-state | `animationPlayState: 'running'` |
321
+ | `animationIterationCount` | CSS animation-iteration-count | `animationIterationCount: 'infinite'` |
322
+ | `animationDirection` | CSS animation-direction | `animationDirection: 'alternate-reverse'` |
323
+
324
+ ### Media / Selector / Cases Properties
325
+
326
+ | Property | Description | Example |
327
+ | ------------- | ---------------------------------- | ---------------------------------- |
328
+ | `@mediaQuery` | CSS @media query as object | `'@mobile': { fontSize: 'A' }` |
329
+ | `:selector` | CSS selector as object | `':hover': { color: 'blue' }` |
330
+ | `$cases` | Global JS conditions on properties | `'$ios': { text: 'Hello Apple!' }` |
331
+
332
+ ---
333
+
334
+ ## Spacing Scale
335
+
336
+ Ratio-based system (base 16px, ratio 1.618 golden ratio):
337
+
338
+ | Token | ~px | Token | ~px | Token | ~px |
339
+ | ----- | --- | ----- | --- | ----- | --- |
340
+ | X | 3 | A | 16 | D | 67 |
341
+ | Y | 6 | A1 | 20 | E | 109 |
342
+ | Z | 10 | A2 | 22 | F | 177 |
343
+ | Z1 | 12 | B | 26 | | |
344
+ | Z2 | 14 | B1 | 32 | | |
345
+ | | | B2 | 36 | | |
346
+ | | | C | 42 | | |
347
+ | | | C1 | 52 | | |
348
+ | | | C2 | 55 | | |
349
+
350
+ Spacing values are generated from a base size and ratio using a mathematical sequence. The font size from Typography is used as the base size for all spacing units. Values work with `padding`, `margin`, `gap`, `width`, `height`, `borderRadius`, `position`, and any spacing-related property.
351
+
352
+ ```js
353
+ { padding: 'A B', gap: 'C', borderRadius: 'Z', fontSize: 'B1' }
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Typography Scale
359
+
360
+ Typography uses a base size (default 16px) and ratio (default 1.25) to generate a type scale sequence. Each unit is sequentially generated by multiplying the base by the ratio.
361
+
362
+ ```js
363
+ // designSystem/typography.js
364
+ export default {
365
+ base: 16,
366
+ ratio: 1.25,
367
+ subSequence: true,
368
+ templates: {}
369
+ }
370
+ ```
371
+
372
+ The same letter tokens (A, B, C, etc.) apply to fontSize, with values computed from the typography sequence rather than the spacing sequence.
373
+
374
+ ---
375
+
376
+ ## Shorthand Props
377
+
378
+ ```js
379
+ flow: 'y' // flexFlow: 'column'
380
+ flow: 'x' // flexFlow: 'row'
381
+ align: 'center space-between' // alignItems + justifyContent
382
+ round: 'B' // borderRadius
383
+ size: 'C' // width + height
384
+ wrap: 'wrap' // flexWrap
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Colors & Themes
390
+
391
+ ```js
392
+ // Usage in components
393
+ { color: 'primary', background: 'surface', borderColor: 'secondary 0.5' }
394
+
395
+ // Color syntax: 'colorName opacity lightness'
396
+ background: 'black .001' // black with 0.1% opacity
397
+ background: 'deepFir 1 +5' // deepFir, 100% opacity, +5 lightness
398
+ background: 'gray2 0.85 +16' // gray2, 85% opacity, +16 lightness
399
+ color: 'white 0.65' // white at 65% opacity
400
+
401
+ // Theme prop
402
+ { Button: { theme: 'primary', text: 'Submit' } }
403
+ { Button: { theme: 'primary .active' } } // Theme with modifier
404
+
405
+ // Inline theme object
406
+ {
407
+ theme: {
408
+ color: 'white',
409
+ '@dark': { color: 'oceanblue', background: 'white' }
410
+ }
411
+ }
412
+
413
+ // Dark/light mode
414
+ { '@dark': { background: 'gray-900' }, '@light': { background: 'white' } }
415
+ ```
416
+
417
+ ---
418
+
419
+ ## Design System Configuration
420
+
421
+ ```js
422
+ // designSystem/index.js — full configuration
423
+ export default {
424
+ COLOR: {
425
+ black: '#000',
426
+ white: '#fff',
427
+ softBlack: '#1d1d1d',
428
+ // Array format for light/dark: [lightValue, darkValue]
429
+ title: ['--gray1 1', '--gray15 1'],
430
+ document: ['--gray15 1', '--gray1 1 +4']
431
+ },
432
+ GRADIENT: {},
433
+ THEME: {
434
+ document: {
435
+ '@light': { color: 'black', background: 'white' },
436
+ '@dark': { color: 'white', background: 'softBlack' }
437
+ },
438
+ transparent: {
439
+ '@dark': { background: 'transparent', color: 'white 0.65' },
440
+ '@light': { background: 'transparent', color: 'black 0.65' }
441
+ },
442
+ button: {
443
+ '@dark': {
444
+ color: 'white',
445
+ background: 'transparent',
446
+ ':hover': { background: 'deepFir' },
447
+ ':active': { background: 'deepFir +15' }
448
+ }
449
+ },
450
+ field: {
451
+ '@dark': {
452
+ color: 'gray14',
453
+ background: 'gray3 0.5',
454
+ ':hover': { background: 'gray3 0.65 +2' },
455
+ ':focus': { background: 'gray3 0.65 +6' }
456
+ }
457
+ }
458
+ },
459
+ FONT: {},
460
+ FONT_FAMILY: {},
461
+ TYPOGRAPHY: {
462
+ base: 16,
463
+ ratio: 1.25,
464
+ subSequence: true,
465
+ templates: {}
466
+ },
467
+ SPACING: {
468
+ base: 16,
469
+ ratio: 1.618,
470
+ subSequence: true
471
+ },
472
+ TIMING: {},
473
+ GRID: {},
474
+ ICONS: {},
475
+ SHAPE: {},
476
+ ANIMATION: {},
477
+ MEDIA: {},
478
+ CASES: {},
479
+ // Configuration flags
480
+ useReset: true,
481
+ useVariable: true,
482
+ useFontImport: true,
483
+ useIconSprite: true,
484
+ useSvgSprite: true,
485
+ useDefaultConfig: true,
486
+ useDocumentTheme: true,
487
+ verbose: false,
488
+ globalTheme: 'dark'
489
+ }
490
+ ```
491
+
492
+ ---
493
+
494
+ ## Atom Components (Primitives)
495
+
496
+ Symbols provides 15 built-in primitive atom components:
497
+
498
+ | Atom | HTML Tag | Description |
499
+ | ---------- | ---------- | ----------------------------- |
500
+ | `Text` | `<span>` | Text content |
501
+ | `Box` | `<div>` | Generic container |
502
+ | `Flex` | `<div>` | Flexbox container |
503
+ | `Grid` | `<div>` | CSS Grid container |
504
+ | `Link` | `<a>` | Anchor with built-in router |
505
+ | `Input` | `<input>` | Form input |
506
+ | `Radio` | `<input>` | Radio button |
507
+ | `Checkbox` | `<input>` | Checkbox |
508
+ | `Svg` | `<svg>` | SVG container |
509
+ | `Icon` | `<svg>` | Icon from icon sprite |
510
+ | `IconText` | `<div>` | Icon + text combination |
511
+ | `Button` | `<button>` | Button with icon/text support |
512
+ | `Img` | `<img>` | Image element |
513
+ | `Iframe` | `<iframe>` | Embedded frame |
514
+ | `Video` | `<video>` | Video element |
515
+
516
+ ```js
517
+ // Usage
518
+ { Box: { padding: 'A', background: 'surface' } }
519
+ { Flex: { flow: 'y', gap: 'B', align: 'center center' } }
520
+ { Grid: { columns: 'repeat(3, 1fr)', gap: 'A' } }
521
+ { Link: { text: 'Click here', href: '/dashboard' } }
522
+ { Button: { text: 'Submit', theme: 'primary', icon: 'check' } }
523
+ { Icon: { name: 'chevronLeft' } }
524
+ { Img: { src: 'photo.png', boxSize: 'D D' } }
525
+ ```
526
+
527
+ ---
528
+
529
+ ## Event Handlers
530
+
531
+ ```js
532
+ {
533
+ // Lifecycle
534
+ onInit: (el, state) => {}, // Once on creation
535
+ onRender: (el, state) => {}, // On each render
536
+ onUpdate: (el, state) => {}, // On props/state change
537
+ onStateUpdate: (changes, el, state, context) => {},
538
+
539
+ // DOM events
540
+ onClick: (e, el, state) => {},
541
+ onInput: (e, el, state) => {},
542
+ onKeydown: (e, el, state) => {},
543
+ onDblclick: (e, el, state) => {},
544
+ onMouseover: (e, el, state) => {},
545
+ onWheel: (e, el, state) => {},
546
+ onSubmit: (e, el, state) => {},
547
+ onLoad: (e, el, state) => {},
548
+
549
+ // Call global functions
550
+ onClick: (e, el) => el.call('functionName', args),
551
+
552
+ // Call methods
553
+ onClick: (e, el) => el.methodName(),
554
+
555
+ // Update state
556
+ onClick: (e, el, state) => state.update({ count: state.count + 1 }),
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ ## Scope (Local Functions)
563
+
564
+ Use `scope` for component-specific helpers. Use `functions/` for reusable utilities.
565
+
566
+ ```js
567
+ export const Dashboard = {
568
+ extends: 'Page',
569
+
570
+ scope: {
571
+ fetchMetrics: (el, s, timeRange) => {
572
+ // el.call() for global functions — no imports
573
+ el.call('apiFetch', 'POST', '/api/metrics', { timeRange }).then((data) =>
574
+ s.update({ metrics: data })
575
+ )
576
+ }
577
+ },
578
+
579
+ onInit: (el, s) => el.scope.fetchMetrics(el, s, 5)
580
+ }
581
+ ```
582
+
583
+ ---
584
+
585
+ ## State Management
586
+
587
+ ### Setting State
588
+
589
+ State is defined in the element and can be any type. States have inheritance where children access the closest state from ancestors.
590
+
591
+ ```js
592
+ {
593
+ state: {
594
+ userProfile: { name: '...' },
595
+ activeItemId: 15,
596
+ data: [{ ... }]
597
+ }
598
+ }
599
+
600
+ // String state inherits from parent state key
601
+ {
602
+ state: { userProfile: { name: 'Mike' } },
603
+ User: {
604
+ state: 'userProfile',
605
+ text: '{{ name }}' // returns 'Mike'
606
+ }
607
+ }
608
+ ```
609
+
610
+ ### Accessing State
611
+
612
+ State values can be accessed in strings (template binding) and functions:
613
+
614
+ ```js
615
+ // Template string binding
616
+ {
617
+ text: '{{ name }} {{ surname }}'
618
+ }
619
+
620
+ // Function access
621
+ {
622
+ text: (el, s) => s.name + ' ' + s.surname
623
+ }
624
+ ```
625
+
626
+ ### Updating State
627
+
628
+ ```js
629
+ {
630
+ state: { isActive: true },
631
+ onClick: (e, el, s) => {
632
+ s.update({ isActive: !s.isActive })
633
+ }
634
+ }
635
+ ```
636
+
637
+ ### State Methods
638
+
639
+ ```js
640
+ s.update({ key: value }) // Update + re-render
641
+ s.apply((s) => {
642
+ s.items.push(newItem)
643
+ }) // Mutate with function
644
+ s.replace(data, { preventUpdate: true }) // Replace without render
645
+ s.root.update({ modal: '/add-network' }) // Update root state
646
+ s.root.quietUpdate({ modal: null }) // Update root without listener
647
+ s.parent.update({ isActive: true }) // Update parent state
648
+ s.toggle('isActive') // Toggle boolean value
649
+ s.destroy() // Destroy state and references
650
+ ```
651
+
652
+ ### Root State
653
+
654
+ Root state is application-level global state accessible via `state.root`:
655
+
656
+ ```js
657
+ {
658
+ User: {
659
+ text: (el, s) => (s.root.authToken ? 'Authorized' : 'Not authorized')
660
+ }
661
+ }
662
+ ```
663
+
664
+ ---
665
+
666
+ ## Children Pattern
667
+
668
+ ```js
669
+ {
670
+ List: {
671
+ children: (el, state) => state.items,
672
+ childrenAs: 'state',
673
+ childExtends: 'ListItem', // v3: childExtends
674
+ childProps: {
675
+ padding: 'A B',
676
+ Checkbox: { checked: (el, s) => s.done },
677
+ Text: { text: (el, s) => s.title },
678
+ },
679
+ },
680
+ }
681
+ ```
682
+
683
+ ---
684
+
685
+ ## Dynamic Imports (External Dependencies Only)
686
+
687
+ ```js
688
+ // dependencies.js defines allowed packages with fixed versions
689
+ export default { 'chart.js': '4.4.9', 'fuse.js': '7.1.0' }
690
+
691
+ // Import at runtime inside handlers — never at top level
692
+ {
693
+ onClick: async (e, el) => {
694
+ const { Chart } = await import('chart.js')
695
+ new Chart(el, { /* config */ })
696
+ },
697
+ }
698
+ ```
699
+
700
+ ---
701
+
702
+ ## Icons
703
+
704
+ ```js
705
+ // designSystem/icons.js — flat camelCase keys with inline SVG strings
706
+ export default {
707
+ chevronLeft: '<svg ...>...</svg>',
708
+ search: '<svg ...>...</svg>'
709
+ }
710
+
711
+ // Usage
712
+ {
713
+ Icon: {
714
+ name: 'chevronLeft'
715
+ }
716
+ }
717
+ {
718
+ Button: {
719
+ icon: 'search'
720
+ }
721
+ }
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Pages & Routing
727
+
728
+ ```js
729
+ // pages/dashboard.js
730
+ export const dashboard = {
731
+ extends: 'Page',
732
+ padding: 'C',
733
+ Header: {},
734
+ Content: {},
735
+ }
736
+
737
+ // pages/index.js — route mapping
738
+ import { main } from './main'
739
+ import { dashboard } from './dashboard'
740
+ export default { '/': main, '/dashboard': dashboard }
741
+
742
+ // Nested pages with sub-routes
743
+ {
744
+ extends: 'Layout',
745
+ routes: {
746
+ '/': { H1: { text: 'Home' } },
747
+ '/about': { H1: { text: 'About' } },
748
+ },
749
+ onRender: (el) => {
750
+ const { pathname } = window.location
751
+ el.call('router', pathname, el, {}, { level: 1 })
752
+ },
753
+ }
754
+ ```
755
+
756
+ ### Link Component (Built-in Router)
757
+
758
+ ```js
759
+ { Link: { text: 'Go to dashboard', href: '/dashboard' } }
760
+ ```
761
+
762
+ ### Router Navigation
763
+
764
+ ```js
765
+ // From components (el context)
766
+ el.router('/dashboard', el.getRoot())
767
+
768
+ // From functions (this context)
769
+ this.call('router', '/dashboard', this.__ref.root)
770
+
771
+ // With dynamic path
772
+ this.call('router', '/network/' + data.protocol, this.__ref.root)
773
+ ```
774
+
775
+ ---
776
+
777
+ ## Element Methods
778
+
779
+ ```js
780
+ element.update(newProps) // Update element
781
+ element.setProps(props) // Update props
782
+ element.set(content) // Set content
783
+ element.remove() // Remove from DOM
784
+ element.lookup('key') // Find ancestor by key
785
+ element.call('functionName', args) // Call global function
786
+ element.scope.localFn(el, s) // Call scope function
787
+ state.update({ key: value }) // Update state and re-render
788
+ ```
789
+
790
+ ---
791
+
792
+ ## HTML Tag Mapping
793
+
794
+ PascalCase keys auto-map to HTML tags:
795
+
796
+ | Key | Tag | Key | Tag | Key | Tag |
797
+ | ------- | ------------- | ------- | ----------- | ------ | ------------- |
798
+ | Header | `<header>` | Nav | `<nav>` | Main | `<main>` |
799
+ | Section | `<section>` | Article | `<article>` | Footer | `<footer>` |
800
+ | H1-H6 | `<h1>`-`<h6>` | P | `<p>` | Span | `<span>` |
801
+ | Div | `<div>` | A/Link | `<a>` | Ul/Ol | `<ul>`/`<ol>` |
802
+ | Form | `<form>` | Input | `<input>` | Button | `<button>` |
803
+ | Img | `<img>` | Video | `<video>` | Canvas | `<canvas>` |
804
+
805
+ Non-matching PascalCase -> `<div>` (or extends from registered component).
806
+
807
+ ---
808
+
809
+ ## Reserved Keywords
810
+
811
+ | Keyword | Purpose |
812
+ | ----------------------- | -------------------------------------------- |
813
+ | `tag` | HTML tag to render |
814
+ | `extends` | Inherit from component(s) — v3 |
815
+ | `childExtends` | Apply extends to all children — v3 |
816
+ | `childExtendsRecursive` | Apply extends recursively to all descendants |
817
+ | `childProps` | Apply props to all children |
818
+ | `state` | Component state |
819
+ | `scope` | Local helper functions |
820
+ | `children` | Array of child elements |
821
+ | `childrenAs` | Map children to 'props' or 'state' |
822
+ | `context` | Application context |
823
+ | `key` | Element key identifier |
824
+ | `query` | Query binding |
825
+ | `data` | Data binding |
826
+ | `attr` | HTML attributes object |
827
+ | `class` | CSS classes object |
828
+ | `style` | Inline styles object |
829
+ | `content` | Dynamic rendering function |
830
+ | `hide` | Conditionally hide element |
831
+ | `if` | Conditionally render element |
832
+ | `html` | Raw HTML content |
833
+ | `text` | Text content |
834
+ | `routes` | Sub-route definitions |
835
+
836
+ ---
837
+
838
+ ## CSS Selectors & Pseudo Elements
839
+
840
+ ```js
841
+ {
842
+ // Pseudo selectors
843
+ ':hover': { background: 'deepFir' },
844
+ ':active': { background: 'deepFir 1 +5' },
845
+ ':focus-visible': { outline: 'solid, X, blue .3' },
846
+ ':empty': { opacity: 0, visibility: 'hidden', pointerEvents: 'none' },
847
+ ':first-child': { margin: 'auto - -' },
848
+ ':focus ~ button': { opacity: '1' },
849
+
850
+ // Pseudo elements
851
+ ':before': { content: '"#"' },
852
+ '::after': { content: '""', position: 'absolute' },
853
+ '::-webkit-scrollbar': { display: 'none' },
854
+
855
+ // Child/sibling selectors
856
+ '> label': { width: '100%' },
857
+ '> *': { width: '100%' },
858
+ '& > span': {},
859
+
860
+ // Style block with &
861
+ '&:hover': { opacity: '1 !important' },
862
+ }
863
+ ```
864
+
865
+ ---
866
+
867
+ ## Conditional Cases (Local)
868
+
869
+ Conditional cases watch passed props and apply only when matched:
870
+
871
+ ```js
872
+ {
873
+ isActive: (el, s) => s.userId === '01',
874
+ color: 'white',
875
+ background: 'blue .65',
876
+ '.isActive': { background: 'blue' },
877
+ '!isActive': { background: 'gray' }, // Negation
878
+ }
879
+ ```
880
+
881
+ ---
882
+
883
+ ## Global Cases ($cases)
884
+
885
+ Cases are JavaScript conditions defined globally in the design system and used conditionally in components:
886
+
887
+ ```js
888
+ // In designSystem — define cases
889
+ {
890
+ cases: {
891
+ isLocalhost: location.host === 'localhost',
892
+ ios: () => ['iPad', 'iPhone', 'iPod'].includes(navigator.platform),
893
+ android: () => navigator.userAgent.toLowerCase().indexOf("android") > -1,
894
+ }
895
+ }
896
+
897
+ // In components — use with $ prefix
898
+ {
899
+ H1: {
900
+ text: 'Hello World!',
901
+ '$localhost': { display: 'none' },
902
+ '$ios': { color: 'black' },
903
+ '$android': { color: 'green' },
904
+ }
905
+ }
906
+ ```
907
+
908
+ ---
909
+
910
+ ## Media Queries & Responsive Breakpoints
911
+
912
+ ```js
913
+ {
914
+ fontSize: 'B',
915
+ padding: 'C1',
916
+
917
+ '@mobile': { fontSize: 'A', padding: 'A' },
918
+ '@mobileXS': { fontSize: 'Z1' },
919
+ '@mobileS': { minWidth: 'G1', fontSize: 'Z2' },
920
+ '@mobileM': { columns: 'repeat(1, 1fr)' },
921
+ '@tablet': { padding: 'B' },
922
+ '@tabletM': { hide: true },
923
+ '@print': {},
924
+ '@tv': {},
925
+ }
926
+ ```
927
+
928
+ **Note:** Nesting inside @media properties will not work. Properties can be replaced only at the same level.
929
+
930
+ ---
931
+
932
+ ## Template String Binding `{{ }}`
933
+
934
+ State values can be bound using mustache-style templates:
935
+
936
+ ```js
937
+ {
938
+ Strong: { text: '{{moniker}}' }, // Binds to s.moniker
939
+ Version: { text: '({{ client_version }})' }, // Binds to s.client_version
940
+ Avatar: { src: '{{ protocol }}.png' }, // In URLs
941
+ Input: { value: '{{ protocol }}' }, // In form values
942
+ }
943
+ ```
944
+
945
+ ---
946
+
947
+ ## Dynamic Props as Functions
948
+
949
+ Props can be a function receiving `(el, s)` to compute values dynamically:
950
+
951
+ ```js
952
+ // Individual prop as function
953
+ {
954
+ text: (el, s) => `Count: ${s.fleet?.length || 0}`,
955
+ hide: (el, s) => !s.thread.length,
956
+ src: (el, s) => s.root.user?.picture || 'default.png',
957
+ href: (el, s) => '/network/' + s.protocol,
958
+ background: (el, s) => el.call('getStatusColor', s.status),
959
+ }
960
+
961
+ // Dynamic childProps as function
962
+ childProps: (el, s) => ({
963
+ flex: s.value,
964
+ background: el.call('stringToHexColor', s.caption),
965
+ })
966
+ ```
967
+
968
+ ---
969
+
970
+ ## `content` Property (Dynamic Rendering)
971
+
972
+ ```js
973
+ export const Modal = {
974
+ content: (el, s) => ({
975
+ Box:
976
+ (s.root.modal && {
977
+ extends: s.root.modal, // Extends from a page path dynamically
978
+ onClick: (ev) => ev.stopPropagation()
979
+ }) ||
980
+ {}
981
+ })
982
+ }
983
+ ```
984
+
985
+ ---
986
+
987
+ ## `hide` Property
988
+
989
+ ```js
990
+ hide: true // Always hidden
991
+ hide: (el, s) => !s.public_key // Conditionally hidden
992
+ hide: () => window.location.pathname === '/' // Based on URL
993
+ ```
994
+
995
+ ---
996
+
997
+ ## `if` Property (Conditional Rendering)
998
+
999
+ ```js
1000
+ {
1001
+ LabelTag: {
1002
+ if: (el, s) => s.label, // Only render if s.label exists
1003
+ text: (el, s) => s.label || 'true',
1004
+ }
1005
+ }
1006
+ ```
1007
+
1008
+ ---
1009
+
1010
+ ## `html` Property (Raw HTML)
1011
+
1012
+ ```js
1013
+ {
1014
+ html: (el, s) => s.message
1015
+ } // Render raw HTML content
1016
+ ```
1017
+
1018
+ ---
1019
+
1020
+ ## `tag` Property Override
1021
+
1022
+ ```js
1023
+ {
1024
+ tag: 'canvas'
1025
+ } // Render as <canvas>
1026
+ {
1027
+ tag: 'form'
1028
+ } // Render as <form>
1029
+ {
1030
+ tag: 'search'
1031
+ } // Render as <search>
1032
+ {
1033
+ tag: 'strong'
1034
+ } // Override auto-mapped tag
1035
+ ```
1036
+
1037
+ ---
1038
+
1039
+ ## `attr` Property for HTML Attributes
1040
+
1041
+ ```js
1042
+ export const Dropdown = {
1043
+ attr: { dropdown: true }, // Sets data attribute on DOM element
1044
+ tag: 'section'
1045
+ }
1046
+ ```
1047
+
1048
+ ---
1049
+
1050
+ ## `style` Property for Raw CSS
1051
+
1052
+ Use `style` for CSS that can't be expressed with props:
1053
+
1054
+ ```js
1055
+ {
1056
+ style: {
1057
+ '&:hover': { opacity: '1 !important' },
1058
+ mixBlendMode: 'luminosity',
1059
+ userSelect: 'none',
1060
+ },
1061
+ }
1062
+ ```
1063
+
1064
+ ---
1065
+
1066
+ ## `state` Property for Scoping
1067
+
1068
+ Bind a subtree to a specific state key:
1069
+
1070
+ ```js
1071
+ {
1072
+ Flex: {
1073
+ state: 'network', // This subtree reads from s.network
1074
+ Box: { ValidatorInfo: {} },
1075
+ },
1076
+ }
1077
+ ```
1078
+
1079
+ ---
1080
+
1081
+ ## Spacing Math
1082
+
1083
+ Tokens can be combined with arithmetic:
1084
+
1085
+ ```js
1086
+ padding: 'A+V2' // A plus V2
1087
+ margin: '-Y1 -Z2 - auto' // Negative values, auto
1088
+ right: 'A+V2' // Combined tokens
1089
+ margin: '- W B+V2 W' // Mix of dash (0), tokens, and math
1090
+ ```
1091
+
1092
+ ---
1093
+
1094
+ ## Color Syntax with Opacity & Lightness
1095
+
1096
+ ```js
1097
+ // Format: 'colorName opacity lightness'
1098
+ background: 'black .001' // black with 0.1% opacity
1099
+ background: 'deepFir 1 +5' // deepFir, 100% opacity, +5 lightness
1100
+ background: 'gray2 0.85 +16' // gray2, 85% opacity, +16 lightness
1101
+ background: 'env .25' // color from design system at 25% opacity
1102
+ color: 'white 0.65' // white at 65% opacity
1103
+ borderColor: 'gray3 0.65' // gray3 at 65% opacity
1104
+ boxShadow: 'black .20, 0px, 5px, 10px, 5px' // shadow with color opacity
1105
+ ```
1106
+
1107
+ ---
1108
+
1109
+ ## Border Syntax
1110
+
1111
+ ```js
1112
+ // String syntax: 'colorName size style'
1113
+ { border: 'oceanblue 1px solid' }
1114
+
1115
+ // Array syntax for individual sides
1116
+ { borderTop: ['oceanblue 0.5', '1px', 'solid'] }
1117
+
1118
+ // Individual border properties
1119
+ {
1120
+ borderWidth: '0 0 1px 0',
1121
+ borderStyle: 'solid',
1122
+ borderColor: '--theme-document-dark-background',
1123
+ }
1124
+ ```
1125
+
1126
+ ---
1127
+
1128
+ ## Shadow Syntax
1129
+
1130
+ ```js
1131
+ // Format: 'colorName x y depth offset'
1132
+ {
1133
+ shadow: 'black A A C'
1134
+ }
1135
+ {
1136
+ boxShadow: 'black .20, 0px, 5px, 10px, 5px'
1137
+ }
1138
+ ```
1139
+
1140
+ ---
1141
+
1142
+ ## Transition Syntax
1143
+
1144
+ ```js
1145
+ {
1146
+ transition: 'background, defaultBezier, A'
1147
+ }
1148
+ {
1149
+ transition: 'A defaultBezier opacity'
1150
+ }
1151
+ ```
1152
+
1153
+ ---
1154
+
1155
+ ## Real-World Examples & Patterns
1156
+
1157
+ ### Backend Fetch & API Integration
1158
+
1159
+ Functions use `this` context (bound to the calling element). Define a central fetch function and call it from other functions:
1160
+
1161
+ ```js
1162
+ // functions/fetch.js — central API wrapper
1163
+ export const fetch = async function fetch(
1164
+ method = 'GET',
1165
+ path = '',
1166
+ data,
1167
+ opts = {}
1168
+ ) {
1169
+ const options = {
1170
+ method: method || 'POST',
1171
+ headers: { 'Content-Type': 'application/json' },
1172
+ ...opts
1173
+ }
1174
+
1175
+ const ENDPOINT = 'https://api.example.com/api' + path
1176
+
1177
+ if (data && (options.method === 'POST' || options.method === 'PUT')) {
1178
+ options.body = JSON.stringify(data)
1179
+ }
1180
+
1181
+ const res = await window.fetch(ENDPOINT, options)
1182
+ if (!res.ok) {
1183
+ const errorText = await res.text()
1184
+ throw new Error(`HTTP ${res.status}: ${errorText}`)
1185
+ }
1186
+
1187
+ const contentType = res.headers.get('content-type')
1188
+ if (contentType && contentType.includes('application/json')) return res.json()
1189
+ return res.text()
1190
+ }
1191
+
1192
+ // functions/read.js — simple GET wrapper
1193
+ export const read = async function read(path) {
1194
+ return await this.call('fetch', 'GET', path)
1195
+ }
1196
+ ```
1197
+
1198
+ ### CRUD Operations Pattern
1199
+
1200
+ ```js
1201
+ // functions/add.js — create new item via form
1202
+ export const add = async function addNew(item = 'network') {
1203
+ const formData = new FormData(this.node)
1204
+ let data = Object.fromEntries(formData)
1205
+
1206
+ const res = await this.call('fetch', 'POST', '/' + item, data)
1207
+ if (!res) return
1208
+
1209
+ this.state.root.quietUpdate({ modal: null })
1210
+ this.call('router', '/item/' + res.id, this.__ref.root)
1211
+ this.node.reset()
1212
+ }
1213
+
1214
+ // functions/edit.js — update item
1215
+ export const edit = async function edit(item = 'network', protocol) {
1216
+ const formData = new FormData(this.node)
1217
+ let data = Object.fromEntries(formData)
1218
+
1219
+ const res = await this.call('fetch', 'PUT', `/${protocol}`, data)
1220
+ this.state.root.quietUpdate({ modal: null })
1221
+ this.call('router', '/network/' + protocol, this.__ref.root)
1222
+ this.node.reset()
1223
+ }
1224
+
1225
+ // functions/remove.js — delete item
1226
+ export const remove = async function remove(item = 'network', protocol) {
1227
+ const res = await this.call('fetch', 'DELETE', '/' + protocol)
1228
+ if (!res) return
1229
+ this.state.root.quietUpdate({ modal: null })
1230
+ this.call('router', '/dashboard', this.__ref.root)
1231
+ }
1232
+ ```
1233
+
1234
+ ### Data Loading & State Initialization
1235
+
1236
+ ```js
1237
+ // functions/setInitialData.js — bulk state replace without triggering extra renders
1238
+ export const setInitialData = function setInitialData(data = {}) {
1239
+ this.state.replace(data, {
1240
+ preventUpdate: true,
1241
+ preventUpdateListener: true
1242
+ })
1243
+ this.update({}, { preventUpdateListener: true })
1244
+ }
1245
+
1246
+ // Usage in a page onRender:
1247
+ onRender: (el, s) => {
1248
+ window.requestAnimationFrame(async () => {
1249
+ const fleet = await el.call('read')
1250
+ el.call('setInitialData', { fleet })
1251
+ })
1252
+ }
1253
+ ```
1254
+
1255
+ ### Authentication & Routing Guard
1256
+
1257
+ ```js
1258
+ // functions/auth.js
1259
+ export const auth = async function auth() {
1260
+ if (this.state.root.success) {
1261
+ if (window.location.pathname === '/') {
1262
+ this.call('router', '/dashboard', this.__ref.root)
1263
+ }
1264
+ } else {
1265
+ if (window.location.pathname === '/') {
1266
+ const res = await this.call('fetch', 'GET', '', null, {
1267
+ route: '/auth/me'
1268
+ })
1269
+ if (res.success) {
1270
+ this.state.root.update(res)
1271
+ this.call('router', '/dashboard', this.__ref.root)
1272
+ }
1273
+ return res
1274
+ } else {
1275
+ this.call('router', '/', this.__ref.root)
1276
+ }
1277
+ }
1278
+ }
1279
+ ```
1280
+
1281
+ ### Form Submission with Login
1282
+
1283
+ ```js
1284
+ // pages/signin.js — async form submission with error handling
1285
+ export const signin = {
1286
+ flow: 'y',
1287
+ align: 'center',
1288
+ height: '100%',
1289
+ margin: 'auto',
1290
+ LoginWindow: {
1291
+ onSubmit: async (ev, el, s) => {
1292
+ ev.preventDefault()
1293
+ s.update({ loading: true })
1294
+ const { identifier, password } = s
1295
+ try {
1296
+ const loginResult = await el.sdk.login(identifier, password)
1297
+ await el.call('initializeUserSession', { loginData: loginResult })
1298
+ el.router('/dashboard', el.getRoot())
1299
+ } catch (error) {
1300
+ el.call('openNotification', {
1301
+ title: 'Failed to sign in',
1302
+ message: error.message,
1303
+ type: 'error'
1304
+ })
1305
+ s.update({ loading: false })
1306
+ }
1307
+ }
1308
+ }
1309
+ }
1310
+ ```
1311
+
1312
+ ---
1313
+
1314
+ ### Dynamic Children from State
1315
+
1316
+ ```js
1317
+ // Table rendering rows from state
1318
+ export const Table = {
1319
+ extends: 'Flex',
1320
+ childExtends: ['NetworkRow', 'Link'], // Multiple extends as array
1321
+ width: '100%',
1322
+ children: (el, s) => s.fleet, // Dynamic children from state
1323
+ childrenAs: 'state', // Pass each item as child's state
1324
+ flow: 'y'
1325
+ }
1326
+
1327
+ // List with childProps controlling child structure
1328
+ export const ValidatorsList = {
1329
+ childrenAs: 'state',
1330
+ childProps: {
1331
+ flexFlow: 'y',
1332
+ ValidatorRow: {},
1333
+ ValidatorContent: {
1334
+ padding: 'B C3 C3',
1335
+ hide: (el, s) => !s.isActive // Conditional visibility
1336
+ }
1337
+ },
1338
+ gap: 'Z',
1339
+ flexFlow: 'y'
1340
+ }
1341
+ ```
1342
+
1343
+ ---
1344
+
1345
+ ### Modal Routing Pattern
1346
+
1347
+ Modals are rendered via root state and page paths:
1348
+
1349
+ ```js
1350
+ // Open modal — set state to page path
1351
+ onClick: (ev, el, s) => {
1352
+ s.update({ modal: '/add-network' })
1353
+ }
1354
+
1355
+ // Modal component — reads root state and renders page content
1356
+ export const Modal = {
1357
+ props: (el, s) => ({
1358
+ position: 'absolute',
1359
+ inset: '0',
1360
+ background: 'black 0.95 +15',
1361
+ backdropFilter: 'blur(3px)',
1362
+ zIndex: 99,
1363
+ ...(s.root?.modal
1364
+ ? { opacity: 1, visibility: 'visible' }
1365
+ : { opacity: 0, visibility: 'hidden' }),
1366
+ onClick: (ev, el, s) => {
1367
+ s.root.update({ modal: false })
1368
+ el.lookup('Modal').removeContent()
1369
+ }
1370
+ }),
1371
+ content: (el, s) => ({
1372
+ Box:
1373
+ (s.root.modal && {
1374
+ extend: s.root.modal,
1375
+ props: { onClick: (ev) => ev.stopPropagation() }
1376
+ }) ||
1377
+ {}
1378
+ })
1379
+ }
1380
+
1381
+ // Close modal
1382
+ this.state.root.quietUpdate({ modal: null })
1383
+ ```
1384
+
1385
+ ---
1386
+
1387
+ ### Form with Select Fields
1388
+
1389
+ ```js
1390
+ export const addNetwork = {
1391
+ extends: 'FormModal',
1392
+ tag: 'form',
1393
+ gap: 'C',
1394
+ onSubmit: async (ev, el, s) => {
1395
+ ev.preventDefault()
1396
+ await el.call('add', 'network')
1397
+ },
1398
+ Hgroup: { H: { text: 'Add Network' }, P: { text: 'Add new network' } },
1399
+ Form: {
1400
+ columns: 'repeat(2, 1fr)',
1401
+ '@mobileM': { columns: 'repeat(1, 1fr)' },
1402
+ children: () => [
1403
+ {
1404
+ gridColumn: '1 / span 2',
1405
+ Caption: { text: 'Protocol' },
1406
+ Field: {
1407
+ Input: {
1408
+ name: 'protocol',
1409
+ required: true,
1410
+ placeholder: 'E.g. Polygon',
1411
+ type: 'text'
1412
+ }
1413
+ }
1414
+ },
1415
+ {
1416
+ Caption: { text: 'Network Layer' },
1417
+ Field: {
1418
+ Input: null, // Remove default Input
1419
+ Select: {
1420
+ padding: 'A A2',
1421
+ round: 'C1',
1422
+ theme: 'field',
1423
+ Selects: {
1424
+ name: 'network_layer',
1425
+ required: true,
1426
+ children: [
1427
+ { text: 'Please select', selected: true, disabled: 'disabled' },
1428
+ { text: 'L1', value: 'L1' },
1429
+ { text: 'L2', value: 'L2' }
1430
+ ]
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ ]
1436
+ },
1437
+ Button: { text: 'Save', theme: 'primary', type: 'submit' }
1438
+ }
1439
+ ```
1440
+
1441
+ ---
1442
+
1443
+ ### Multiple Extends (Array)
1444
+
1445
+ ```js
1446
+ // Extend from multiple components
1447
+ Link: {
1448
+ extends: ['Link', 'IconButton'],
1449
+ paddingInline: 'A Z1',
1450
+ icon: 'chevron left',
1451
+ href: '/',
1452
+ }
1453
+
1454
+ // childExtends with array
1455
+ export const Table = {
1456
+ extends: 'Flex',
1457
+ childExtends: ['NetworkRow', 'Link'],
1458
+ }
1459
+ ```
1460
+
1461
+ ---
1462
+
1463
+ ### Inline childExtends (Object)
1464
+
1465
+ ```js
1466
+ export const AIThread = {
1467
+ children: (el, s) => s.thread,
1468
+ childrenAs: 'state',
1469
+ childExtends: {
1470
+ props: (el, s) => ({
1471
+ alignSelf: s.role === 'user' ? 'start' : 'end',
1472
+ onRender: (el) => {
1473
+ const t = setTimeout(() => {
1474
+ el.node.scrollIntoView()
1475
+ clearTimeout(t)
1476
+ }, 35)
1477
+ }
1478
+ }),
1479
+ content: (el, s) => {
1480
+ if (s.role === 'user') {
1481
+ return {
1482
+ extends: 'AIMessage',
1483
+ shape: 'bubble',
1484
+ round: 'X C C C',
1485
+ Message: { html: (el, s) => s.message }
1486
+ }
1487
+ }
1488
+ return { extends: 'P', color: 'paragraph', html: (el, s) => s.message }
1489
+ }
1490
+ }
1491
+ }
1492
+ ```
1493
+
1494
+ ---
1495
+
1496
+ ### Suffix Naming for Multiple Same-Type Children
1497
+
1498
+ Use underscore suffix to create multiple instances of the same component:
1499
+
1500
+ ```js
1501
+ {
1502
+ NavButton: { text: 'All Networks', icon: 'chevron left', href: '/' },
1503
+ NavButton_add: { // Same component, different instance
1504
+ icon: 'plus',
1505
+ text: 'Add node',
1506
+ margin: '- - - auto',
1507
+ onClick: (ev, el, s) => { s.root.update({ modal: '/add-node' }) },
1508
+ },
1509
+ IconButton_add: { theme: 'button', icon: 'moreVertical' },
1510
+ Input_trigger: { visibility: 'hidden' },
1511
+ }
1512
+ ```
1513
+
1514
+ ---
1515
+
1516
+ ### Grid Layout Component
1517
+
1518
+ ```js
1519
+ export const NetworkRow = {
1520
+ extends: 'Grid',
1521
+ templateColumns: '3fr 3fr 3fr 2fr 2fr',
1522
+ gap: 'Z2',
1523
+ href: (el, s) => '/network/' + s.protocol,
1524
+ align: 'center',
1525
+ padding: 'A1 A2',
1526
+ childProps: { gap: 'Z2', flexAlign: 'center' },
1527
+ borderWidth: '0 0 1px 0',
1528
+ borderStyle: 'solid',
1529
+ borderColor: '--theme-document-dark-background',
1530
+ cursor: 'pointer',
1531
+ transition: 'background, defaultBezier, A',
1532
+ ':hover': { background: 'deepFir' },
1533
+ ':active': { background: 'deepFir 1 +5' },
1534
+ Name: {
1535
+ Avatar: { src: '{{ protocol }}.png', boxSize: 'B1' },
1536
+ Title: { tag: 'strong', text: (el, s) => s.protocol }
1537
+ },
1538
+ Env: {
1539
+ childExtends: 'NetworkRowLabel',
1540
+ childProps: { background: 'env .25' },
1541
+ children: (el, s) => s.parsed?.env
1542
+ }
1543
+ }
1544
+ ```
1545
+
1546
+ ---
1547
+
1548
+ ### External Dependencies & Chart.js Integration
1549
+
1550
+ ```js
1551
+ // dependencies.js — declare external packages with fixed versions
1552
+ export default { 'chart.js': '4.4.9' }
1553
+
1554
+ // Chart component using canvas tag with onRender cleanup
1555
+ export const Chart = {
1556
+ tag: 'canvas',
1557
+ minWidth: 'G',
1558
+ minHeight: 'D',
1559
+ onRender: async (el, s) => {
1560
+ const { Chart } = await import('chart.js')
1561
+ const ctx = el.node.getContext('2d')
1562
+
1563
+ const chart = new Chart(ctx, {
1564
+ type: 'line',
1565
+ data: {
1566
+ /* chart data */
1567
+ },
1568
+ options: { responsive: true, maintainAspectRatio: false }
1569
+ })
1570
+
1571
+ const interval = setInterval(() => {
1572
+ chart.update('none')
1573
+ }, 1100)
1574
+
1575
+ // Return cleanup function — called when element is removed
1576
+ return () => {
1577
+ clearInterval(interval)
1578
+ chart.destroy()
1579
+ }
1580
+ }
1581
+ }
1582
+ ```
1583
+
1584
+ ---
1585
+
1586
+ ### onRender Cleanup Function
1587
+
1588
+ When `onRender` returns a function, it's called when the element is removed:
1589
+
1590
+ ```js
1591
+ {
1592
+ onRender: (el, s) => {
1593
+ const interval = setInterval(() => {
1594
+ /* ... */
1595
+ }, 1000)
1596
+ const handler = (e) => {
1597
+ /* ... */
1598
+ }
1599
+ window.addEventListener('resize', handler)
1600
+
1601
+ // Cleanup function
1602
+ return () => {
1603
+ clearInterval(interval)
1604
+ window.removeEventListener('resize', handler)
1605
+ }
1606
+ }
1607
+ }
1608
+ ```
1609
+
1610
+ ---
1611
+
1612
+ ### Lookup Pattern
1613
+
1614
+ ```js
1615
+ // Lookup by key name
1616
+ const Dropdown = element.lookup('Dropdown')
1617
+
1618
+ // Lookup by predicate function
1619
+ const Dropdown = element.lookup((el) => el.props.isDropdownRoot)
1620
+
1621
+ // Lookup for ancestor navigation
1622
+ const modal = el.lookup('Modal')
1623
+ modal.removeContent()
1624
+ ```
1625
+
1626
+ ---
1627
+
1628
+ ### Dropdown Pattern
1629
+
1630
+ ```js
1631
+ {
1632
+ extends: 'DropdownParent',
1633
+ Button: {
1634
+ text: (el, s) => s.isUpdate ? 'Updates' : 'All validators',
1635
+ theme: 'transparent',
1636
+ Icon: { name: 'chevronDown', order: 2 },
1637
+ },
1638
+ Dropdown: {
1639
+ left: '-X',
1640
+ backdropFilter: 'blur(3px)',
1641
+ background: 'softBlack .9 +65',
1642
+ childExtends: 'Button',
1643
+ childProps: { theme: 'transparent', align: 'start', padding: 'Z2 A' },
1644
+ flexFlow: 'y',
1645
+ },
1646
+ }
1647
+ ```
1648
+
1649
+ ---
1650
+
1651
+ ### AI Chat Thread Pattern
1652
+
1653
+ ```js
1654
+ export const Prompt = {
1655
+ state: { keyword: '', thread: [], images: [] },
1656
+ tag: 'form',
1657
+ onSubmit: (ev, el, s) => {
1658
+ ev.preventDefault()
1659
+ const value = s.keyword
1660
+ if (!value) return
1661
+
1662
+ s.apply((s) => {
1663
+ s.thread.push({ role: 'user', message: value })
1664
+ s.keyword = ''
1665
+ })
1666
+
1667
+ el.Relative.Textarea.value = '' // Direct DOM access for form reset
1668
+
1669
+ setTimeout(async () => {
1670
+ const res = await el.call('giveMeAnswer', value)
1671
+ s.apply((s) => {
1672
+ s.thread.push({ role: 'agent', message: res.summary })
1673
+ })
1674
+ }, 1000)
1675
+ },
1676
+ Relative: {
1677
+ Textarea: {
1678
+ placeholder: '"Ask, Search, Prompt..."',
1679
+ onInput: (ev, el, s) => {
1680
+ let prompt = el.node.value.trim()
1681
+ s.replace({ keyword: prompt })
1682
+ },
1683
+ onKeydown: (ev, el, s) => {
1684
+ if (ev.key === 'Enter' && !ev.shiftKey) ev.preventDefault()
1685
+ if (ev.key === 'Escape') {
1686
+ ev.stopPropagation()
1687
+ el.node.blur()
1688
+ }
1689
+ }
1690
+ },
1691
+ Submit: { extends: 'SquareButton', type: 'submit', icon: 'check' }
1692
+ },
1693
+ AIThread: { hide: (el, s) => !s.thread.length }
1694
+ }
1695
+ ```
1696
+
1697
+ ---
1698
+
1699
+ ### Uptime Visualization (Generated Children)
1700
+
1701
+ ```js
1702
+ export const Uptime = {
1703
+ extends: 'Flex',
1704
+ flow: 'y',
1705
+ gap: 'Z',
1706
+ Title: { fontSize: 'Z', text: 'Uptime', order: -1 },
1707
+ Flex: {
1708
+ gap: 'X',
1709
+ flow: 'row wrap',
1710
+ height: '2.8em',
1711
+ overflow: 'hidden',
1712
+ children: (el, s) => new Array(300).fill({}), // Generate 300 empty children
1713
+ childProps: {
1714
+ minWidth: 'Z1',
1715
+ boxSize: 'Z1',
1716
+ background: 'green .3',
1717
+ border: '1px, solid, green',
1718
+ round: 'W'
1719
+ },
1720
+ ':hover > div': { opacity: 0.5 }
1721
+ }
1722
+ }
1723
+ ```
1724
+
1725
+ ---
1726
+
1727
+ ### Component Extending Base Symbols
1728
+
1729
+ ```js
1730
+ export const H1 = {
1731
+ extends: 'smbls.H1',
1732
+ color: 'title'
1733
+ }
1734
+ ```
1735
+
1736
+ ---
1737
+
1738
+ ## Creating Custom Properties (Transformers)
1739
+
1740
+ ```js
1741
+ // Define a custom property transformer
1742
+ export default function paddingEm(val, element, state, context) {
1743
+ const { unit } = element.props
1744
+ const paddingEm = unit === 'em' ? val / 16 + 'em' : val
1745
+ return { paddingEm }
1746
+ }
1747
+
1748
+ // Use in components
1749
+ {
1750
+ paddingEm: 12
1751
+ }
1752
+ ```
1753
+
1754
+ ---
1755
+
1756
+ ## CLI & Syncing
1757
+
1758
+ ```sh
1759
+ # One-time fetch from platform
1760
+ smbls fetch
1761
+
1762
+ # One-time push to platform
1763
+ smbls push
1764
+
1765
+ # Listen for changes (continuous sync)
1766
+ smbls sync
1767
+ ```
1768
+
1769
+ ### symbols.json Configuration
1770
+
1771
+ ```json
1772
+ {
1773
+ "key": "your-project-key",
1774
+ "distDir": "./smbls",
1775
+ "framework": "symbols",
1776
+ "packageManager": "npm"
1777
+ }
1778
+ ```
1779
+
1780
+ `distDir` points to the folder where Symbols CLI creates files. If not present, Symbols uses a temporary file.
1781
+
1782
+ ---
1783
+
1784
+ ## Design System Articles Reference
1785
+
1786
+ ### Font Configuration
1787
+
1788
+ ```js
1789
+ // Font properties: url, fontWeight, fontStretch, isVariable
1790
+ export default {
1791
+ FONT: {
1792
+ Inter: {
1793
+ url: 'https://fonts.googleapis.com/css2?family=Inter:wght@100..900',
1794
+ isVariable: true,
1795
+ fontWeight: '100 900'
1796
+ }
1797
+ },
1798
+ FONT_FAMILY: {
1799
+ primary: 'Inter, sans-serif'
1800
+ }
1801
+ }
1802
+ ```
1803
+
1804
+ ### Grid Configuration
1805
+
1806
+ ```js
1807
+ export default {
1808
+ GRID: {
1809
+ columns: 12,
1810
+ gutter: 'A',
1811
+ maxWidth: '1200px'
1812
+ }
1813
+ }
1814
+ ```
1815
+
1816
+ ### Icon System
1817
+
1818
+ ```js
1819
+ // Icons are defined as SVG strings with camelCase keys
1820
+ export default {
1821
+ ICONS: {
1822
+ arrowDown: '<svg>...</svg>',
1823
+ chevronLeft: '<svg>...</svg>',
1824
+ search: '<svg>...</svg>'
1825
+ }
1826
+ }
1827
+ ```
1828
+
1829
+ ### Media Query Configuration
1830
+
1831
+ ```js
1832
+ // Media queries are responsive breakpoints
1833
+ export default {
1834
+ MEDIA: {
1835
+ mobileXS: '320px',
1836
+ mobileS: '375px',
1837
+ mobileM: '425px',
1838
+ tablet: '768px',
1839
+ tabletM: '960px',
1840
+ laptop: '1024px',
1841
+ desktop: '1440px'
1842
+ }
1843
+ }
1844
+ ```
1845
+
1846
+ ### Shape Configuration
1847
+
1848
+ ```js
1849
+ // Shapes use CSS clip-path for custom shapes
1850
+ export default {
1851
+ SHAPE: {
1852
+ tag: {
1853
+ /* clip-path polygon */
1854
+ },
1855
+ tooltip: {
1856
+ /* clip-path with direction modifiers */
1857
+ },
1858
+ bubble: {
1859
+ /* rounded bubble shape */
1860
+ }
1861
+ }
1862
+ }
1863
+ ```
1864
+
1865
+ ### Animation Configuration
1866
+
1867
+ ```js
1868
+ // Keyframe animations defined in design system
1869
+ export default {
1870
+ ANIMATION: {
1871
+ fadeIn: {
1872
+ from: { opacity: 0 },
1873
+ to: { opacity: 1 }
1874
+ },
1875
+ shake: {
1876
+ '0%': { transform: 'translateX(0)' },
1877
+ '25%': { transform: 'translateX(-5px)' },
1878
+ '50%': { transform: 'translateX(5px)' },
1879
+ '75%': { transform: 'translateX(-5px)' },
1880
+ '100%': { transform: 'translateX(0)' }
1881
+ }
1882
+ }
1883
+ }
1884
+ ```
1885
+
1886
+ ### Timing Configuration
1887
+
1888
+ ```js
1889
+ // Timing sequence for animation durations and bezier curves
1890
+ export default {
1891
+ TIMING: {
1892
+ base: 300,
1893
+ ratio: 1.618,
1894
+ // Custom bezier curves
1895
+ defaultBezier: 'cubic-bezier(.19,.22,.4,.86)'
1896
+ }
1897
+ }
1898
+ ```
1899
+
1900
+ ### Unit Configuration
1901
+
1902
+ ```js
1903
+ // CSS unit configuration
1904
+ export default {
1905
+ // Unit options: 'em', 'px', 'vmax'
1906
+ unit: 'em'
1907
+ }
1908
+ ```
1909
+
1910
+ ---
1911
+
1912
+ ## DO's and DON'Ts
1913
+
1914
+ ### DO:
1915
+
1916
+ - Use `extends` and `childExtends` (v3 plural form)
1917
+ - Flatten all props directly into the component object
1918
+ - Use `onEventName` prefix for all events
1919
+ - Reference components by PascalCase key name in the tree
1920
+ - Use `el.call('functionName')` for global utilities
1921
+ - Use `el.scope.fn()` for local helpers
1922
+ - Use design system tokens (`padding: 'A'`, `color: 'primary'`)
1923
+ - Keep all folders completely flat
1924
+ - One export per file, name matches filename
1925
+ - Components are always plain objects
1926
+ - Use `if` for conditional rendering, `hide` for conditional visibility
1927
+
1928
+ ### DON'T:
1929
+
1930
+ - Use `extend` or `childExtend` (v2 singular — BANNED)
1931
+ - Use `props: { }` wrapper (v2 — BANNED)
1932
+ - Use `on: { }` wrapper (v2 — BANNED)
1933
+ - Import components/functions between project files
1934
+ - Create function-based components
1935
+ - Create subfolders inside any folder
1936
+ - Use default exports for components (use named exports)
1937
+ - Mix framework-specific code (React, Vue, etc.)
1938
+ - Hardcode styles — use design tokens
1939
+ - Add top-level `import`/`require` for project files
1940
+ - Nest properties inside @media queries (replace at same level only)
1941
+
1942
+ ---
1943
+
1944
+ ## CRITICAL: Icon Rendering in Buttons (Verified Patterns)
1945
+
1946
+ This section documents **verified working patterns** for rendering icons. Incorrect patterns cause silent failures where buttons render as empty shapes.
1947
+
1948
+ ### The `Icon` Component Limitation
1949
+
1950
+ The `Icon` component (sprite-based) **does NOT render** when nested inside `Button` or `Flex+tag:button` elements. This is a known limitation.
1951
+
1952
+ ```js
1953
+ // BROKEN — Icon will NOT render inside Button or Flex+tag:button
1954
+ MyBtn: {
1955
+ extends: 'Button',
1956
+ Icon: { extends: 'Icon', name: 'heart' } // ❌ Silent failure
1957
+ }
1958
+
1959
+ MyBtn: {
1960
+ extends: 'Flex',
1961
+ tag: 'button',
1962
+ Icon: { extends: 'Icon', name: 'heart' } // ❌ Silent failure
1963
+ }
1964
+ ```
1965
+
1966
+ ### CORRECT: Use `Svg` Atom with `html` Prop
1967
+
1968
+ The `Svg` atom with the `html` prop is the **only reliable way** to render icons inside button-like elements. The child key **must be named `Svg`** (not `FlameIcon`, `MyIcon`, etc.) for auto-resolution to work.
1969
+
1970
+ ```js
1971
+ // CORRECT — Svg atom with html prop inside Flex+tag:button
1972
+ MyBtn: {
1973
+ extends: 'Flex',
1974
+ tag: 'button',
1975
+ flexAlign: 'center center',
1976
+ cursor: 'pointer',
1977
+ background: 'transparent',
1978
+ border: 'none',
1979
+
1980
+ Svg: {
1981
+ viewBox: '0 0 24 24',
1982
+ width: '22',
1983
+ height: '22',
1984
+ color: 'flame',
1985
+ html: '<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" fill="currentColor"/>'
1986
+ }
1987
+ }
1988
+ ```
1989
+
1990
+ ### `html` Prop Only Works on `Svg` Atom
1991
+
1992
+ The `html` prop sets `innerHTML` **only on the `Svg` atom**. It does NOT work on `Box`, `Flex`, or `Button`.
1993
+
1994
+ ```js
1995
+ // BROKEN — html prop ignored on Flex/Box/Button
1996
+ { extends: 'Flex', html: '<svg>...</svg>' } // ❌
1997
+
1998
+ // CORRECT — html prop works on Svg atom
1999
+ { Svg: { viewBox: '0 0 24 24', html: '<path .../>' } } // ✅
2000
+ ```
2001
+
2002
+ ### Standalone Svg (not inside a button)
2003
+
2004
+ When using `Svg` as a standalone element (e.g., logo icon in a header), the key name must be `Svg` or use `extends: 'Svg'`:
2005
+
2006
+ ```js
2007
+ LogoArea: {
2008
+ extends: 'Flex',
2009
+ flexAlign: 'center center',
2010
+ gap: 'Z',
2011
+
2012
+ Svg: { // Key IS 'Svg' — auto-resolves
2013
+ viewBox: '0 0 24 24',
2014
+ width: '22', height: '22',
2015
+ color: 'flame',
2016
+ html: '<path d="..." fill="currentColor"/>'
2017
+ },
2018
+
2019
+ LogoText: { extends: 'Text', text: 'myapp' }
2020
+ }
2021
+ ```
2022
+
2023
+ ### Nav Tabs with Icon + Label
2024
+
2025
+ For navigation tabs that need both an icon and a text label:
2026
+
2027
+ ```js
2028
+ NavItem: {
2029
+ extends: 'Flex',
2030
+ tag: 'button',
2031
+ flow: 'y',
2032
+ flexAlign: 'center center',
2033
+ gap: 'Y',
2034
+ flex: 1,
2035
+ background: 'transparent',
2036
+ border: 'none',
2037
+ cursor: 'pointer'
2038
+ },
2039
+
2040
+ DiscoverTab: {
2041
+ extends: 'NavItem',
2042
+ Svg: {
2043
+ viewBox: '0 0 24 24', width: '22', height: '22', color: 'flame',
2044
+ html: '<path d="..." fill="currentColor"/>'
2045
+ },
2046
+ Label: { extends: 'Text', text: 'Discover', fontSize: 'X', color: 'flame' }
2047
+ }
2048
+ ```
2049
+
2050
+ ---
2051
+
2052
+ ## CRITICAL: `el.call()` Function Context
2053
+
2054
+ When a function is called via `el.call('functionName', arg1, arg2)`:
2055
+
2056
+ - The **DOMQL element** is passed as `this` inside the function — NOT as the first argument
2057
+ - `arg1`, `arg2` etc. are the additional arguments
2058
+ - Access the DOM node via `this.node` (not `this.__node`)
2059
+
2060
+ ```js
2061
+ // functions/myFunction.js
2062
+ export const myFunction = function myFunction (arg1) {
2063
+ const el = this // 'this' IS the DOMQL element
2064
+ const node = this.node // DOM node
2065
+ // arg1 is the first argument passed to el.call('myFunction', arg1)
2066
+ }
2067
+
2068
+ // In component — call without passing el as argument
2069
+ onClick: (e, el) => el.call('myFunction', someArg) // ✅ correct
2070
+ onClick: (e, el) => el.call('myFunction', el, someArg) // ❌ wrong — el passed twice
2071
+ ```
2072
+
2073
+ ### Preventing Double Initialization in `onRender`
2074
+
2075
+ `onRender` fires on every render cycle. Use a guard flag to run imperative logic only once:
2076
+
2077
+ ```js
2078
+ CardStack: {
2079
+ extends: 'Box',
2080
+ flex: 1,
2081
+ position: 'relative',
2082
+
2083
+ onRender: (el) => {
2084
+ if (el.__initialized) return // Guard against double-init
2085
+ el.__initialized = true
2086
+ el.call('initMyLogic')
2087
+ }
2088
+ }
2089
+ ```
2090
+
2091
+ ### Accessing DOM Node in Functions
2092
+
2093
+ ```js
2094
+ export const initMyLogic = function initMyLogic () {
2095
+ const node = this.node // ✅ correct
2096
+ if (!node || !node.appendChild) return // Guard for safety
2097
+
2098
+ // Imperative DOM manipulation
2099
+ node.innerHTML = ''
2100
+ const div = document.createElement('div')
2101
+ node.appendChild(div)
2102
+ }
2103
+ ```
2104
+
2105
+ ---
2106
+
2107
+ ## CRITICAL: Flex Layout Properties
2108
+
2109
+ Use `flexAlign` (not `align`) for combined alignItems + justifyContent shorthand. Use `flow` (not `flexFlow`) for direction shorthand.
2110
+
2111
+ ```js
2112
+ // CORRECT
2113
+ { extends: 'Flex', flexAlign: 'center center', flow: 'y' }
2114
+
2115
+ // Also valid (explicit)
2116
+ { extends: 'Flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }
2117
+
2118
+ // WRONG — 'align' is not a valid shorthand in v3
2119
+ { extends: 'Flex', align: 'center center' } // ❌ has no effect
2120
+ ```
2121
+
2122
+ ---
2123
+
2124
+ ## CRITICAL: Tab/View Switching with DOM IDs
2125
+
2126
+ For multi-tab layouts where views are shown/hidden imperatively, assign `id` props to views and tabs, then use `document.getElementById` in switch functions:
2127
+
2128
+ ```js
2129
+ // In page definition — assign ids
2130
+ ContentArea: {
2131
+ DiscoverView: { id: 'view-discover', position: 'absolute', inset: 0, display: 'flex' },
2132
+ MessagesView: { id: 'view-messages', position: 'absolute', inset: 0, display: 'none' }
2133
+ },
2134
+
2135
+ BottomNav: {
2136
+ DiscoverTab: {
2137
+ id: 'tab-discover',
2138
+ onClick: (e, el) => el.call('switchTab', 'discover')
2139
+ }
2140
+ }
2141
+
2142
+ // functions/switchTab.js
2143
+ export const switchTab = function switchTab (tab) {
2144
+ const views = ['discover', 'messages', 'likes', 'profile']
2145
+ views.forEach(v => {
2146
+ const viewEl = document.getElementById('view-' + v)
2147
+ const navEl = document.getElementById('tab-' + v)
2148
+ if (viewEl) viewEl.style.display = v === tab ? 'flex' : 'none'
2149
+ if (navEl) {
2150
+ const svg = navEl.querySelector('svg')
2151
+ const span = navEl.querySelector('span')
2152
+ const color = v === tab ? '#FF4458' : '#777'
2153
+ if (svg) svg.style.color = color
2154
+ if (span) span.style.color = color
2155
+ }
2156
+ })
2157
+ }
2158
+ ```