@symbo.ls/mcp 1.0.10 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +5 -2
- package/symbols_mcp/skills/AUDIT.md +148 -174
- package/symbols_mcp/skills/BRAND_IDENTITY.md +75 -0
- package/symbols_mcp/skills/COMPONENTS.md +266 -0
- package/symbols_mcp/skills/COOKBOOK.md +850 -0
- package/symbols_mcp/skills/DEFAULT_COMPONENTS.md +3491 -1637
- package/symbols_mcp/skills/DEFAULT_LIBRARY.md +301 -0
- package/symbols_mcp/skills/DESIGN_CRITIQUE.md +70 -59
- package/symbols_mcp/skills/DESIGN_DIRECTION.md +109 -175
- package/symbols_mcp/skills/DESIGN_SYSTEM.md +722 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +65 -57
- package/symbols_mcp/skills/DESIGN_TO_CODE.md +83 -64
- package/symbols_mcp/skills/DESIGN_TREND.md +62 -50
- package/symbols_mcp/skills/FIGMA_MATCHING.md +69 -58
- package/symbols_mcp/skills/LEARNINGS.md +374 -0
- package/symbols_mcp/skills/MARKETING_ASSETS.md +71 -59
- package/symbols_mcp/skills/MIGRATION.md +561 -0
- package/symbols_mcp/skills/PATTERNS.md +536 -0
- package/symbols_mcp/skills/PRESENTATION.md +78 -0
- package/symbols_mcp/skills/PROJECT_STRUCTURE.md +398 -0
- package/symbols_mcp/skills/RULES.md +519 -0
- package/symbols_mcp/skills/RUNNING_APPS.md +476 -0
- package/symbols_mcp/skills/SEO-METADATA.md +64 -9
- package/symbols_mcp/skills/SNIPPETS.md +598 -0
- package/symbols_mcp/skills/SSR-BRENDER.md +99 -0
- package/symbols_mcp/skills/SYNTAX.md +835 -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/BRAND_INDENTITY.md +0 -69
- 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_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/THE_PRESENTATION.md +0 -69
- package/symbols_mcp/skills/UI_UX_PATTERNS.md +0 -68
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
# DOMQL v3 Syntax Reference
|
|
2
|
+
|
|
3
|
+
Authoritative reference for generating correct DOMQL v3 code. Every pattern is derived from real working code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Element Anatomy
|
|
8
|
+
|
|
9
|
+
Produce DOMQL elements as plain JS objects. Every key has a specific role:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
export const MyCard = {
|
|
13
|
+
tag: 'section', // HTML tag (default: div)
|
|
14
|
+
|
|
15
|
+
// CSS props (top-level, promoted via propertizeElement)
|
|
16
|
+
padding: 'B C',
|
|
17
|
+
gap: 'A',
|
|
18
|
+
flow: 'column',
|
|
19
|
+
theme: 'dialog',
|
|
20
|
+
round: 'C',
|
|
21
|
+
|
|
22
|
+
// HTML attributes
|
|
23
|
+
attr: { role: 'region', 'aria-label': ({ props }) => props.label },
|
|
24
|
+
|
|
25
|
+
// State
|
|
26
|
+
state: { open: false },
|
|
27
|
+
|
|
28
|
+
// Events (v3 top-level)
|
|
29
|
+
onClick: (event, el, state) => { state.update({ open: !state.open }) },
|
|
30
|
+
onRender: (el, state) => { console.log('rendered') },
|
|
31
|
+
|
|
32
|
+
// Children (PascalCase keys)
|
|
33
|
+
Header: { text: ({ props }) => props.title },
|
|
34
|
+
Body: { html: ({ props }) => props.content }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Element Lifecycle
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
create(props, parent, key, options)
|
|
44
|
+
|- createElement() creates bare element
|
|
45
|
+
|- applyExtends() merges extends stack (element wins)
|
|
46
|
+
|- propertizeElement() routes onXxx events, promotes CSS props
|
|
47
|
+
|- addMethods() attaches el.lookup / el.update / etc.
|
|
48
|
+
|- initProps() builds props from propsStack
|
|
49
|
+
+- createNode()
|
|
50
|
+
|- throughInitialExec() executes function props
|
|
51
|
+
|- applyEventsOnNode() binds element.on.* to DOM
|
|
52
|
+
+- iterates children -> create() recursively
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`propertizeElement` runs BEFORE `addMethods`. Do not rely on prototype methods during propertization.
|
|
56
|
+
|
|
57
|
+
### Propertization and Define System
|
|
58
|
+
|
|
59
|
+
`propertizeElement()` classifies keys between root and `props`. Keys starting with `$` overlap between css-in-props conditionals (`$isActive`) and define handlers (`$router`, deprecated v2 handlers like `$propsCollection`, `$collection`).
|
|
60
|
+
|
|
61
|
+
Define handlers (`element.define[key]` or `context.define[key]`) must stay at the element root so `throughInitialDefine` can process them. Propertization checks for define handlers BEFORE applying `CSS_SELECTOR_PREFIXES`:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const defineValue = this.define?.[key]
|
|
65
|
+
const globalDefineValue = this.context?.define?.[key]
|
|
66
|
+
if (isFunction(defineValue) || isFunction(globalDefineValue)) continue
|
|
67
|
+
if (CSS_SELECTOR_PREFIXES.has(firstChar)) {
|
|
68
|
+
obj.props[key] = value // move to props for css-in-props
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## REGISTRY Keys
|
|
75
|
+
|
|
76
|
+
These keys are handled by DOMQL internally and are NOT promoted to CSS props:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
attr, style, text, html, data, classlist, state, scope, deps,
|
|
80
|
+
extends, children, content,
|
|
81
|
+
childExtend (deprecated), childExtends, childExtendRecursive (deprecated), childExtendsRecursive,
|
|
82
|
+
props, if, define, __name, __ref, __hash, __text,
|
|
83
|
+
key, tag, query, parent, node, variables, on, component, context
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Any key NOT in this list and not PascalCase (component) is promoted to `element.props` as a CSS prop.
|
|
87
|
+
|
|
88
|
+
Always use `childExtends` (plural). The singular `childExtend` is deprecated v2 syntax kept for backwards compatibility.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Extending and Composing
|
|
93
|
+
|
|
94
|
+
| Pattern | Syntax |
|
|
95
|
+
|---|---|
|
|
96
|
+
| Single extend | `extends: 'Button'` |
|
|
97
|
+
| Multiple (first = highest priority) | `extends: [Link, RouterLink]` |
|
|
98
|
+
| String reference (from `context.components`) | `extends: 'Hoverable'` |
|
|
99
|
+
| Multiple strings | `extends: ['IconText', 'FocusableComponent']` |
|
|
100
|
+
|
|
101
|
+
### Merge Semantics
|
|
102
|
+
|
|
103
|
+
| Type | Rule |
|
|
104
|
+
|---|---|
|
|
105
|
+
| Own properties | Always win over extends |
|
|
106
|
+
| Objects | Deep-merged (both sides preserved) |
|
|
107
|
+
| Functions | NOT merged; element's function replaces extend's |
|
|
108
|
+
| Arrays | Concatenated |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## CSS Props (Top-Level Promotion)
|
|
113
|
+
|
|
114
|
+
Place CSS props at the element root. Non-registry, non-PascalCase keys become `element.props`:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
export const Card = {
|
|
118
|
+
padding: 'B C', // -> props.padding
|
|
119
|
+
gap: 'Z', // -> props.gap
|
|
120
|
+
flow: 'column', // shorthand for flexDirection
|
|
121
|
+
align: 'center', // NOT flexAlign
|
|
122
|
+
fontSize: 'A',
|
|
123
|
+
fontWeight: '500',
|
|
124
|
+
color: 'currentColor',
|
|
125
|
+
background: 'codGray',
|
|
126
|
+
round: 'C', // border-radius token
|
|
127
|
+
opacity: '0.85',
|
|
128
|
+
overflow: 'hidden',
|
|
129
|
+
transition: 'B defaultBezier',
|
|
130
|
+
transitionProperty: 'opacity, transform',
|
|
131
|
+
zIndex: 10,
|
|
132
|
+
tag: 'section', // stays at root (REGISTRY)
|
|
133
|
+
attr: { href: '...' } // stays at root (REGISTRY)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Pseudo-Classes and Pseudo-Elements
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
export const Hoverable = {
|
|
141
|
+
opacity: 0.85,
|
|
142
|
+
':hover': { opacity: 0.9, transform: 'scale(1.015)' },
|
|
143
|
+
':active': { opacity: 1, transform: 'scale(1.015)' },
|
|
144
|
+
':focus-visible': { outline: 'solid X blue.3' },
|
|
145
|
+
':not(:first-child)': {
|
|
146
|
+
'@dark': { borderWidth: '1px 0 0' },
|
|
147
|
+
'@light': { borderWidth: '1px 0 0' }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### CSS Class State Modifiers (Emotion `.className`)
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
export const Item = {
|
|
156
|
+
opacity: 0.6,
|
|
157
|
+
'.active': { opacity: 1, fontWeight: '600' },
|
|
158
|
+
'.disabled': { opacity: 0.3, pointerEvents: 'none' },
|
|
159
|
+
'.hidden': { transform: 'translate3d(0,10%,0)', opacity: 0, visibility: 'hidden' }
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Raw Style Object (Escape Hatch)
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
export const DropdownParent = {
|
|
167
|
+
style: {
|
|
168
|
+
'&:hover': {
|
|
169
|
+
zIndex: 1000,
|
|
170
|
+
'& [dropdown]': { transform: 'translate3d(0,0,0)', opacity: 1 }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Media Queries
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
export const Grid = {
|
|
180
|
+
columns: 'repeat(4, 1fr)',
|
|
181
|
+
'@tabletSm': { columns: 'repeat(2, 1fr)' },
|
|
182
|
+
'@mobileL': { columns: '1fr' },
|
|
183
|
+
'@dark': { background: 'codGray' },
|
|
184
|
+
'@light': { background: 'concrete' }
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Events
|
|
191
|
+
|
|
192
|
+
### v3 Syntax (Top-Level `onXxx`)
|
|
193
|
+
|
|
194
|
+
Use top-level `onXxx` handlers. Two signatures exist:
|
|
195
|
+
|
|
196
|
+
**DOM events** -- signature: `(event, el, state)`:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
onClick: (event, el, state) => { /* ... */ }
|
|
200
|
+
onChange: (event, el, state) => { /* ... */ }
|
|
201
|
+
onInput: (event, el, state) => { state.update({ value: event.target.value }) }
|
|
202
|
+
onSubmit: (event, el, state) => { event.preventDefault() }
|
|
203
|
+
onKeydown: (event, el, state) => { if (event.key === 'Enter') /* ... */ }
|
|
204
|
+
onMouseover: (event, el, state) => { /* ... */ }
|
|
205
|
+
onBlur: (event, el, state) => { /* ... */ }
|
|
206
|
+
onFocus: (event, el, state) => { /* ... */ }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Lifecycle events** -- signature: `(el, state)`:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
onInit: (el, state) => { /* before render */ }
|
|
213
|
+
onRender: (el, state) => { /* after render */ }
|
|
214
|
+
onCreate: (el, state) => { /* after full creation */ }
|
|
215
|
+
onUpdate: (el, state) => { /* after state/props update */ }
|
|
216
|
+
onStateUpdate: (el, state) => { /* after state update */ }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Element Lifecycle Events (Full Signatures)
|
|
220
|
+
|
|
221
|
+
| Event | Signature | When | Notes |
|
|
222
|
+
|---|---|---|---|
|
|
223
|
+
| `onInit` | `(element, state, context, updateOptions)` | Before init | Return `false` to break |
|
|
224
|
+
| `onAttachNode` | `(element, state, context, updateOptions)` | After DOM node attached | |
|
|
225
|
+
| `onRender` | `(element, state, context, updateOptions)` | After render | |
|
|
226
|
+
| `onComplete` | `(element, state, context, updateOptions)` | After full creation | |
|
|
227
|
+
| `onBeforeUpdate` | `(changes, element, state, context, updateOptions)` | Before update | `changes` is first param |
|
|
228
|
+
| `onUpdate` | `(element, state, context, updateOptions)` | After update | |
|
|
229
|
+
|
|
230
|
+
### State Events
|
|
231
|
+
|
|
232
|
+
| Event | Signature | When | Notes |
|
|
233
|
+
|---|---|---|---|
|
|
234
|
+
| `onStateInit` | `(element, state, context, updateOptions)` | Before state init | Return `false` to break |
|
|
235
|
+
| `onStateCreated` | `(element, state, context, updateOptions)` | After state created | |
|
|
236
|
+
| `onBeforeStateUpdate` | `(changes, element, state, context, updateOptions)` | Before state update | Return `false` to prevent; `changes` first |
|
|
237
|
+
| `onStateUpdate` | `(changes, element, state, context, updateOptions)` | After state update | `changes` is first param |
|
|
238
|
+
|
|
239
|
+
`onBeforeStateUpdate` and `onStateUpdate` receive `changes` as their FIRST parameter.
|
|
240
|
+
|
|
241
|
+
### DOMQL Lifecycle Names (Never Bound to DOM)
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
init, beforeClassAssign, render, renderRouter, attachNode,
|
|
245
|
+
stateInit, stateCreated, beforeStateUpdate, stateUpdate,
|
|
246
|
+
beforeUpdate, done, create, complete, frame, update
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Event Detection Rule
|
|
250
|
+
|
|
251
|
+
A key is a v3 event handler when:
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
key.length > 2 &&
|
|
255
|
+
key.startsWith('on') &&
|
|
256
|
+
key[2] === key[2].toUpperCase() && // onClick, onRender -- NOT "one", "only"
|
|
257
|
+
isFunction(value)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Async Events
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
onRender: async (el, state) => {
|
|
264
|
+
try {
|
|
265
|
+
const result = await el.call('fetchData', el.props.id)
|
|
266
|
+
state.update({ data: result })
|
|
267
|
+
} catch (e) {
|
|
268
|
+
state.update({ error: e.message })
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## State
|
|
276
|
+
|
|
277
|
+
### Define, Read, Update
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
// Define
|
|
281
|
+
state: { count: 0, open: false, selected: null }
|
|
282
|
+
|
|
283
|
+
// Read in definitions
|
|
284
|
+
text: ({ state }) => state.label
|
|
285
|
+
opacity: ({ state }) => state.loading ? 0.5 : 1
|
|
286
|
+
isActive: ({ key, state }) => state.active === key
|
|
287
|
+
|
|
288
|
+
// Update from events
|
|
289
|
+
onClick: (event, el, state) => {
|
|
290
|
+
state.update({ on: !state.on }) // partial update
|
|
291
|
+
state.set({ on: false }) // replace
|
|
292
|
+
state.reset() // reset to initial
|
|
293
|
+
state.toggle('open') // toggle boolean
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Root State Access
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
// From events
|
|
301
|
+
const rootState = el.getRootState()
|
|
302
|
+
const user = el.getRootState('user')
|
|
303
|
+
|
|
304
|
+
// In definitions
|
|
305
|
+
text: (el) => el.getRootState('currentPage')
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Targeted Updates (Performance)
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
state.root.update({ activeModal: true }, {
|
|
312
|
+
onlyUpdate: 'ModalCard' // only ModalCard subtree re-renders
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## State Methods
|
|
319
|
+
|
|
320
|
+
| Method | Description |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `state.update(value, options?)` | Deep overwrite, triggers re-render |
|
|
323
|
+
| `state.set(value, options?)` | Replace state entirely (removes old values) |
|
|
324
|
+
| `state.reset(options?)` | Reset to initial values |
|
|
325
|
+
| `state.add(value, options?)` | Add item to array state |
|
|
326
|
+
| `state.toggle(key, options?)` | Toggle boolean property |
|
|
327
|
+
| `state.remove(key, options?)` | Remove property |
|
|
328
|
+
| `state.apply(fn, options?)` | Apply fn that RETURNS new value |
|
|
329
|
+
| `state.applyFunction(fn, options?)` | Apply fn that MUTATES state directly |
|
|
330
|
+
| `state.replace(value, options?)` | SHALLOW replace (nested keys disappear) |
|
|
331
|
+
| `state.clean(options?)` | Empty the state |
|
|
332
|
+
| `state.parse()` | Get purified plain object |
|
|
333
|
+
| `state.quietUpdate(value)` | Update without triggering re-render |
|
|
334
|
+
| `state.quietReplace(value)` | Replace without triggering re-render |
|
|
335
|
+
| `state.destroy(options?)` | Completely remove state |
|
|
336
|
+
| `state.setByPath('a.b.c', value)` | Update nested by dot-path |
|
|
337
|
+
|
|
338
|
+
`apply()` expects the function to RETURN a new value. `applyFunction()` expects direct MUTATION:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
state.apply(s => ({ ...s, count: s.count + 1 })) // return
|
|
342
|
+
state.applyFunction(s => { s.count++ }) // mutate
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### State Update Options
|
|
346
|
+
|
|
347
|
+
| Option | Description |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `isHoisted` | Mark update as hoisted |
|
|
350
|
+
| `preventHoistElementUpdate` | Prevent hoisted element from updating |
|
|
351
|
+
|
|
352
|
+
### State Navigation
|
|
353
|
+
|
|
354
|
+
```js
|
|
355
|
+
state.parent // parent element's state
|
|
356
|
+
state.root // application-level root state
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
State as string inherits from parent:
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
// Parent has state: { userProfile: { name: 'John' } }
|
|
363
|
+
state: 'userProfile' // child inherits parent's userProfile key
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## `attr` (HTML Attributes)
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
export const Input = {
|
|
372
|
+
tag: 'input',
|
|
373
|
+
attr: {
|
|
374
|
+
type: 'text',
|
|
375
|
+
autocomplete: 'off',
|
|
376
|
+
placeholder: ({ props }) => props.placeholder,
|
|
377
|
+
name: ({ props }) => props.name,
|
|
378
|
+
disabled: ({ props }) => props.disabled || null, // null removes attr
|
|
379
|
+
value: (el) => el.call('exec', el.props.value, el),
|
|
380
|
+
required: ({ props }) => props.required,
|
|
381
|
+
role: 'button',
|
|
382
|
+
'aria-label': ({ props }) => props.aria?.label || props.text,
|
|
383
|
+
tabIndex: ({ props }) => props.tabIndex
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Return `null` or `undefined` from an attr function to remove the attribute.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## `text` and `html`
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
export const Label = { text: ({ props }) => props.label }
|
|
396
|
+
export const Badge = { text: 'New' }
|
|
397
|
+
export const Price = { text: ({ state }) => `$${state.amount.toFixed(2)}` }
|
|
398
|
+
export const RichText = { html: ({ props }) => props.html } // XSS risk
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Children
|
|
404
|
+
|
|
405
|
+
### Named Children
|
|
406
|
+
|
|
407
|
+
PascalCase or numeric keys become child elements:
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
export const Card = {
|
|
411
|
+
flow: 'y',
|
|
412
|
+
Header: {
|
|
413
|
+
flow: 'x',
|
|
414
|
+
Title: { text: ({ props }) => props.title },
|
|
415
|
+
},
|
|
416
|
+
Body: { html: ({ props }) => props.content },
|
|
417
|
+
Footer: {
|
|
418
|
+
CloseButton: { extends: 'SquareButton', icon: 'x' }
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### `childExtends`
|
|
424
|
+
|
|
425
|
+
Extend all direct children. Use a named component string:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
export const NavList = { childExtends: 'NavLink' }
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### `childExtendsRecursive`
|
|
432
|
+
|
|
433
|
+
Apply to ALL descendants:
|
|
434
|
+
|
|
435
|
+
```js
|
|
436
|
+
export const Tree = { childExtendsRecursive: { fontSize: 'A' } }
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### `children` (Dynamic Child List)
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
export const DropdownList = {
|
|
443
|
+
children: ({ props }) => props.options || [],
|
|
444
|
+
childExtends: 'OptionItem'
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### `childrenAs`
|
|
449
|
+
|
|
450
|
+
Control how children data maps to elements:
|
|
451
|
+
|
|
452
|
+
| Value | Behavior |
|
|
453
|
+
|---|---|
|
|
454
|
+
| `'props'` (default) | Each item becomes child's `props` |
|
|
455
|
+
| `'state'` | Each item becomes child's `state` |
|
|
456
|
+
| `'element'` | Each item is used directly as element definition |
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
{ children: [{ text: 'Hello' }] } // -> { props: { text: 'Hello' } }
|
|
460
|
+
{ children: [{ count: 5 }], childrenAs: 'state' } // -> { state: { count: 5 } }
|
|
461
|
+
{ children: [{ tag: 'span', text: 'Hi' }], childrenAs: 'element' } // -> { tag: 'span', text: 'Hi' }
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### `state: 'key'` (Narrow State Scope)
|
|
465
|
+
|
|
466
|
+
Narrow parent state for children:
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
export const TeamList = {
|
|
470
|
+
state: 'members',
|
|
471
|
+
childExtends: 'TeamItem',
|
|
472
|
+
children: ({ state }) => state
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const TeamItem = {
|
|
476
|
+
state: true, // REQUIRED for children to receive individual state
|
|
477
|
+
Title: { text: ({ state }) => state.name }
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
`state: true` is required on child components reading `({ state }) => state.field` when used with `childExtends`.
|
|
482
|
+
|
|
483
|
+
### `content` (Single Dynamic Child)
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
export const Page = { content: ({ props }) => props.page }
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Children as Async
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
{
|
|
493
|
+
children: async (element, state, context) => await window.fetch('...endpoint'),
|
|
494
|
+
childrenAs: 'state',
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Props
|
|
501
|
+
|
|
502
|
+
### Pass and Access
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
// Pass (consumer side)
|
|
506
|
+
{ extends: 'Button', props: { text: 'Submit', href: '/dashboard', disabled: false } }
|
|
507
|
+
|
|
508
|
+
// Access (definition side)
|
|
509
|
+
attr: {
|
|
510
|
+
placeholder: ({ props }) => props.placeholder,
|
|
511
|
+
value: (el) => el.props.value,
|
|
512
|
+
disabled: ({ props }) => props.disabled || null
|
|
513
|
+
}
|
|
514
|
+
text: ({ props }) => props.label
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Boolean/Computed Props
|
|
518
|
+
|
|
519
|
+
`is*`, `has*`, `use*` prefixes are treated as boolean flags:
|
|
520
|
+
|
|
521
|
+
```js
|
|
522
|
+
isActive: ({ key, state }) => state.active === key
|
|
523
|
+
hasIcon: ({ props }) => Boolean(props.icon)
|
|
524
|
+
useCache: true
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### `childProps`
|
|
528
|
+
|
|
529
|
+
Inject props into all named children:
|
|
530
|
+
|
|
531
|
+
```js
|
|
532
|
+
export const Layout = {
|
|
533
|
+
childProps: {
|
|
534
|
+
onClick: (ev) => { ev.stopPropagation() }
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## `define` (Custom Property Transformers)
|
|
542
|
+
|
|
543
|
+
```js
|
|
544
|
+
define: {
|
|
545
|
+
isActive: (param, el, state, context) => {
|
|
546
|
+
if (param) el.classList.add('active')
|
|
547
|
+
else el.classList.remove('active')
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Built-In Defines
|
|
553
|
+
|
|
554
|
+
| Define | Purpose |
|
|
555
|
+
|---|---|
|
|
556
|
+
| `metadata` | SEO metadata (see SEO-METADATA.md) |
|
|
557
|
+
| `routes` | Route definitions for the router |
|
|
558
|
+
| `$router` | Render route content into the element |
|
|
559
|
+
|
|
560
|
+
```js
|
|
561
|
+
export const aboutPage = {
|
|
562
|
+
metadata: {
|
|
563
|
+
title: 'About Us',
|
|
564
|
+
description: (el, s) => s.aboutText,
|
|
565
|
+
'og:image': '/about.png'
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## `if` (Conditional Rendering)
|
|
573
|
+
|
|
574
|
+
```js
|
|
575
|
+
export const AuthView = {
|
|
576
|
+
if: (el, state) => state.isAuthenticated,
|
|
577
|
+
Dashboard: { /* renders only when true */ }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export const ErrorMsg = {
|
|
581
|
+
if: ({ props }) => Boolean(props.error),
|
|
582
|
+
text: ({ props }) => props.error
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## `scope` and `data`
|
|
589
|
+
|
|
590
|
+
```js
|
|
591
|
+
// scope: 'state' -- element.scope becomes element.state
|
|
592
|
+
export const Form = {
|
|
593
|
+
scope: 'state',
|
|
594
|
+
state: { name: '', email: '' }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// data -- non-reactive storage (no re-renders)
|
|
598
|
+
export const Chart = {
|
|
599
|
+
data: { chartInstance: null },
|
|
600
|
+
onRender: (el) => {
|
|
601
|
+
el.data.chartInstance = new Chart(el.node, { /* ... */ })
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Element Methods
|
|
609
|
+
|
|
610
|
+
| Category | Method | Description |
|
|
611
|
+
|---|---|---|
|
|
612
|
+
| **Navigation** | `el.lookup('key')` | Find ancestor by key or predicate |
|
|
613
|
+
| | `el.lookdown('key')` | Find first descendant by key |
|
|
614
|
+
| | `el.lookdownAll('key')` | Find all descendants by key |
|
|
615
|
+
| | `el.spotByPath(['Header', 'Nav', 'Logo'])` | Find by array path |
|
|
616
|
+
| | `el.nextElement()` | Next sibling |
|
|
617
|
+
| | `el.previousElement()` | Previous sibling |
|
|
618
|
+
| | `el.getRoot()` | Root element |
|
|
619
|
+
| **Updates** | `el.update({ key: value })` | Deep overwrite element properties |
|
|
620
|
+
| | `el.set({ key: value })` | Set content element |
|
|
621
|
+
| | `el.setProps({ key: value })` | Update props specifically |
|
|
622
|
+
| **Content** | `el.updateContent(newContent)` | Update content |
|
|
623
|
+
| | `el.removeContent()` | Remove content |
|
|
624
|
+
| **State** | `el.getRootState()` | App-level root state |
|
|
625
|
+
| | `el.getRootState('key')` | Specific key from root state |
|
|
626
|
+
| | `el.getContext('key')` | Value from element's context |
|
|
627
|
+
| **DOM** | `el.setNodeStyles({ key: value })` | Apply inline styles |
|
|
628
|
+
| | `el.remove()` | Remove from tree and DOM |
|
|
629
|
+
| **Context** | `el.call('fnKey', ...args)` | Lookup: `context.utils -> functions -> methods -> snippets` |
|
|
630
|
+
| **Router** | `el.router(path, root)` | SPA navigation |
|
|
631
|
+
| **Dependencies** | `el.require('moduleName')` | Cross-environment dependency loading |
|
|
632
|
+
| **Debug** | `el.parse(exclude)` | One-level purified plain object |
|
|
633
|
+
| | `el.parseDeep(exclude)` | Deep purified plain object |
|
|
634
|
+
| | `el.keys()` | List element's own keys |
|
|
635
|
+
| | `el.verbose()` | Log element in console |
|
|
636
|
+
|
|
637
|
+
### Element Update Options
|
|
638
|
+
|
|
639
|
+
Pass as second argument to `el.update(value, options)`:
|
|
640
|
+
|
|
641
|
+
| Option | Description |
|
|
642
|
+
|---|---|
|
|
643
|
+
| `onlyUpdate` | Only update specific subtree by key |
|
|
644
|
+
| `preventUpdate` | Prevent element update |
|
|
645
|
+
| `preventStateUpdate` | Prevent state update |
|
|
646
|
+
| `preventUpdateListener` | Skip update event listeners |
|
|
647
|
+
| `preventUpdateAfter` | Skip post-update hooks |
|
|
648
|
+
| `lazyLoad` | Enable lazy loading for the update |
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## `el.call()` (Context Function Lookup)
|
|
653
|
+
|
|
654
|
+
Lookup order: `context.utils -> context.functions -> context.methods -> context.snippets`
|
|
655
|
+
|
|
656
|
+
```js
|
|
657
|
+
el.call('router', href, root, {}, options)
|
|
658
|
+
el.call('exec', value, el)
|
|
659
|
+
el.call('isString', value)
|
|
660
|
+
el.call('fetchData', id)
|
|
661
|
+
el.call('replaceLiteralsWithObjectFields', template)
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Router
|
|
667
|
+
|
|
668
|
+
### Declare Pages
|
|
669
|
+
|
|
670
|
+
```js
|
|
671
|
+
// pages/index.js
|
|
672
|
+
export default {
|
|
673
|
+
'/': homePage,
|
|
674
|
+
'/dashboard': dashboardPage,
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Link Navigation
|
|
679
|
+
|
|
680
|
+
```js
|
|
681
|
+
export const NavItem = {
|
|
682
|
+
extends: 'Link',
|
|
683
|
+
text: ({ props }) => props.label,
|
|
684
|
+
href: '/dashboard'
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Programmatic Navigation
|
|
689
|
+
|
|
690
|
+
Call `event.preventDefault()` BEFORE the router call:
|
|
691
|
+
|
|
692
|
+
```js
|
|
693
|
+
onClick: (event, el) => {
|
|
694
|
+
event.preventDefault()
|
|
695
|
+
el.call('router', '/dashboard', el.__ref.root, {}, {
|
|
696
|
+
scrollToTop: true,
|
|
697
|
+
scrollToOptions: { behavior: 'instant' }
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Custom Router Element (Persistent Layouts)
|
|
703
|
+
|
|
704
|
+
Configure in `config.js` to render pages inside a specific element:
|
|
705
|
+
|
|
706
|
+
```js
|
|
707
|
+
// config.js
|
|
708
|
+
export default {
|
|
709
|
+
router: {
|
|
710
|
+
customRouterElement: 'Folder.Content' // dot-separated path from root
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
The `/` page defines the persistent layout shell. Sub-pages render inside the target element without destroying the layout.
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## `element.require()` (Cross-Environment Dependency Loading)
|
|
720
|
+
|
|
721
|
+
```js
|
|
722
|
+
{
|
|
723
|
+
tag: 'canvas',
|
|
724
|
+
onRender: async (element, state) => {
|
|
725
|
+
const Chart = element.require('chartjs')
|
|
726
|
+
const ctx = element.node.getContext('2d')
|
|
727
|
+
new Chart(ctx, { type: 'bar', data: { /* ... */ } })
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## Common Patterns
|
|
735
|
+
|
|
736
|
+
### Loading State
|
|
737
|
+
|
|
738
|
+
```js
|
|
739
|
+
export const DataList = {
|
|
740
|
+
state: { items: [], loading: true, error: null },
|
|
741
|
+
Loader: { if: ({ state }) => state.loading, extends: 'Spinner' },
|
|
742
|
+
Error: { if: ({ state }) => Boolean(state.error), text: ({ state }) => state.error },
|
|
743
|
+
Items: {
|
|
744
|
+
if: ({ state }) => !state.loading && !state.error,
|
|
745
|
+
children: ({ state }) => state.items,
|
|
746
|
+
childExtends: 'ListItem'
|
|
747
|
+
},
|
|
748
|
+
onRender: async (el, state) => {
|
|
749
|
+
try {
|
|
750
|
+
const items = await el.call('fetchItems')
|
|
751
|
+
state.update({ items, loading: false })
|
|
752
|
+
} catch (e) {
|
|
753
|
+
state.update({ error: e.message, loading: false })
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Active List Item
|
|
760
|
+
|
|
761
|
+
```js
|
|
762
|
+
export const Menu = {
|
|
763
|
+
state: { active: null },
|
|
764
|
+
childExtends: 'MenuItem',
|
|
765
|
+
childProps: {
|
|
766
|
+
isActive: ({ key, state }) => state.active === key,
|
|
767
|
+
'.active': { fontWeight: '600', color: 'primary' },
|
|
768
|
+
onClick: (ev, el, state) => { state.update({ active: el.key }) }
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Modal
|
|
774
|
+
|
|
775
|
+
```js
|
|
776
|
+
export const ModalCard = {
|
|
777
|
+
position: 'absolute', align: 'center center',
|
|
778
|
+
top: 0, left: 0, boxSize: '100% 100%',
|
|
779
|
+
transition: 'all C defaultBezier',
|
|
780
|
+
opacity: '0', visibility: 'hidden', pointerEvents: 'none', zIndex: '-1',
|
|
781
|
+
|
|
782
|
+
isActive: (el, s) => s.root.activeModal,
|
|
783
|
+
'.isActive': { opacity: '1', zIndex: 999999, visibility: 'visible', pointerEvents: 'initial' },
|
|
784
|
+
|
|
785
|
+
onClick: (event, element) => { element.call('closeModal') },
|
|
786
|
+
childProps: { onClick: (ev) => { ev.stopPropagation() } },
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
## Naming Conventions
|
|
793
|
+
|
|
794
|
+
| Category | Convention | Examples |
|
|
795
|
+
|---|---|---|
|
|
796
|
+
| Components | PascalCase | `CustomComponent`, `NavBar`, `UserProfile` |
|
|
797
|
+
| Properties | camelCase | `paddingInlineStart`, `fontSize`, `backgroundColor` |
|
|
798
|
+
| Repeating keys | Snake_Case suffixes | `Li_1`, `Li_2`, `Li_One` |
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
## Reserved Keywords
|
|
803
|
+
|
|
804
|
+
These keys are handled by the DOMQL engine and are NOT CSS props or child components:
|
|
805
|
+
|
|
806
|
+
```
|
|
807
|
+
key, extends, childExtends, childExtendsRecursive, childProps, props,
|
|
808
|
+
state, tag, query, data, scope, children, childrenAs, context
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
All other keys: lowercase/camelCase = CSS props, PascalCase = child components.
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Finding DOMQL Elements in Browser DOM
|
|
816
|
+
|
|
817
|
+
Every DOMQL-managed DOM node has `.ref` pointing to its DOMQL element:
|
|
818
|
+
|
|
819
|
+
```js
|
|
820
|
+
const domqlElement = someNode.ref
|
|
821
|
+
domqlElement.key // element key name
|
|
822
|
+
domqlElement.props // current props
|
|
823
|
+
domqlElement.state // element state
|
|
824
|
+
domqlElement.parent // parent DOMQL element
|
|
825
|
+
|
|
826
|
+
// Find by key
|
|
827
|
+
for (const node of document.querySelectorAll('*')) {
|
|
828
|
+
if (node.ref?.key === 'ModalCard') { /* ... */ break }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Debug CSS state
|
|
832
|
+
ref.__ref.__class // CSS object input to Emotion
|
|
833
|
+
ref.__ref.__classNames // generated Emotion class names
|
|
834
|
+
window.getComputedStyle(ref.node).opacity
|
|
835
|
+
```
|