@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.
- package/.env.example +16 -0
- package/.env.railway +13 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/mcp.json +57 -0
- package/package.json +20 -0
- package/pyproject.toml +25 -0
- package/railway.toml +26 -0
- package/run.sh +17 -0
- package/symbols_mcp/__init__.py +1 -0
- package/symbols_mcp/server.py +1114 -0
- package/symbols_mcp/skills/ACCESSIBILITY.md +471 -0
- package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +70 -0
- package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +257 -0
- package/symbols_mcp/skills/BRAND_INDENTITY.md +69 -0
- package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +304 -0
- package/symbols_mcp/skills/CLAUDE.md +2158 -0
- package/symbols_mcp/skills/CLI_QUICK_START.md +205 -0
- package/symbols_mcp/skills/DESIGN_CRITIQUE.md +64 -0
- package/symbols_mcp/skills/DESIGN_DIRECTION.md +320 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +64 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +487 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +136 -0
- package/symbols_mcp/skills/DESIGN_TO_CODE.md +64 -0
- package/symbols_mcp/skills/DESIGN_TREND.md +50 -0
- package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +236 -0
- package/symbols_mcp/skills/FIGMA_MATCHING.md +63 -0
- package/symbols_mcp/skills/GARY_TAN.md +80 -0
- package/symbols_mcp/skills/MARKETING_ASSETS.md +66 -0
- package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +614 -0
- package/symbols_mcp/skills/QUICKSTART.md +79 -0
- package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +1405 -0
- package/symbols_mcp/skills/THE_PRESENTATION.md +69 -0
- package/symbols_mcp/skills/UI_UX_PATTERNS.md +68 -0
- 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
|
+
```
|