@symbo.ls/mcp 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/symbols_mcp/skills/COMPONENTS.md +421 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM.md +430 -0
- package/symbols_mcp/skills/MIGRATION.md +520 -0
- package/symbols_mcp/skills/PATTERNS.md +509 -0
- package/symbols_mcp/skills/PROJECT_STRUCTURE.md +400 -0
- package/symbols_mcp/skills/RULES.md +488 -0
- package/symbols_mcp/skills/SEO-METADATA.md +41 -1
- package/symbols_mcp/skills/SYNTAX.md +777 -0
- package/symbols_mcp/skills/ACCESSIBILITY.md +0 -471
- package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +0 -70
- package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +0 -265
- package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +0 -304
- package/symbols_mcp/skills/CLAUDE.md +0 -2158
- package/symbols_mcp/skills/CLI_QUICK_START.md +0 -205
- package/symbols_mcp/skills/DEFAULT_COMPONENTS.md +0 -2002
- package/symbols_mcp/skills/DEFAULT_DESIGN_SYSTEM.md +0 -496
- package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +0 -487
- package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +0 -136
- package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +0 -236
- package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +0 -634
- package/symbols_mcp/skills/OPTIMIZATIONS_FOR_AGENT.md +0 -253
- package/symbols_mcp/skills/PROJECT_SETUP.md +0 -217
- package/symbols_mcp/skills/QUICKSTART.md +0 -79
- package/symbols_mcp/skills/REMOTE_PREVIEW.md +0 -144
- package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +0 -1405
- package/symbols_mcp/skills/UI_UX_PATTERNS.md +0 -68
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
# DOMQL v3 Syntax — Complete Language Reference
|
|
2
|
+
|
|
3
|
+
Every pattern here is derived from real working code. Use this as the authoritative reference for writing correct DOMQL v3.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Element Anatomy
|
|
8
|
+
|
|
9
|
+
A DOMQL element is a plain JS object. Every key has a specific meaning:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
export const MyCard = {
|
|
13
|
+
// ── Composition ────────────────────────────────────────────────────
|
|
14
|
+
extends: 'Flex', // extend from registered component(s)
|
|
15
|
+
|
|
16
|
+
// ── DOM ────────────────────────────────────────────────────────────
|
|
17
|
+
tag: 'section', // HTML tag (default: div)
|
|
18
|
+
|
|
19
|
+
// ── CSS props (top-level → promoted to props via propertizeElement) ─
|
|
20
|
+
padding: 'B C', // design-token value
|
|
21
|
+
gap: 'A',
|
|
22
|
+
flow: 'column',
|
|
23
|
+
theme: 'dialog',
|
|
24
|
+
round: 'C',
|
|
25
|
+
|
|
26
|
+
// ── HTML attributes ────────────────────────────────────────────────
|
|
27
|
+
attr: {
|
|
28
|
+
role: 'region',
|
|
29
|
+
'aria-label': ({ props }) => props.label
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// ── State ──────────────────────────────────────────────────────────
|
|
33
|
+
state: { open: false },
|
|
34
|
+
|
|
35
|
+
// ── Events (v3 style) ──────────────────────────────────────────────
|
|
36
|
+
onClick: (event, el, state) => { state.update({ open: !state.open }) },
|
|
37
|
+
onRender: (el, state) => { console.log('rendered') },
|
|
38
|
+
|
|
39
|
+
// ── Child elements ─────────────────────────────────────────────────
|
|
40
|
+
Header: {
|
|
41
|
+
extends: 'Flex',
|
|
42
|
+
text: ({ props }) => props.title
|
|
43
|
+
},
|
|
44
|
+
Body: {
|
|
45
|
+
html: ({ props }) => props.content
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 2. Element Lifecycle (internal flow)
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
create(props, parent, key, options)
|
|
56
|
+
├── createElement() creates bare element
|
|
57
|
+
├── applyExtends() merges extends stack (element wins over extends)
|
|
58
|
+
├── propertizeElement() routes onXxx events, promotes CSS props
|
|
59
|
+
├── addMethods() attaches el.lookup / el.update / etc.
|
|
60
|
+
├── initProps() builds props from propsStack
|
|
61
|
+
└── createNode()
|
|
62
|
+
├── throughInitialExec() executes function props
|
|
63
|
+
├── applyEventsOnNode() binds element.on.* → DOM addEventListener
|
|
64
|
+
└── iterates children → create() recursively
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Critical ordering**: `propertizeElement` runs BEFORE `addMethods`. Do not rely on prototype methods in `propertizeElement`.
|
|
68
|
+
|
|
69
|
+
### Propertization and the Define System
|
|
70
|
+
|
|
71
|
+
`propertizeElement()` classifies keys between the element root and `props`. Keys starting with `$` overlap between css-in-props conditionals (`$isActive`) and define handlers (built-in `$router`, plus deprecated v2 handlers like `$propsCollection`, `$collection` in older projects).
|
|
72
|
+
|
|
73
|
+
**Rule:** Keys with a matching define handler (`element.define[key]` or `context.define[key]`) must stay at the element root so `throughInitialDefine` can process them. This is critical for `$router` and for backwards compatibility with older projects. The propertization checks for define handlers BEFORE applying `CSS_SELECTOR_PREFIXES`:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// Check define handlers first — these stay at root
|
|
77
|
+
const defineValue = this.define?.[key]
|
|
78
|
+
const globalDefineValue = this.context?.define?.[key]
|
|
79
|
+
if (isFunction(defineValue) || isFunction(globalDefineValue)) continue
|
|
80
|
+
|
|
81
|
+
// Only then apply prefix-based classification
|
|
82
|
+
if (CSS_SELECTOR_PREFIXES.has(firstChar)) {
|
|
83
|
+
obj.props[key] = value // move to props for css-in-props
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 3. REGISTRY Keys (handled by DOMQL, NOT promoted to CSS props)
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
attr, style, text, html, data, classlist, state, scope, deps,
|
|
93
|
+
extends, children, content,
|
|
94
|
+
childExtend (deprecated), childExtends, childExtendRecursive (deprecated), childExtendsRecursive,
|
|
95
|
+
props, if, define, __name, __ref, __hash, __text,
|
|
96
|
+
key, tag, query, parent, node, variables, on, component, context
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Any key NOT in this list and not a component name (uppercase first) → promoted to `element.props` as a CSS prop.
|
|
100
|
+
|
|
101
|
+
> **v3 note:** Use `childExtends` (plural) — `childExtend` (singular) is deprecated v2 syntax. The singular forms still exist in REGISTRY for backwards compatibility with older projects, but new code should always use the plural form.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 4. Extending & Composing
|
|
106
|
+
|
|
107
|
+
### Single extend
|
|
108
|
+
```js
|
|
109
|
+
export const PrimaryButton = { extends: 'Button' }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Multiple extends — order matters (first = highest priority)
|
|
113
|
+
```js
|
|
114
|
+
export const RouteLink = { extends: [Link, RouterLink] }
|
|
115
|
+
export const Button = { extends: ['IconText', 'FocusableComponent'] }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### String reference (resolved from `context.components`)
|
|
119
|
+
```js
|
|
120
|
+
export const MyItem = { extends: 'Hoverable' }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Merge semantics
|
|
124
|
+
- Element's own properties **always win** over extends properties
|
|
125
|
+
- Objects are deep-merged (both sides preserved)
|
|
126
|
+
- Functions are NOT merged — element's function replaces extend's
|
|
127
|
+
- Arrays are concatenated
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 5. CSS Props — Top-Level Promotion
|
|
132
|
+
|
|
133
|
+
Top-level non-registry, non-component keys become `element.props`:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
export const Card = {
|
|
137
|
+
padding: 'B C', // → props.padding
|
|
138
|
+
gap: 'Z', // → props.gap
|
|
139
|
+
flow: 'column', // → props.flow (shorthand for flexDirection)
|
|
140
|
+
align: 'center', // → props.align (NOT flexAlign — see RULES.md)
|
|
141
|
+
fontSize: 'A', // → props.fontSize
|
|
142
|
+
fontWeight: '500', // → props.fontWeight
|
|
143
|
+
color: 'currentColor', // → props.color
|
|
144
|
+
background: 'codGray', // → props.background
|
|
145
|
+
round: 'C', // → props.round (border-radius token)
|
|
146
|
+
opacity: '0.85',
|
|
147
|
+
overflow: 'hidden',
|
|
148
|
+
transition: 'B defaultBezier',
|
|
149
|
+
transitionProperty: 'opacity, transform',
|
|
150
|
+
zIndex: 10,
|
|
151
|
+
tag: 'section', // stays at root (in REGISTRY)
|
|
152
|
+
attr: { href: ... } // stays at root (in REGISTRY)
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Pseudo-classes and pseudo-elements
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
export const Hoverable = {
|
|
160
|
+
opacity: 0.85,
|
|
161
|
+
':hover': { opacity: 0.9, transform: 'scale(1.015)' },
|
|
162
|
+
':active': { opacity: 1, transform: 'scale(1.015)' },
|
|
163
|
+
':focus-visible': { outline: 'solid X blue.3' },
|
|
164
|
+
':not(:first-child)': {
|
|
165
|
+
'@dark': { borderWidth: '1px 0 0' },
|
|
166
|
+
'@light': { borderWidth: '1px 0 0' }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### CSS class state modifiers (Emotion `.className`)
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
export const Item = {
|
|
175
|
+
opacity: 0.6,
|
|
176
|
+
'.active': { opacity: 1, fontWeight: '600' },
|
|
177
|
+
'.disabled': { opacity: 0.3, pointerEvents: 'none' },
|
|
178
|
+
'.hidden': { transform: 'translate3d(0,10%,0)', opacity: 0, visibility: 'hidden' }
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Raw style object (escape hatch)
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
export const DropdownParent = {
|
|
186
|
+
style: {
|
|
187
|
+
'&:hover': {
|
|
188
|
+
zIndex: 1000,
|
|
189
|
+
'& [dropdown]': { transform: 'translate3d(0,0,0)', opacity: 1 }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Media queries (responsive)
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
export const Grid = {
|
|
199
|
+
columns: 'repeat(4, 1fr)',
|
|
200
|
+
'@tabletSm': { columns: 'repeat(2, 1fr)' },
|
|
201
|
+
'@mobileL': { columns: '1fr' },
|
|
202
|
+
'@dark': { background: 'codGray' },
|
|
203
|
+
'@light': { background: 'concrete' }
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 6. Events
|
|
210
|
+
|
|
211
|
+
### v3 syntax — top-level `onXxx` (preferred)
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
export const MyForm = {
|
|
215
|
+
// DOM events — signature: (event, el, state)
|
|
216
|
+
onClick: (event, el, state) => { /* ... */ },
|
|
217
|
+
onChange: (event, el, state) => { /* ... */ },
|
|
218
|
+
onInput: (event, el, state) => { state.update({ value: event.target.value }) },
|
|
219
|
+
onSubmit: (event, el, state) => { event.preventDefault(); /* ... */ },
|
|
220
|
+
onKeydown: (event, el, state) => { if (event.key === 'Enter') /* ... */ },
|
|
221
|
+
onMouseover:(event, el, state) => { /* ... */ },
|
|
222
|
+
onBlur: (event, el, state) => { /* ... */ },
|
|
223
|
+
onFocus: (event, el, state) => { /* ... */ },
|
|
224
|
+
|
|
225
|
+
// DOMQL lifecycle events — signature: (el, state)
|
|
226
|
+
onRender: (el, state) => { /* after element renders */ },
|
|
227
|
+
onInit: (el, state) => { /* before render */ },
|
|
228
|
+
onUpdate: (el, state) => { /* after state/props update */ },
|
|
229
|
+
onStateUpdate: (el, state) => { /* specifically after state update */ },
|
|
230
|
+
onCreate: (el, state) => { /* after full creation */ }
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### DOMQL lifecycle events (never bound to DOM)
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
init, beforeClassAssign, render, renderRouter, attachNode,
|
|
238
|
+
stateInit, stateCreated, beforeStateUpdate, stateUpdate,
|
|
239
|
+
beforeUpdate, done, create, complete, frame, update
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Event detection rule (v3)
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
// A key is a v3 event handler if:
|
|
246
|
+
key.length > 2 &&
|
|
247
|
+
key.startsWith('on') &&
|
|
248
|
+
key[2] === key[2].toUpperCase() && // onClick, onRender — NOT "one", "only"
|
|
249
|
+
isFunction(value)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Async events
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
onRender: async (el, state) => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await el.call('fetchData', el.props.id)
|
|
258
|
+
state.update({ data: result })
|
|
259
|
+
} catch (e) {
|
|
260
|
+
state.update({ error: e.message })
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 7. State
|
|
268
|
+
|
|
269
|
+
### Defining state
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
export const Counter = {
|
|
273
|
+
state: { count: 0, open: false, selected: null },
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Reading state in element definitions
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
export const Item = {
|
|
281
|
+
text: ({ state }) => state.label,
|
|
282
|
+
opacity: ({ state }) => state.loading ? 0.5 : 1,
|
|
283
|
+
isActive: ({ key, state }) => state.active === key
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Updating state from events
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
onClick: (event, el, state) => {
|
|
291
|
+
state.update({ on: !state.on }) // partial update
|
|
292
|
+
state.set({ on: false }) // replace
|
|
293
|
+
state.reset() // reset to initial
|
|
294
|
+
state.toggle('open') // toggle boolean
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Accessing root state
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
onClick: (event, el) => {
|
|
302
|
+
const rootState = el.getRootState()
|
|
303
|
+
const user = el.getRootState('user')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// In definitions
|
|
307
|
+
text: (el) => el.getRootState('currentPage')
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Targeted state updates (performance)
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
state.root.update({
|
|
314
|
+
activeModal: true
|
|
315
|
+
}, {
|
|
316
|
+
onlyUpdate: 'ModalCard' // only ModalCard subtree re-renders
|
|
317
|
+
})
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 8. `attr` — HTML Attributes
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
export const Input = {
|
|
326
|
+
tag: 'input',
|
|
327
|
+
attr: {
|
|
328
|
+
type: 'text',
|
|
329
|
+
autocomplete: 'off',
|
|
330
|
+
placeholder: ({ props }) => props.placeholder,
|
|
331
|
+
name: ({ props }) => props.name,
|
|
332
|
+
disabled: ({ props }) => props.disabled || null, // null removes the attr
|
|
333
|
+
value: (el) => el.call('exec', el.props.value, el),
|
|
334
|
+
required: ({ props }) => props.required,
|
|
335
|
+
role: 'button',
|
|
336
|
+
'aria-label': ({ props }) => props.aria?.label || props.text,
|
|
337
|
+
tabIndex: ({ props }) => props.tabIndex
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Rule**: Returning `null` or `undefined` from an attr function removes the attribute.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## 9. `text` and `html`
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
// Plain text content
|
|
350
|
+
export const Label = { text: ({ props }) => props.label }
|
|
351
|
+
export const Badge = { text: 'New' }
|
|
352
|
+
export const Price = { text: ({ state }) => `$${state.amount.toFixed(2)}` }
|
|
353
|
+
|
|
354
|
+
// Raw HTML (XSS risk — use sparingly)
|
|
355
|
+
export const RichText = { html: ({ props }) => props.html }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 10. Children
|
|
361
|
+
|
|
362
|
+
### Named children (most common)
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
export const Card = {
|
|
366
|
+
extends: 'Flex',
|
|
367
|
+
Header: {
|
|
368
|
+
extends: 'Flex',
|
|
369
|
+
Title: { text: ({ props }) => props.title },
|
|
370
|
+
},
|
|
371
|
+
Body: { html: ({ props }) => props.content },
|
|
372
|
+
Footer: {
|
|
373
|
+
CloseButton: { extends: 'SquareButton', icon: 'x' }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Rule**: Child keys that start with uppercase or are numeric → child elements. All others → CSS props or builtins.
|
|
379
|
+
|
|
380
|
+
### `childExtends` — extend all direct children
|
|
381
|
+
|
|
382
|
+
Must be a named component string (see RULES.md Rule 10):
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
export const NavList = {
|
|
386
|
+
childExtends: 'NavLink' // ✅ named string only
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### `childExtendRecursive` — apply to ALL descendants
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
export const Tree = {
|
|
394
|
+
childExtendRecursive: { fontSize: 'A' }
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### `children` — dynamic child list from data
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
export const DropdownList = {
|
|
402
|
+
children: ({ props }) => props.options || [],
|
|
403
|
+
childExtends: 'OptionItem'
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### `childrenAs` — control how children data maps to elements
|
|
408
|
+
|
|
409
|
+
By default, each item in `children` becomes `props` on the child element. Use `childrenAs` to change this:
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
// Default (childrenAs: 'props') — each item becomes element props
|
|
413
|
+
{ children: [{ text: 'Hello' }] }
|
|
414
|
+
// → child gets: { props: { text: 'Hello' } }
|
|
415
|
+
|
|
416
|
+
// childrenAs: 'state' — each item becomes element state
|
|
417
|
+
{ children: [{ count: 5 }], childrenAs: 'state' }
|
|
418
|
+
// → child gets: { state: { count: 5 } }
|
|
419
|
+
|
|
420
|
+
// childrenAs: 'element' — each item is used directly as element definition
|
|
421
|
+
{ children: [{ tag: 'span', text: 'Hi' }], childrenAs: 'element' }
|
|
422
|
+
// → child IS: { tag: 'span', text: 'Hi' }
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### `state: 'key'` — narrow state scope for children
|
|
426
|
+
|
|
427
|
+
When a parent has nested state (e.g. `state.data` is an array), use `state: 'data'` on the container to narrow the scope so children receive individual items:
|
|
428
|
+
|
|
429
|
+
```js
|
|
430
|
+
// Parent narrows state to the 'members' key
|
|
431
|
+
export const TeamList = {
|
|
432
|
+
state: 'members',
|
|
433
|
+
childExtends: 'TeamItem',
|
|
434
|
+
children: ({ state }) => state
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Child reads individual item state — requires state: true
|
|
438
|
+
export const TeamItem = {
|
|
439
|
+
state: true,
|
|
440
|
+
Title: { text: ({ state }) => state.name }
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Important**: `state: true` is required on child components that read `({ state }) => state.field` when used with `childExtends`. Without it, children don't receive individual state from the parent's children array.
|
|
445
|
+
|
|
446
|
+
### `content` — single dynamic child
|
|
447
|
+
|
|
448
|
+
```js
|
|
449
|
+
export const Page = {
|
|
450
|
+
content: ({ props }) => props.page
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## 11. Props
|
|
457
|
+
|
|
458
|
+
### Passing props (consumer side)
|
|
459
|
+
|
|
460
|
+
```js
|
|
461
|
+
const instance = {
|
|
462
|
+
extends: 'Button',
|
|
463
|
+
props: {
|
|
464
|
+
text: 'Submit',
|
|
465
|
+
href: '/dashboard',
|
|
466
|
+
disabled: false
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Accessing props inside definitions
|
|
472
|
+
|
|
473
|
+
```js
|
|
474
|
+
export const Input = {
|
|
475
|
+
attr: {
|
|
476
|
+
placeholder: ({ props }) => props.placeholder,
|
|
477
|
+
value: (el) => el.props.value,
|
|
478
|
+
disabled: ({ props }) => props.disabled || null
|
|
479
|
+
},
|
|
480
|
+
text: ({ props }) => props.label
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Boolean / computed props
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
export const MyComponent = {
|
|
488
|
+
isActive: ({ key, state }) => state.active === key, // computed boolean
|
|
489
|
+
hasIcon: ({ props }) => Boolean(props.icon),
|
|
490
|
+
useCache: true
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`is*`, `has*`, `use*` prefixed props are treated as boolean flags.
|
|
495
|
+
|
|
496
|
+
### `childProps` — inject props into named children
|
|
497
|
+
|
|
498
|
+
```js
|
|
499
|
+
export const Layout = {
|
|
500
|
+
childProps: {
|
|
501
|
+
onClick: (ev) => { ev.stopPropagation() } // all children get this
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## 12. `define` — Custom Property Transformers
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
define: {
|
|
512
|
+
isActive: (param, el, state, context) => {
|
|
513
|
+
if (param) el.classList.add('active')
|
|
514
|
+
else el.classList.remove('active')
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Built-in defines
|
|
520
|
+
|
|
521
|
+
These are registered globally and work on any element:
|
|
522
|
+
|
|
523
|
+
- `metadata` — SEO metadata (see SEO-METADATA.md). Values can be static or functions:
|
|
524
|
+
|
|
525
|
+
```js
|
|
526
|
+
export const aboutPage = {
|
|
527
|
+
metadata: {
|
|
528
|
+
title: 'About Us',
|
|
529
|
+
description: (el, s) => s.aboutText,
|
|
530
|
+
'og:image': '/about.png'
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
- `routes` — route definitions for the router
|
|
536
|
+
- `$router` — renders route content into the element
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## 13. `if` — Conditional Rendering
|
|
541
|
+
|
|
542
|
+
```js
|
|
543
|
+
export const AuthView = {
|
|
544
|
+
if: (el, state) => state.isAuthenticated,
|
|
545
|
+
Dashboard: { /* only renders if condition is true */ }
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export const ErrorMsg = {
|
|
549
|
+
if: ({ props }) => Boolean(props.error),
|
|
550
|
+
text: ({ props }) => props.error
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## 14. `scope`, `data`
|
|
557
|
+
|
|
558
|
+
```js
|
|
559
|
+
// scope: 'state' — element.scope becomes element.state
|
|
560
|
+
export const Form = {
|
|
561
|
+
scope: 'state',
|
|
562
|
+
state: { name: '', email: '' }
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// data — non-reactive storage (doesn't trigger re-renders)
|
|
566
|
+
export const Chart = {
|
|
567
|
+
data: { chartInstance: null },
|
|
568
|
+
onRender: (el) => {
|
|
569
|
+
el.data.chartInstance = new Chart(el.node, { /* ... */ })
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## 15. Element Methods (on every element)
|
|
577
|
+
|
|
578
|
+
```js
|
|
579
|
+
// Navigation
|
|
580
|
+
el.lookup('key') // find ancestor by key or predicate
|
|
581
|
+
el.lookdown('key') // find first descendant by key
|
|
582
|
+
el.lookdownAll('key') // find all descendants
|
|
583
|
+
el.nextElement() // sibling after
|
|
584
|
+
el.previousElement() // sibling before
|
|
585
|
+
|
|
586
|
+
// Updates
|
|
587
|
+
el.update({ key: value }) // partial update of element properties
|
|
588
|
+
el.set({ key: value }) // full replacement
|
|
589
|
+
el.setProps({ key: value }) // update props specifically
|
|
590
|
+
|
|
591
|
+
// Content
|
|
592
|
+
el.updateContent(newContent)
|
|
593
|
+
el.removeContent()
|
|
594
|
+
|
|
595
|
+
// State
|
|
596
|
+
el.getRootState() // app-level root state
|
|
597
|
+
el.getRootState('key') // specific key from root state
|
|
598
|
+
el.getRoot() // root element
|
|
599
|
+
el.getContext('key') // from element's context
|
|
600
|
+
|
|
601
|
+
// DOM
|
|
602
|
+
el.setNodeStyles({ key: value }) // apply inline styles
|
|
603
|
+
el.remove() // remove element from DOM
|
|
604
|
+
|
|
605
|
+
// Calling context functions
|
|
606
|
+
el.call('fnKey', ...args) // looks up in context.utils → functions → methods → snippets
|
|
607
|
+
|
|
608
|
+
// Debug
|
|
609
|
+
el.parse() // serialize element to plain object
|
|
610
|
+
el.keys() // list element's own keys
|
|
611
|
+
el.verbose() // log element in console
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## 16. State Methods
|
|
617
|
+
|
|
618
|
+
```js
|
|
619
|
+
state.update({ key: value }) // partial update, triggers re-render
|
|
620
|
+
state.set({ key: value }) // replace, triggers re-render
|
|
621
|
+
state.reset() // reset to initial value
|
|
622
|
+
state.toggle('open') // toggle boolean property
|
|
623
|
+
state.remove() // remove state node
|
|
624
|
+
state.quietUpdate({ key: value }) // update without re-render
|
|
625
|
+
state.add(item) // add to collection
|
|
626
|
+
state.setByPath('a.b.c', value) // update nested by path
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## 17. `el.call()` — Context Function Lookup
|
|
632
|
+
|
|
633
|
+
```js
|
|
634
|
+
el.call('router', href, root, {}, options)
|
|
635
|
+
el.call('exec', value, el) // execute a potentially-function prop
|
|
636
|
+
el.call('isString', value)
|
|
637
|
+
el.call('fetchData', id)
|
|
638
|
+
el.call('replaceLiteralsWithObjectFields', template)
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Lookup order: `context.utils → context.functions → context.methods → context.snippets`
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## 18. Router (Navigation)
|
|
646
|
+
|
|
647
|
+
### Declaring pages
|
|
648
|
+
|
|
649
|
+
```js
|
|
650
|
+
// pages/index.js
|
|
651
|
+
export default {
|
|
652
|
+
'/': homePage,
|
|
653
|
+
'/dashboard': dashboardPage,
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Navigation via Link
|
|
658
|
+
|
|
659
|
+
```js
|
|
660
|
+
export const NavItem = {
|
|
661
|
+
extends: 'Link',
|
|
662
|
+
text: ({ props }) => props.label,
|
|
663
|
+
href: '/dashboard'
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### Programmatic navigation
|
|
668
|
+
|
|
669
|
+
```js
|
|
670
|
+
onClick: (event, el) => {
|
|
671
|
+
event.preventDefault() // MUST come BEFORE router call
|
|
672
|
+
el.call('router', '/dashboard', el.__ref.root, {}, {
|
|
673
|
+
scrollToTop: true,
|
|
674
|
+
scrollToOptions: { behavior: 'instant' }
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Custom router element (persistent layouts)
|
|
680
|
+
|
|
681
|
+
Configure in `config.js` to render pages inside a specific element in the tree:
|
|
682
|
+
|
|
683
|
+
```js
|
|
684
|
+
// config.js
|
|
685
|
+
export default {
|
|
686
|
+
router: {
|
|
687
|
+
customRouterElement: 'Folder.Content' // dot-separated path from root
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
The `/` page defines the persistent layout shell. Sub-pages render inside the target element without destroying the layout. The path is resolved by traversing `root.Folder.Content`.
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## 19. Common Patterns
|
|
697
|
+
|
|
698
|
+
### Loading state
|
|
699
|
+
|
|
700
|
+
```js
|
|
701
|
+
export const DataList = {
|
|
702
|
+
state: { items: [], loading: true, error: null },
|
|
703
|
+
|
|
704
|
+
Loader: { if: ({ state }) => state.loading, extends: 'Spinner' },
|
|
705
|
+
Error: { if: ({ state }) => Boolean(state.error), text: ({ state }) => state.error },
|
|
706
|
+
Items: {
|
|
707
|
+
if: ({ state }) => !state.loading && !state.error,
|
|
708
|
+
children: ({ state }) => state.items,
|
|
709
|
+
childExtends: 'ListItem'
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
onRender: async (el, state) => {
|
|
713
|
+
try {
|
|
714
|
+
const items = await el.call('fetchItems')
|
|
715
|
+
state.update({ items, loading: false })
|
|
716
|
+
} catch (e) {
|
|
717
|
+
state.update({ error: e.message, loading: false })
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Active list item
|
|
724
|
+
|
|
725
|
+
```js
|
|
726
|
+
export const Menu = {
|
|
727
|
+
state: { active: null },
|
|
728
|
+
childExtends: 'MenuItem',
|
|
729
|
+
childProps: {
|
|
730
|
+
isActive: ({ key, state }) => state.active === key,
|
|
731
|
+
'.active': { fontWeight: '600', color: 'primary' },
|
|
732
|
+
onClick: (ev, el, state) => { state.update({ active: el.key }) }
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### Modal (v3 complete pattern)
|
|
738
|
+
|
|
739
|
+
```js
|
|
740
|
+
export const ModalCard = {
|
|
741
|
+
position: 'absolute', flexAlign: 'center center',
|
|
742
|
+
top: 0, left: 0, boxSize: '100% 100%',
|
|
743
|
+
transition: 'all C defaultBezier',
|
|
744
|
+
opacity: '0', visibility: 'hidden', pointerEvents: 'none', zIndex: '-1',
|
|
745
|
+
|
|
746
|
+
isActive: (el, s) => s.root.activeModal,
|
|
747
|
+
'.isActive': { opacity: '1', zIndex: 999999, visibility: 'visible', pointerEvents: 'initial' },
|
|
748
|
+
|
|
749
|
+
onClick: (event, element) => { element.call('closeModal') },
|
|
750
|
+
childProps: { onClick: (ev) => { ev.stopPropagation() } },
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## 20. Finding DOMQL Elements in the Browser DOM
|
|
757
|
+
|
|
758
|
+
DOMQL elements use generated Emotion class names (`smbls-xxx`). Access via `node.ref`:
|
|
759
|
+
|
|
760
|
+
```js
|
|
761
|
+
// Every DOMQL-managed DOM node has .ref pointing to its DOMQL element
|
|
762
|
+
const domqlElement = someNode.ref
|
|
763
|
+
domqlElement.key // element key name
|
|
764
|
+
domqlElement.props // current props
|
|
765
|
+
domqlElement.state // element state
|
|
766
|
+
domqlElement.parent // parent DOMQL element
|
|
767
|
+
|
|
768
|
+
// Find by key
|
|
769
|
+
for (const node of document.querySelectorAll('*')) {
|
|
770
|
+
if (node.ref?.key === 'ModalCard') { /* ... */ break }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Debug CSS state
|
|
774
|
+
ref.__ref.__class // CSS object input to Emotion
|
|
775
|
+
ref.__ref.__classNames // generated Emotion class names
|
|
776
|
+
window.getComputedStyle(ref.node).opacity
|
|
777
|
+
```
|