@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.
@@ -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
+ ```