@symbo.ls/element 3.14.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/src/create.js ADDED
@@ -0,0 +1,1705 @@
1
+ 'use strict'
2
+
3
+ import { createStore, isStore } from '@symbo.ls/signal'
4
+ import { createEffect, batch } from '@symbo.ls/signal'
5
+ import { cacheNode, detectTag } from './cache.js'
6
+ import { REGISTRY, LIFECYCLE_EVENTS } from './registry.js'
7
+ import { registerEvent } from './delegate.js'
8
+ import { applyExtends } from './extends.js'
9
+ import { addMethods } from './methods.js'
10
+ import { dispose } from './dispose.js'
11
+ import { replaceLiteralsWithObjectFields, exec } from '@symbo.ls/utils'
12
+ import { runPluginHook } from '@symbo.ls/utils/function.js'
13
+ import { applyStateMethods } from '@symbo.ls/state'
14
+ import { CSS_PROPS_REGISTRY } from 'css-in-props/src/props'
15
+ import { DEFAULT_CSS_PROPERTIES_LIST } from 'css-in-props/src/props/defaults'
16
+ import { css as atomicCss } from '@symbo.ls/css'
17
+ import { pushConfig, popConfig } from '@symbo.ls/scratch'
18
+ import { checkAttributeByTagName, applyAttrTransforms } from 'attrs-in-props'
19
+
20
+ let _uniqId = 1
21
+
22
+ // Detect onXxx event handler pattern
23
+ function isEventHandler(key) {
24
+ return key.length > 2 &&
25
+ key.charCodeAt(0) === 111 && // 'o'
26
+ key.charCodeAt(1) === 110 && // 'n'
27
+ key.charCodeAt(2) >= 65 && key.charCodeAt(2) <= 90 // A-Z
28
+ }
29
+
30
+ // Extract event name from onXxx key (onClick -> click, onMouseDown -> mouseDown)
31
+ function eventNameFrom(key) {
32
+ return key.charAt(2).toLowerCase() + key.slice(3)
33
+ }
34
+
35
+ // Convert camelCase event name to lowercase DOM event (mouseDown -> mousedown)
36
+ function toDomEventName(name) {
37
+ return name.toLowerCase()
38
+ }
39
+
40
+ // Deep merge for child component definitions — override wins, but nested objects are recursively merged
41
+ function deepMergeChild(base, override) {
42
+ const result = { ...base }
43
+ for (const key in override) {
44
+ const baseVal = result[key]
45
+ const overVal = override[key]
46
+ if (baseVal && overVal &&
47
+ typeof baseVal === 'object' && typeof overVal === 'object' &&
48
+ !Array.isArray(baseVal) && !Array.isArray(overVal) &&
49
+ typeof baseVal !== 'function' && typeof overVal !== 'function') {
50
+ result[key] = deepMergeChild(baseVal, overVal)
51
+ } else {
52
+ result[key] = overVal
53
+ }
54
+ }
55
+ return result
56
+ }
57
+
58
+ // --- Create Element ---
59
+
60
+ export function create(definition, parent, key, options = {}, attachOptions) {
61
+ // Normalize definition
62
+ if (definition == null || definition === false) return null
63
+ if (typeof definition === 'string' || typeof definition === 'number') {
64
+ definition = { text: definition }
65
+ }
66
+
67
+
68
+ const element = Object.create(null)
69
+ element.key = key || definition.key || `_${_uniqId++}`
70
+ element.parent = parent || null
71
+ element.context = definition.context || parent?.context || options.context || {}
72
+ if (!element.context.__create) element.context.__create = create
73
+
74
+ // Internal ref
75
+ const ref = element.__ref = {
76
+ __if: true,
77
+ __children: [],
78
+ __exec: {},
79
+ __effects: [],
80
+ __eventCleanup: [],
81
+ root: parent?.__ref?.root || element,
82
+ path: parent?.__ref?.path ? [...parent.__ref.path, element.key] : [element.key]
83
+ }
84
+
85
+ // Auto-extend from key name: Key_suffix -> extends Key component
86
+ if (key && !definition.extends && !definition.extend) {
87
+ const underscoreIdx = key.indexOf('_')
88
+ if (underscoreIdx > 0) {
89
+ const baseName = key.substring(0, underscoreIdx)
90
+ const components = element.context?.components
91
+ if (components && (components[baseName] || components['smbls.' + baseName])) {
92
+ definition = { extends: baseName, ...definition }
93
+ }
94
+ }
95
+ }
96
+
97
+ // Merge extends chain
98
+ applyExtends(element, definition, options)
99
+
100
+ // Copy properties from definition — deep merge child overrides, shallow merge known keys, override rest
101
+ const MERGE_KEYS = new Set(['childProps', 'attr', 'style', 'scope', 'data'])
102
+
103
+ // Capture props set by the extends chain before they get merged flat
104
+ const inheritedProps = (element.props && typeof element.props === 'object')
105
+ ? element.props
106
+ : null
107
+
108
+ for (const prop in definition) {
109
+ if (prop === 'parent' || prop === 'node' || prop === '__ref' || prop === 'extends' || prop === 'context' || prop === 'props') continue
110
+ const val = definition[prop]
111
+ const existing = element[prop]
112
+ const bothObjects = existing && val &&
113
+ typeof existing === 'object' && typeof val === 'object' &&
114
+ !Array.isArray(existing) && !Array.isArray(val) &&
115
+ typeof existing !== 'function' && typeof val !== 'function'
116
+ if (bothObjects && (prop.charCodeAt(0) >= 65 && prop.charCodeAt(0) <= 90)) {
117
+ // Deep merge for uppercase child component overrides (definition wins)
118
+ element[prop] = deepMergeChild(existing, val)
119
+ } else if (bothObjects && MERGE_KEYS.has(prop)) {
120
+ element[prop] = { ...existing, ...val }
121
+ } else {
122
+ element[prop] = val
123
+ }
124
+ }
125
+
126
+ // Helper to merge a props-like object onto element directly
127
+ function mergePropsOntoElement (propsObj) {
128
+ for (const prop in propsObj) {
129
+ const val = propsObj[prop]
130
+ const existing = element[prop]
131
+ const bothObjects = existing && val &&
132
+ typeof existing === 'object' && typeof val === 'object' &&
133
+ !Array.isArray(existing) && !Array.isArray(val) &&
134
+ typeof existing !== 'function' && typeof val !== 'function'
135
+ if (bothObjects && (prop.charCodeAt(0) >= 65 && prop.charCodeAt(0) <= 90)) {
136
+ element[prop] = deepMergeChild(existing, val)
137
+ } else if (bothObjects && MERGE_KEYS.has(prop)) {
138
+ element[prop] = { ...existing, ...val }
139
+ } else {
140
+ element[prop] = val
141
+ }
142
+ }
143
+ }
144
+
145
+ // Merge inherited props from extends chain flat onto element
146
+ if (inheritedProps) mergePropsOntoElement(inheritedProps)
147
+
148
+ // Merge definition.props flat onto element (definition overrides inherited)
149
+ if (definition.props && typeof definition.props === 'object') {
150
+ mergePropsOntoElement(definition.props)
151
+ }
152
+
153
+ if (definition.data !== undefined) element.data = definition.data
154
+
155
+ if (definition.classlist !== undefined) element.classlist = definition.classlist
156
+ if (definition.text !== undefined) element.text = definition.text
157
+ if (definition.html !== undefined) element.html = definition.html
158
+ if (definition.scope !== undefined) {
159
+ element.scope = parent?.scope
160
+ ? Object.assign(Object.create(parent.scope), definition.scope)
161
+ : definition.scope
162
+ } else if (parent?.scope) {
163
+ element.scope = Object.create(parent.scope)
164
+ }
165
+ if (definition.if !== undefined) element.if = definition.if
166
+
167
+
168
+ // Add methods
169
+ addMethods(element)
170
+
171
+ // Create state (reactive store)
172
+ createElementState(element, parent, definition)
173
+
174
+ // Evaluate `if` condition
175
+ if (element.if != null) {
176
+ const ifResult = typeof element.if === 'function'
177
+ ? element.if(element, element.state, element.context)
178
+ : element.if
179
+ ref.__if = !!ifResult
180
+ }
181
+
182
+ if (!ref.__if) {
183
+ if (parent) parent[element.key] = element
184
+ // Register only the `if` effect so the element can appear later when the
185
+ // condition becomes true. Other effects will be registered inside the
186
+ // effect's first-time activation branch.
187
+ if (typeof definition.if === 'function') {
188
+ registerEffects(element, { if: definition.if })
189
+ }
190
+ return element
191
+ } else if (typeof definition.if === 'function') {
192
+ // Element starts visible — mark as initialized so the `if` effect's
193
+ // re-activation branch (false→true) doesn't re-run full creation.
194
+ ref.__ifInitialized = true
195
+ }
196
+
197
+ // Detect tag
198
+ if (!element.tag) element.tag = detectTag(element)
199
+
200
+ // Fire onInit
201
+ triggerLifecycle('Init', element, options)
202
+
203
+ // Create DOM node
204
+ const node = element.node = cacheNode(element)
205
+ if (!node) return element
206
+
207
+ triggerLifecycle('AttachNode', element, options)
208
+
209
+ // Set key as data attribute for dev debugging
210
+ if (node.setAttribute && element.key) {
211
+ node.setAttribute('data-key', element.key)
212
+ }
213
+
214
+ // Stamp data-smbls-app on the app's root node so its design-system CSS
215
+ // variables (scoped to `[data-smbls-app="<key>"]`) cascade through its
216
+ // subtree without leaking to sibling apps in the same document.
217
+ if (
218
+ node.setAttribute &&
219
+ element.context?.scopeSelector &&
220
+ element.context.scopeSelector !== ':root' &&
221
+ element.context.key &&
222
+ element.key === element.context.key
223
+ ) {
224
+ node.setAttribute('data-smbls-app', element.context.key)
225
+ }
226
+
227
+ // Apply id attribute
228
+ if (element.id && node.setAttribute) {
229
+ node.id = element.id
230
+ }
231
+
232
+ // Register reactive effects for dynamic properties
233
+ registerEffects(element, definition)
234
+
235
+ // Apply static mixins (text, html, style, attr, data, classlist)
236
+ applyStaticMixins(element)
237
+
238
+ // Apply attrs-in-props (src, href, etc. from element properties)
239
+ applyAttrsInProps(element)
240
+
241
+ // Register events (onXxx handlers directly on element)
242
+ registerEvents(element, options)
243
+
244
+ // FB-render-runtime: previously a thrown error inside createChildren()
245
+ // would silently abort the element-mount walk, leaving the page with a
246
+ // 0-child root div and no console output. Wrap with try/catch + log so
247
+ // operators can see what crashed.
248
+ try {
249
+ createChildren(element, options)
250
+ } catch (childErr) {
251
+ if (typeof console !== 'undefined' && console.error) {
252
+ const elPath = element.__ref?.path?.join('.') || element.key || '<root>'
253
+ console.error(
254
+ `[smbls] createChildren failed at ${elPath}:`,
255
+ childErr?.message || childErr,
256
+ childErr?.stack ? '\n' + childErr.stack.split('\n').slice(0, 6).join('\n') : ''
257
+ )
258
+ }
259
+ }
260
+
261
+ // Assign node to DOM
262
+ assignNode(element, parent, attachOptions)
263
+
264
+ // Track in parent
265
+ if (parent) {
266
+ parent[element.key] = element
267
+ if (parent.__ref?.__children) {
268
+ if (!parent.__ref.__children.includes(element.key)) {
269
+ parent.__ref.__children.push(element.key)
270
+ }
271
+ }
272
+ }
273
+
274
+ // Evaluate extends-inherited html functions (e.g. Svg component).
275
+ // Guard: if the function returns a function (e.g. an extended component
276
+ // forwards `value` which itself is a function getter), skip setting
277
+ // innerHTML — otherwise we'd serialize the function source into the DOM
278
+ // (visible as `(el, s) => …` text inside textareas / inputs).
279
+ if (typeof element.html === 'function' && element.node && !element.node.innerHTML) {
280
+ try {
281
+ const val = element.html(element, element.state, element.context)
282
+ if (val != null && typeof val !== 'function') element.node.innerHTML = val
283
+ } catch (e) { /* skip if html function errors */ }
284
+ }
285
+
286
+ triggerLifecycle('Create', element, options)
287
+ triggerLifecycle('Complete', element, options)
288
+ triggerLifecycle('Render', element, options)
289
+ triggerLifecycle('RenderRouter', element, options)
290
+
291
+ // Start onFrame loop if defined
292
+ if (typeof element.onFrame === 'function') {
293
+ const frameHandler = element.onFrame
294
+ const frameDsConfig = element.context?.designSystem
295
+ const tick = () => {
296
+ if (!element.node || !element.node.isConnected) return
297
+ if (frameDsConfig) pushConfig(frameDsConfig)
298
+ try { frameHandler(element, element.state, element.context) } catch (e) { console.error('[DOMQL] onFrame error:', e) }
299
+ finally {
300
+ if (frameDsConfig) popConfig()
301
+ }
302
+ requestAnimationFrame(tick)
303
+ }
304
+ requestAnimationFrame(tick)
305
+ }
306
+
307
+ return element
308
+ }
309
+
310
+ // --- State Creation ---
311
+
312
+ function createElementState(element, parent, definition) {
313
+ // Prefer the local definition's `state`, but fall back to anything that
314
+ // applyExtends already merged onto element.state from the extends chain.
315
+ // Without this fallback, a component like `{ extends: 'Dashboard' }` where
316
+ // Dashboard defines `state: { useJson: true }` would lose its state because
317
+ // the local definition has no `state` field, and we'd silently inherit
318
+ // parent state instead.
319
+ const stateDef = definition.state !== undefined
320
+ ? definition.state
321
+ : (element.state && !isStore(element.state) ? element.state : undefined)
322
+
323
+ if (stateDef === undefined || stateDef === null) {
324
+ if (parent?.state) {
325
+ element.state = parent.state
326
+ applyStateMethods(element, element.state)
327
+ } else if (element.context?.state) {
328
+ // Root element: use prepared state from context
329
+ const ctxState = element.context.state
330
+ const stateData = { ...ctxState }
331
+
332
+ // Merge polyglot translations for active language into root state
333
+ const poly = element.context?.polyglot
334
+ if (poly?.translations) {
335
+ const lang = ctxState.lang || poly.defaultLang || 'en'
336
+ const langT = poly.translations[lang]
337
+ if (langT && typeof langT === 'object') {
338
+ for (const k in langT) {
339
+ if (stateData[k] === undefined) stateData[k] = langT[k]
340
+ }
341
+ }
342
+ // Also merge top-level translation keys (skip language code keys)
343
+ const langs = new Set(poly.languages || [])
344
+ for (const k in poly.translations) {
345
+ if (!langs.has(k) && stateData[k] === undefined) {
346
+ stateData[k] = poly.translations[k]
347
+ }
348
+ }
349
+ }
350
+
351
+ element.state = isStore(stateData) ? stateData : createStore(stateData)
352
+ element.state.__element = element
353
+ element.state.root = element.state
354
+ applyStateMethods(element, element.state)
355
+ } else {
356
+ element.state = {}
357
+ // Self-reference root so consumer code can rely on `s.root` always
358
+ // being defined, matching the root-element branch above. Non-enumerable
359
+ // so Object.keys / for…in / JSON.stringify skip it (this is a plain
360
+ // object — unlike a store proxy, which hides root from ownKeys via
361
+ // the proxy trap).
362
+ Object.defineProperty(element.state, 'root', {
363
+ value: element.state, enumerable: false, configurable: true, writable: true
364
+ })
365
+ applyStateMethods(element, element.state)
366
+ }
367
+ return
368
+ }
369
+
370
+ if (typeof stateDef === 'string') {
371
+ element.state = resolveStatePath(stateDef, parent)
372
+ // Propagate root reference to plain sub-state objects (v3 compat: s.root).
373
+ // Final fallback to self-ref keeps `s.root` defined even for orphan
374
+ // string-bound state (no parent, or parent has no state).
375
+ if (element.state && typeof element.state === 'object' && !isStore(element.state)) {
376
+ const rootRef = parent?.state?.root || parent?.state || element.state
377
+ Object.defineProperty(element.state, 'root', {
378
+ value: rootRef, enumerable: false, configurable: true, writable: true
379
+ })
380
+ }
381
+ applyStateMethods(element, element.state)
382
+ return
383
+ }
384
+
385
+ if (typeof stateDef === 'function') {
386
+ const resolved = stateDef(element, parent?.state)
387
+ element.state = createStore(resolved || {})
388
+ } else {
389
+ element.state = createStore({ ...stateDef })
390
+ }
391
+
392
+ element.state.__element = element
393
+ if (parent?.state) {
394
+ element.state.parent = parent.state
395
+ // Inherit root reference from parent chain
396
+ element.state.root = parent.state.root || parent.state
397
+ } else {
398
+ // Root element: self-reference for v3 compat (state.root === state)
399
+ element.state.root = element.state
400
+ }
401
+ applyStateMethods(element, element.state)
402
+ }
403
+
404
+ function resolvePath(obj, path) {
405
+ if (!path) return obj
406
+ const parts = path.split('/')
407
+ let result = obj
408
+ for (const part of parts) {
409
+ if (result === undefined || result === null) return {}
410
+ result = result[part]
411
+ }
412
+ return result ?? {}
413
+ }
414
+
415
+ function resolveStatePath(path, parent) {
416
+ if (!parent) return {}
417
+ if (path === '.') return parent.state || {}
418
+ if (path.startsWith('../')) {
419
+ const key = path.slice(3)
420
+ return key ? resolvePath(parent.state, key) : parent.state ?? {}
421
+ }
422
+ if (path.startsWith('~/')) {
423
+ const rootState = parent.state?.root || parent.state
424
+ const key = path.slice(2)
425
+ return key ? resolvePath(rootState, key) : rootState ?? {}
426
+ }
427
+ // Check parent state first (supports nested paths like 'product/0')
428
+ if (parent.state?.[path] !== undefined) return parent.state[path]
429
+ const nested = resolvePath(parent.state, path)
430
+ if (nested !== undefined && nested !== null && Object.keys(nested).length > 0) return nested
431
+
432
+ // Fall back to polyglot translations for current language
433
+ const poly = parent.context?.polyglot
434
+ if (poly?.translations) {
435
+ const lang = parent.state?.root?.lang || poly.defaultLang || 'en'
436
+ const langTranslations = poly.translations[lang]
437
+ if (langTranslations?.[path] !== undefined) return langTranslations[path]
438
+ // Check top-level translations
439
+ if (poly.translations[path] !== undefined) return poly.translations[path]
440
+ }
441
+
442
+ return {}
443
+ }
444
+
445
+ // --- Reactive Effects ---
446
+
447
+ function registerEffects(element, definition) {
448
+ const ref = element.__ref
449
+
450
+ // Text effect
451
+ if (typeof element.text === 'function') {
452
+ const fn = element.text
453
+ const effect = createEffect(() => {
454
+ try {
455
+ let val = fn(element, element.state, element.context)
456
+ if (element.node && val != null && typeof val !== 'function' && typeof val !== 'object') {
457
+ // Resolve {{ }} templates in function return values
458
+ if (typeof val === 'string' && val.includes('{{')) {
459
+ val = replaceLiteralsWithObjectFields.call(element, val, element.state)
460
+ }
461
+ if (element.tag === 'string') {
462
+ element.node.nodeValue = val
463
+ } else {
464
+ element.node.textContent = val
465
+ }
466
+ }
467
+ } catch (e) { /* text effect may fail before state is populated */ }
468
+ }, { depth: ref.path.length })
469
+ ref.__effects.push(effect)
470
+ ref.__exec.text = fn
471
+ } else if (typeof element.text === 'string' && element.text.includes('{{')) {
472
+ const template = element.text
473
+ const effect = createEffect(() => {
474
+ const val = replaceLiteralsWithObjectFields.call(element, template, element.state)
475
+ if (val != null) element.text = val // Update element.text with resolved value
476
+ if (element.node) {
477
+ if (element.tag === 'string') {
478
+ element.node.nodeValue = val ?? ''
479
+ } else {
480
+ element.node.textContent = val ?? ''
481
+ }
482
+ }
483
+ }, { depth: ref.path.length })
484
+ ref.__effects.push(effect)
485
+ ref.__exec.text = template
486
+ }
487
+
488
+ // Reactive effects for function-valued CSS properties (CSS_PROPS_REGISTRY and DEFAULT_CSS)
489
+ for (const prop in element) {
490
+ if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
491
+ if (typeof element[prop] !== 'function') continue
492
+ if (REGISTRY.has(prop)) continue
493
+ if (isEventHandler(prop)) continue
494
+ if (ref.__exec[prop]) continue
495
+
496
+ if (CSS_PROPS_REGISTRY[prop]) {
497
+ const fn = element[prop]
498
+ const cssProp = prop
499
+ const effect = createEffect(() => {
500
+ // Push this element's design system config so CSS_PROPS_REGISTRY
501
+ // handlers (which internally call getActiveConfig() for token resolution)
502
+ // see the right config in multi-app setups (canvas editor vs. iframe project).
503
+ // The document is on the config, so getActiveDocument() resolves it too.
504
+ const ds = element.context?.designSystem
505
+ if (ds) pushConfig(ds)
506
+ try {
507
+ const val = fn(element, element.state, element.context)
508
+ const cssResult = CSS_PROPS_REGISTRY[cssProp](val, element, element.state, element.context)
509
+ if (cssResult && element.node) {
510
+ applyStyles(element.node, cssResult)
511
+ } else if (element.node && (cssProp === 'show' || cssProp === 'hide')) {
512
+ element.node.style.display = ''
513
+ }
514
+ } finally {
515
+ if (ds) popConfig()
516
+ }
517
+ }, { depth: ref.path.length })
518
+ ref.__effects.push(effect)
519
+ ref.__exec[prop] = fn
520
+ } else if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) {
521
+ const fn = element[prop]
522
+ const cssProp = prop
523
+ const effect = createEffect(() => {
524
+ const ds = element.context?.designSystem
525
+ if (ds) pushConfig(ds)
526
+ try {
527
+ const val = fn(element, element.state, element.context)
528
+ if (element.node) {
529
+ if (val != null) applyStyles(element.node, { [cssProp]: val })
530
+ else element.node.style[cssProp] = ''
531
+ }
532
+ } finally {
533
+ if (ds) popConfig()
534
+ }
535
+ }, { depth: ref.path.length })
536
+ ref.__effects.push(effect)
537
+ ref.__exec[prop] = fn
538
+ }
539
+ }
540
+
541
+
542
+ // HTML effect.
543
+ // Guard: skip when the html function returns a function (forwarded reactive
544
+ // getter from an extended component). Avoids serializing the function source
545
+ // into innerHTML — see same guard in the eager-pass block above.
546
+ if (typeof definition.html === 'function') {
547
+ const fn = definition.html
548
+ const effect = createEffect(() => {
549
+ const val = fn(element, element.state, element.context)
550
+ if (element.node && val != null && typeof val !== 'function') {
551
+ element.node.innerHTML = val
552
+ }
553
+ }, { depth: ref.path.length })
554
+ ref.__effects.push(effect)
555
+ ref.__exec.html = fn
556
+ }
557
+
558
+ // Value effect — reactively set node.value on input/textarea/select.
559
+ // Preserves cursor position for the currently focused input so a re-render
560
+ // mid-typing does not yank the caret to the end.
561
+ if (typeof definition.value === 'function') {
562
+ const fn = definition.value
563
+ const effect = createEffect(() => {
564
+ try {
565
+ const val = fn(element, element.state, element.context)
566
+ if (!element.node || !('value' in element.node)) return
567
+ const next = (val == null) ? '' : String(val)
568
+ if (element.node.value === next) return
569
+ const focused = (typeof document !== 'undefined') && document.activeElement === element.node
570
+ const selStart = focused && typeof element.node.selectionStart === 'number'
571
+ ? element.node.selectionStart : null
572
+ const selEnd = focused && typeof element.node.selectionEnd === 'number'
573
+ ? element.node.selectionEnd : null
574
+ element.node.value = next
575
+ if (focused && selStart != null && selEnd != null) {
576
+ try { element.node.setSelectionRange(selStart, selEnd) } catch (e) {}
577
+ }
578
+ } catch (e) { /* value effect may fail before state is populated */ }
579
+ }, { depth: ref.path.length })
580
+ ref.__effects.push(effect)
581
+ ref.__exec.value = fn
582
+ }
583
+
584
+ // Style effect
585
+ if (typeof definition.style === 'function') {
586
+ const fn = definition.style
587
+ const effect = createEffect(() => {
588
+ const styles = fn(element, element.state, element.context)
589
+ applyStyles(element.node, styles)
590
+ }, { depth: ref.path.length })
591
+ ref.__effects.push(effect)
592
+ ref.__exec.style = fn
593
+ }
594
+
595
+ // Attribute effects for function-valued attrs (check element.attr which includes extends)
596
+ if (element.attr && typeof element.attr === 'object') {
597
+ for (const attrName in element.attr) {
598
+ if (typeof element.attr[attrName] === 'function') {
599
+ const fn = element.attr[attrName]
600
+ const name = attrName
601
+ const effect = createEffect(() => {
602
+ try {
603
+ const val = fn(element, element.state, element.context)
604
+ if (element.node?.setAttribute) {
605
+ if (val == null || val === false) {
606
+ element.node.removeAttribute(name)
607
+ } else if (typeof val !== 'function' && typeof val !== 'object') {
608
+ element.node.setAttribute(name, val === true ? '' : val)
609
+ }
610
+ }
611
+ } catch (e) {
612
+ // Attr function may fail if element properties aren't ready yet
613
+ }
614
+ }, { depth: ref.path.length })
615
+ ref.__effects.push(effect)
616
+ }
617
+ }
618
+ }
619
+
620
+ // If condition effect
621
+ if (typeof definition.if === 'function') {
622
+ const fn = definition.if
623
+ const effect = createEffect(() => {
624
+ const ifResult = fn(element, element.state, element.context)
625
+ const wasVisible = ref.__if
626
+ ref.__if = !!ifResult
627
+
628
+ if (wasVisible && !ref.__if && element.node) {
629
+ element.node.remove()
630
+ } else if (!wasVisible && ref.__if) {
631
+ // First-time activation: run the full creation steps that were skipped
632
+ // when initial `if` was false. This builds the element's node, children,
633
+ // attrs, and events the first time the condition becomes true.
634
+ if (!ref.__ifInitialized) {
635
+ ref.__ifInitialized = true
636
+ if (!element.tag) element.tag = detectTag(element)
637
+ triggerLifecycle('Init', element, {})
638
+ if (!element.node) element.node = cacheNode(element)
639
+ if (!element.node) return
640
+ triggerLifecycle('AttachNode', element, {})
641
+ if (element.node.setAttribute && element.key) {
642
+ element.node.setAttribute('data-key', element.key)
643
+ }
644
+ if (element.id && element.node.setAttribute) {
645
+ element.node.id = element.id
646
+ }
647
+ // Register reactive effects for dynamic props (excluding `if` to avoid recursion)
648
+ const prevIf = definition.if
649
+ delete definition.if
650
+ try { registerEffects(element, definition) } finally { definition.if = prevIf }
651
+ applyStaticMixins(element)
652
+ applyAttrsInProps(element)
653
+ registerEvents(element, {})
654
+ createChildren(element, {})
655
+ assignNode(element, element.parent)
656
+ } else {
657
+ // Subsequent re-activations: just re-attach existing node
658
+ if (!element.node) element.node = cacheNode(element)
659
+ // Find the next sibling in DOMQL tree that has a node, to insert before it
660
+ const parent = element.parent
661
+ const parentNode = parent?.node
662
+ if (parentNode && element.node) {
663
+ let nextSibNode = null
664
+ const keys = Object.keys(parent)
665
+ const myIdx = keys.indexOf(element.key)
666
+ for (let i = myIdx + 1; i < keys.length; i++) {
667
+ const sib = parent[keys[i]]
668
+ if (sib?.node?.parentNode === parentNode) {
669
+ nextSibNode = sib.node
670
+ break
671
+ }
672
+ }
673
+ if (nextSibNode) {
674
+ parentNode.insertBefore(element.node, nextSibNode)
675
+ } else {
676
+ parentNode.appendChild(element.node)
677
+ }
678
+ } else {
679
+ assignNode(element, element.parent)
680
+ }
681
+ applyStaticMixins(element)
682
+ }
683
+ }
684
+ }, { depth: ref.path.length })
685
+ ref.__effects.push(effect)
686
+ }
687
+
688
+ // CSS-in-props effects (dynamic CSS properties as functions)
689
+ for (const prop in element) {
690
+ if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
691
+ if (REGISTRY.has(prop)) continue
692
+ if (typeof element[prop] !== 'function') continue
693
+
694
+ // Skip child components (Uppercase)
695
+ const firstChar = prop.charCodeAt(0)
696
+ if (firstChar >= 65 && firstChar <= 90) continue
697
+
698
+ // Skip onXxx event handlers
699
+ if (isEventHandler(prop)) continue
700
+
701
+ const fn = element[prop]
702
+ ref.__exec[prop] = fn
703
+
704
+ // Reactive effects for function-valued HTML attributes (src, href, etc.)
705
+ // Skip props already handled as CSS properties
706
+ const tag = element.tag || 'div'
707
+ if (checkAttributeByTagName(tag, prop) && !CSS_PROPS_REGISTRY[prop] && !(DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop))) {
708
+ const attrName = prop
709
+ const effect = createEffect(() => {
710
+ try {
711
+ const val = fn(element, element.state, element.context)
712
+ if (element.node?.setAttribute) {
713
+ if (val == null || val === false) {
714
+ element.node.removeAttribute(attrName)
715
+ } else if (typeof val !== 'function' && typeof val !== 'object') {
716
+ // Use property setter for src on iframes/media to trigger navigation
717
+ if (attrName === 'src' && 'src' in element.node) {
718
+ element.node.src = val
719
+ } else {
720
+ element.node.setAttribute(attrName, val === true ? '' : val)
721
+ }
722
+ }
723
+ }
724
+ } catch (e) { /* attr function may fail before state is populated */ }
725
+ }, { depth: ref.path.length })
726
+ ref.__effects.push(effect)
727
+ }
728
+ }
729
+
730
+ // Reactive conditionals (.prop / !prop blocks)
731
+ registerConditionalEffects(element)
732
+ }
733
+
734
+ // --- Static Mixin Application ---
735
+
736
+ function applyStaticMixins(element) {
737
+ const { node } = element
738
+ if (!node) return
739
+
740
+ // Text (skip template literals — handled by reactive effect)
741
+ if (typeof element.text === 'string' && !element.text.includes('{{')) {
742
+ if (element.tag === 'string') {
743
+ node.nodeValue = element.text
744
+ } else {
745
+ node.textContent = element.text
746
+ }
747
+ } else if (typeof element.text === 'number') {
748
+ node.textContent = element.text
749
+ }
750
+
751
+ // CSS-in-props static transforms (show, hide, padding, flexAlign, etc.)
752
+ // Conditionals (.prop / !prop) are handled reactively in registerConditionalEffects
753
+ applyCssInProps(element)
754
+
755
+ // HTML
756
+ if (typeof element.html === 'string') {
757
+ node.innerHTML = element.html
758
+ }
759
+
760
+ // Static value for input/textarea/select. The reactive effect path
761
+ // (see registerEffects) handles function-valued `value:` props.
762
+ if (
763
+ element.value != null &&
764
+ typeof element.value !== 'function' &&
765
+ 'value' in node
766
+ ) {
767
+ const next = String(element.value)
768
+ if (node.value !== next) node.value = next
769
+ }
770
+
771
+ // Style
772
+ if (element.style && typeof element.style === 'object') {
773
+ applyStyles(node, element.style)
774
+ }
775
+
776
+ // Attributes
777
+ if (element.attr && typeof element.attr === 'object') {
778
+ for (const key in element.attr) {
779
+ const val = element.attr[key]
780
+ if (typeof val === 'function' || typeof val === 'object') continue
781
+ if (val == null || val === false) continue
782
+ if (node.setAttribute) {
783
+ node.setAttribute(key, val === true ? '' : val)
784
+ }
785
+ }
786
+ }
787
+
788
+ // Data attributes
789
+ if (element.data && typeof element.data === 'object') {
790
+ for (const key in element.data) {
791
+ if (node.dataset) node.dataset[key] = element.data[key]
792
+ }
793
+ }
794
+
795
+ // Classlist
796
+ if (element.classlist || element.class) {
797
+ const cls = element.classlist || element.class
798
+ if (typeof cls === 'string') {
799
+ cls.split(' ').forEach(c => c && node.classList.add(c))
800
+ } else if (typeof cls === 'object') {
801
+ for (const name in cls) {
802
+ const val = typeof cls[name] === 'function'
803
+ ? cls[name](element, element.state, element.context)
804
+ : cls[name]
805
+ if (val) node.classList.add(typeof val === 'string' ? val : name)
806
+ }
807
+ }
808
+ }
809
+ }
810
+
811
+ function applyStyles(node, styles) {
812
+ if (!node || !styles || !node.style) return
813
+
814
+ let cssRules = null
815
+
816
+ for (const prop in styles) {
817
+ const val = styles[prop]
818
+ if (val == null) continue
819
+
820
+ // Handle &: selectors — generate CSS rules
821
+ if (prop.charAt(0) === '&' && typeof val === 'object') {
822
+ if (!cssRules) cssRules = {}
823
+ cssRules[prop] = val
824
+ continue
825
+ }
826
+
827
+ try {
828
+ const str = String(val)
829
+ if (str.includes('!important')) {
830
+ const cssProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
831
+ node.style.setProperty(cssProp, str.replace(/\s*!important\s*$/, ''), 'important')
832
+ } else {
833
+ node.style[prop] = val
834
+ }
835
+ } catch (e) {
836
+ // Invalid style value
837
+ }
838
+ }
839
+
840
+ // Inject CSS rules for &: selectors
841
+ if (cssRules) {
842
+ injectScopedCSS(node, cssRules)
843
+ }
844
+ }
845
+
846
+ let _scopedStyleId = 0
847
+
848
+ function injectScopedCSS(node, rules) {
849
+ const id = `_ds${_scopedStyleId++}`
850
+ node.classList.add(id)
851
+
852
+ const doc = node.ownerDocument
853
+ let styleEl = doc.querySelector('style[data-smbls-scoped]')
854
+ if (!styleEl) {
855
+ styleEl = doc.createElement('style')
856
+ styleEl.setAttribute('data-smbls-scoped', '')
857
+ doc.head.appendChild(styleEl)
858
+ }
859
+
860
+ let css = ''
861
+ for (const selector in rules) {
862
+ // Replace & with scoped class, and auto-scope unscoped comma-separated parts
863
+ const scopedClass = '.' + id
864
+ const resolvedSelector = selector.replace(/&/g, scopedClass)
865
+ .split(',')
866
+ .map(s => {
867
+ const t = s.trim()
868
+ return t.includes(scopedClass) ? t : scopedClass + t
869
+ })
870
+ .join(', ')
871
+ const val = rules[selector]
872
+
873
+ if (typeof val === 'object') {
874
+ // Check for nested selectors (e.g. '& [dropdown]' inside '&:focus-within')
875
+ let props = ''
876
+ let nested = ''
877
+ for (const k in val) {
878
+ const v = val[k]
879
+ if (typeof v === 'object') {
880
+ const nestedSelector = k.replace(/&/g, '.' + id)
881
+ const nestedSuffix = ' ' + nestedSelector.replace('.' + id, '')
882
+ const fullSelector = resolvedSelector.split(', ').map(s => s + nestedSuffix).join(', ')
883
+ let nestedProps = ''
884
+ for (const nk in v) {
885
+ nestedProps += `${nk.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v[nk]};`
886
+ }
887
+ nested += `${fullSelector} { ${nestedProps} }\n`
888
+ } else {
889
+ props += `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v};`
890
+ }
891
+ }
892
+ if (props) css += `${resolvedSelector} { ${props} }\n`
893
+ if (nested) css += nested
894
+ }
895
+ }
896
+
897
+ if (css) styleEl.textContent += css
898
+ }
899
+
900
+ // --- Conditionals ---
901
+ // Reactive `.propName` (truthy) and `!propName` (falsy) blocks.
902
+ //
903
+ // Each conditional becomes its own `createEffect` so signal-driven predicates
904
+ // (state reads inside a function-valued prop) re-fire it. Block values are
905
+ // applied via inline styles (with CSS_PROPS_REGISTRY token resolution) and
906
+ // reverted by clearing the inline keys when the predicate flips off — the
907
+ // static class never bakes in the conditional values, so there is nothing to
908
+ // unmerge on revert.
909
+ //
910
+ // Imperative updates to a non-signal predicate (e.g. `element.update({ isActive })`
911
+ // where `isActive` is a plain element prop, not in state) re-run via
912
+ // `__conditionalRunners`, which `update.js` invokes after applying the prop.
913
+
914
+ function registerConditionalEffects(element) {
915
+ const ref = element.__ref
916
+ ref.__conditionalRunners = []
917
+
918
+ for (const key in element) {
919
+ if (!Object.prototype.hasOwnProperty.call(element, key)) continue
920
+ const c0 = key.charAt(0)
921
+ if (c0 !== '.' && c0 !== '!') continue
922
+
923
+ const propName = key.slice(1)
924
+ if (!propName) continue
925
+
926
+ const block = element[key]
927
+ if (!block || typeof block !== 'object') continue
928
+
929
+ const negate = c0 === '!'
930
+ const local = { lastMatch: false, appliedKeys: null }
931
+
932
+ const evaluate = () => {
933
+ if (!element.node) return
934
+
935
+ // Resolution order matches css-in-props transformer
936
+ // (`applyConditionalCaseProps`): element prop → state → context.cases.
937
+ // Reading `element.state[propName]` inside the effect subscribes to the
938
+ // signal so state-only predicates re-fire on `state.update`.
939
+ let resolved
940
+ const propVal = element[propName]
941
+ if (propVal === undefined) {
942
+ if (element.state && propName in element.state) {
943
+ resolved = element.state[propName]
944
+ } else {
945
+ const caseFn = element.context?.cases?.[propName]
946
+ if (caseFn === undefined) return
947
+ resolved = typeof caseFn === 'function' ? caseFn.call(element, element) : caseFn
948
+ }
949
+ } else if (typeof propVal === 'function') {
950
+ resolved = propVal(element, element.state, element.context)
951
+ } else {
952
+ resolved = propVal
953
+ }
954
+
955
+ const match = negate ? !resolved : !!resolved
956
+
957
+ if (match === local.lastMatch) return
958
+ local.lastMatch = match
959
+
960
+ const ds = element.context?.designSystem
961
+ if (ds) pushConfig(ds)
962
+ try {
963
+ if (match) {
964
+ const styles = {}
965
+ for (const prop in block) {
966
+ if (!Object.prototype.hasOwnProperty.call(block, prop)) continue
967
+ const raw = typeof block[prop] === 'function'
968
+ ? block[prop](element, element.state, element.context)
969
+ : block[prop]
970
+ if (raw == null) continue
971
+
972
+ if (CSS_PROPS_REGISTRY[prop]) {
973
+ const r = CSS_PROPS_REGISTRY[prop](raw, element, element.state, element.context)
974
+ if (r) {
975
+ const arr = Array.isArray(r) ? r : [r]
976
+ for (const o of arr) if (o) Object.assign(styles, o)
977
+ }
978
+ } else if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) {
979
+ styles[prop] = raw
980
+ }
981
+ }
982
+ const keys = Object.keys(styles)
983
+ if (keys.length) {
984
+ applyStyles(element.node, styles)
985
+ local.appliedKeys = keys
986
+ } else {
987
+ local.appliedKeys = null
988
+ }
989
+ } else if (local.appliedKeys) {
990
+ for (const k of local.appliedKeys) element.node.style[k] = ''
991
+ local.appliedKeys = null
992
+ }
993
+ } finally {
994
+ if (ds) popConfig()
995
+ }
996
+ }
997
+
998
+ ref.__conditionalRunners.push(evaluate)
999
+
1000
+ const effect = createEffect(evaluate, { depth: ref.path.length })
1001
+ ref.__effects.push(effect)
1002
+ }
1003
+ }
1004
+
1005
+ // --- CSS-in-props ---
1006
+ // Processes element properties through CSS_PROPS_REGISTRY transforms
1007
+
1008
+ const __APPLY_CSS_STACK = new WeakSet()
1009
+ function applyCssInProps(element) {
1010
+ const { node } = element
1011
+ if (!node) return
1012
+
1013
+ if (__APPLY_CSS_STACK.has(element)) {
1014
+ console.error('[applyCssInProps] re-entry detected for element', element.key, 'prop path:', element.__ref?.path)
1015
+ return
1016
+ }
1017
+ __APPLY_CSS_STACK.add(element)
1018
+
1019
+ const ds = element.context?.designSystem
1020
+ if (ds) pushConfig(ds)
1021
+
1022
+ try {
1023
+ const cssStyles = {}
1024
+ let hasStyles = false
1025
+
1026
+ for (const prop in element) {
1027
+ if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
1028
+ if (typeof element[prop] === 'function') continue
1029
+ if (REGISTRY.has(prop)) continue
1030
+
1031
+ const firstChar = prop.charCodeAt(0)
1032
+ if (firstChar >= 65 && firstChar <= 90) continue
1033
+ if (isEventHandler(prop)) continue
1034
+
1035
+ if (CSS_PROPS_REGISTRY[prop]) {
1036
+ const val = element[prop]
1037
+ const cssResult = CSS_PROPS_REGISTRY[prop](val, element, element.state, element.context)
1038
+ if (cssResult) {
1039
+ if (Array.isArray(cssResult)) {
1040
+ for (const r of cssResult) if (r) Object.assign(cssStyles, r)
1041
+ } else {
1042
+ Object.assign(cssStyles, cssResult)
1043
+ }
1044
+ hasStyles = true
1045
+ }
1046
+ } else if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) {
1047
+ const val = element[prop]
1048
+ if (val != null && typeof val !== 'object') {
1049
+ cssStyles[prop] = val
1050
+ hasStyles = true
1051
+ }
1052
+ } else if (prop.charCodeAt(0) === 45 && prop.charCodeAt(1) === 45) {
1053
+ // CSS custom properties (--var-name): apply as inline style
1054
+ const val = element[prop]
1055
+ if (val != null && typeof val !== 'object') {
1056
+ cssStyles[prop] = val
1057
+ hasStyles = true
1058
+ }
1059
+ } else if (prop.charAt(0) === '@' || prop.charAt(0) === ':') {
1060
+ // Selector-based CSS: @dark, @media, :hover
1061
+ const val = element[prop]
1062
+ if (val && typeof val === 'object') {
1063
+ const resolved = {}
1064
+ for (const k in val) {
1065
+ let v = val[k]
1066
+ // Evaluate function-valued CSS within selector blocks (e.g. @dark background)
1067
+ if (typeof v === 'function') v = v(element, element.state, element.context)
1068
+ if (v == null || typeof v === 'function') continue
1069
+ if (CSS_PROPS_REGISTRY[k]) {
1070
+ const result = CSS_PROPS_REGISTRY[k](v, element, element.state, element.context)
1071
+ if (result) {
1072
+ if (Array.isArray(result)) { for (const r of result) if (r) Object.assign(resolved, r) }
1073
+ else Object.assign(resolved, result)
1074
+ }
1075
+ } else {
1076
+ resolved[k] = v
1077
+ }
1078
+ }
1079
+ if (Object.keys(resolved).length) {
1080
+ // Per-element theme selectors (`@dark`, `@light`, or any custom
1081
+ // scheme `@name`) all compile to a single
1082
+ // `[data-theme="<scheme>"] &`
1083
+ // rule. The !important marker ensures the themed value wins over
1084
+ // the default (the unconditional property on the same element —
1085
+ // e.g. `background: 'neutral100'`).
1086
+ //
1087
+ // `globalTheme: 'auto'` is handled outside the CSS: `scratch.set`
1088
+ // watches `prefers-color-scheme` and writes `data-theme` on the
1089
+ // scope root to follow OS changes live. That keeps a single CSS
1090
+ // selector pattern — no `@media` duplicates to parse — and lets
1091
+ // custom themes like `@ocean` use the same mechanism.
1092
+ //
1093
+ // We intentionally treat `@dark`/`@light` as theme selectors even
1094
+ // if `designSystem.media.dark` / `.light` is defined — projects
1095
+ // historically mapped them to `[data-theme="…"]` there, and
1096
+ // routing through the media-shortcut branch would emit invalid
1097
+ // `@media [data-theme="…"]` rules.
1098
+ const isThemeSelector = prop.length > 1 && prop.charAt(0) === '@' &&
1099
+ !prop.startsWith('@media') && !prop.startsWith('@keyframes') && !prop.startsWith('@supports') &&
1100
+ (prop === '@dark' || prop === '@light' ||
1101
+ !element.context?.designSystem?.media?.[prop.slice(1)])
1102
+ if (isThemeSelector) {
1103
+ const important = {}
1104
+ for (const rk in resolved) {
1105
+ const rv = resolved[rk]
1106
+ important[rk] = typeof rv === 'string' && !rv.includes('!important') ? rv + ' !important' : rv
1107
+ }
1108
+ const scheme = prop.slice(1)
1109
+ cssStyles[`[data-theme="${scheme}"] &`] = important
1110
+ } else {
1111
+ // Resolve design system media shortcuts (@mobileL -> @media (max-width: 768px))
1112
+ let cssProp = prop
1113
+ if (prop.charAt(0) === '@' && !prop.startsWith('@media') && !prop.startsWith('@keyframes')) {
1114
+ const mediaKey = prop.slice(1)
1115
+ const mediaVal = element.context?.designSystem?.media?.[mediaKey] ||
1116
+ element.context?.designSystem?.media?.[mediaKey]
1117
+ if (mediaVal) cssProp = `@media ${mediaVal}`
1118
+ }
1119
+ // Separate flat CSS props from nested selector objects
1120
+ const flat = {}
1121
+ for (const rk in resolved) {
1122
+ const rv = resolved[rk]
1123
+ if (rv && typeof rv === 'object' && !Array.isArray(rv)) {
1124
+ // Nested selector (e.g. '> hgroup > h6': { color: 'blue' })
1125
+ // Resolve CSS tokens in nested values
1126
+ const nestedResolved = {}
1127
+ for (const nk in rv) {
1128
+ if (CSS_PROPS_REGISTRY[nk]) {
1129
+ const nr = CSS_PROPS_REGISTRY[nk](rv[nk], element, element.state, element.context)
1130
+ if (nr) {
1131
+ if (Array.isArray(nr)) { for (const r of nr) if (r) Object.assign(nestedResolved, r) }
1132
+ else Object.assign(nestedResolved, nr)
1133
+ }
1134
+ } else {
1135
+ nestedResolved[nk] = rv[nk]
1136
+ }
1137
+ }
1138
+ // Pseudo-selectors attach directly, combinators need space
1139
+ const sep = rk.startsWith(':') ? '' : ' '
1140
+ // Distribute nested selector across comma-separated parts
1141
+ if (cssProp.includes(',')) {
1142
+ const distributed = cssProp.split(',').map(s => `${s.trim()}${sep}${rk}`).join(', ')
1143
+ cssStyles[distributed] = nestedResolved
1144
+ } else {
1145
+ cssStyles[`${cssProp}${sep}${rk}`] = nestedResolved
1146
+ }
1147
+ } else {
1148
+ flat[rk] = rv
1149
+ }
1150
+ }
1151
+ if (Object.keys(flat).length) cssStyles[cssProp] = flat
1152
+ }
1153
+ hasStyles = true
1154
+ }
1155
+ hasStyles = true
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ if (hasStyles) {
1161
+ // Document already set by the outer push at the top of applyCssInProps
1162
+
1163
+ // Apply display:none !important as inline style (for show/hide to work reliably)
1164
+ if (cssStyles.display && String(cssStyles.display).includes('!important')) {
1165
+ node.style.setProperty('display', String(cssStyles.display).replace(/\s*!important\s*$/, ''), 'important')
1166
+ delete cssStyles.display
1167
+ }
1168
+
1169
+ const className = atomicCss(cssStyles, null, element.context?.cssPrefix)
1170
+ if (className && node.classList) {
1171
+ className.split(' ').forEach(c => c && node.classList.add(c))
1172
+ }
1173
+
1174
+ // Enforce explicit display when it conflicts with generated display from flow/align
1175
+ // Also handle hide:true / show:false (which set display:none via CSS_PROPS_REGISTRY)
1176
+ const effectiveDisplay = element.display != null && typeof element.display !== 'function'
1177
+ ? element.display
1178
+ : (element.hide === true || element.show === false) ? 'none' : null
1179
+ if (effectiveDisplay != null) {
1180
+ const generatedDisplay = cssStyles.display
1181
+ if (generatedDisplay && generatedDisplay !== effectiveDisplay) {
1182
+ if (effectiveDisplay === 'none') {
1183
+ // For display:none, remove conflicting atomic display classes instead of inline style
1184
+ // (inline style would block conditional show/hide from updating later)
1185
+ const toRemove = []
1186
+ for (const cls of node.classList) {
1187
+ if (cls.startsWith('_d-') && cls !== '_d-none') toRemove.push(cls)
1188
+ }
1189
+ for (const cls of toRemove) node.classList.remove(cls)
1190
+ node.classList.add('_d-none')
1191
+ } else {
1192
+ // For grid/block/inline, inline style is safe
1193
+ node.style.display = element.display
1194
+ }
1195
+ }
1196
+ }
1197
+ }
1198
+ } finally {
1199
+ if (ds) popConfig()
1200
+ __APPLY_CSS_STACK.delete(element)
1201
+ }
1202
+ }
1203
+
1204
+ // --- Attrs-in-props ---
1205
+ // Applies HTML attributes from element properties based on tag
1206
+
1207
+ function applyAttrsInProps(element) {
1208
+ const { node } = element
1209
+ if (!node || !node.setAttribute) return
1210
+
1211
+ // Apply attribute transforms (src, href, poster, etc. with file/template resolution)
1212
+ const transformed = applyAttrTransforms(element)
1213
+ for (const attr in transformed) {
1214
+ const val = transformed[attr]
1215
+ if (val != null && val !== false && typeof val !== 'function' && typeof val !== 'object') {
1216
+ node.setAttribute(attr, val === true ? '' : val)
1217
+ }
1218
+ }
1219
+
1220
+ const tag = element.tag || 'div'
1221
+ for (const prop in element) {
1222
+ if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
1223
+ if (typeof element[prop] === 'function') continue
1224
+ if (REGISTRY.has(prop)) continue
1225
+ if (prop === 'on') continue
1226
+ if (transformed[prop] !== undefined) continue
1227
+
1228
+ const firstChar = prop.charCodeAt(0)
1229
+ if (firstChar >= 65 && firstChar <= 90) continue
1230
+ if (isEventHandler(prop)) continue
1231
+
1232
+ // Skip CSS properties — already handled by applyCssInProps via CSS.
1233
+ // Setting design system tokens (e.g. 'A') as SVG/HTML attributes causes browser errors.
1234
+ if (CSS_PROPS_REGISTRY[prop] || (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop))) continue
1235
+
1236
+ // Check if this prop is a valid HTML attribute for this tag.
1237
+ // Note: do NOT pre-guard with `HTML_ATTRIBUTES[prop]` — that map is keyed
1238
+ // by tag name, not attribute name, so the guard would only ever pass for
1239
+ // props whose name happens to also be a tag (e.g. `a`, `div`). The proper
1240
+ // check is checkAttributeByTagName, which inspects the tag's attribute list.
1241
+ if (checkAttributeByTagName(tag, prop)) {
1242
+ let val = element[prop]
1243
+ if (val == null || val === false) continue
1244
+
1245
+ // Resolve {{ }} template literals
1246
+ if (typeof val === 'string' && val.includes('{{')) {
1247
+ val = replaceLiteralsWithObjectFields(val, element, element.state, element.context)
1248
+ element[prop] = val
1249
+ }
1250
+
1251
+ if (val == null || val === false || typeof val === 'function' || typeof val === 'object') continue
1252
+ node.setAttribute(prop, val === true ? '' : val)
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ // --- Events ---
1258
+ // v3 syntax: onXxx handlers directly on the component (no `on: {}` wrapper)
1259
+
1260
+ function registerEvents(element, options) {
1261
+ for (const key in element) {
1262
+ if (!Object.prototype.hasOwnProperty.call(element, key)) continue
1263
+ if (!isEventHandler(key)) continue
1264
+
1265
+ let handler = element[key]
1266
+
1267
+ // String shortcut: onClick: 'fnName' → el.call('fnName', ev)
1268
+ if (typeof handler === 'string') {
1269
+ const fnName = handler
1270
+ handler = function stringHandlerShortcut (ev) {
1271
+ return element.call(fnName, ev)
1272
+ }
1273
+ }
1274
+
1275
+ if (typeof handler !== 'function') continue
1276
+
1277
+ const eventName = eventNameFrom(key)
1278
+ if (LIFECYCLE_EVENTS.has(eventName)) continue
1279
+
1280
+ registerEvent(toDomEventName(eventName), element, handler)
1281
+ }
1282
+ }
1283
+
1284
+ // --- Children ---
1285
+
1286
+ // Deep merge childProps with childDef — nested objects are merged, not replaced
1287
+ function deepMergeChildProps(base, override) {
1288
+ const result = { ...base }
1289
+ for (const key in override) {
1290
+ const bVal = result[key]
1291
+ const oVal = override[key]
1292
+ if (bVal && oVal && typeof bVal === 'object' && typeof oVal === 'object' &&
1293
+ !Array.isArray(bVal) && !Array.isArray(oVal) &&
1294
+ typeof bVal !== 'function' && typeof oVal !== 'function') {
1295
+ result[key] = deepMergeChildProps(bVal, oVal)
1296
+ } else {
1297
+ result[key] = oVal
1298
+ }
1299
+ }
1300
+ return result
1301
+ }
1302
+
1303
+ // Renders resolved children (array, object, or string state ref) into the element's DOM
1304
+ function renderChildren(element, children, options) {
1305
+ if (!children || !element.node) return
1306
+
1307
+ const childExtends = element.childExtends
1308
+
1309
+ if (Array.isArray(children)) {
1310
+ const doc = element.context?.document || globalThis.document
1311
+ const fragment = doc?.createDocumentFragment()
1312
+ if (!fragment) return
1313
+
1314
+ const childPropsRaw = element.childProps
1315
+ const childrenAs = element.childrenAs
1316
+
1317
+ for (let i = 0; i < children.length; i++) {
1318
+ let childDef = children[i]
1319
+ const rawChild = childDef
1320
+
1321
+ if (typeof childDef === 'string' || typeof childDef === 'number') {
1322
+ childDef = { text: childDef }
1323
+ }
1324
+
1325
+ // Resolve function-valued childProps per child
1326
+ let childProps = childPropsRaw
1327
+ if (typeof childPropsRaw === 'function') {
1328
+ try {
1329
+ childProps = childPropsRaw(element, childDef)
1330
+ } catch (e) {
1331
+ childProps = null
1332
+ }
1333
+ }
1334
+
1335
+ if (childrenAs === 'state') {
1336
+ // For string/number children, create state with both value and text for v3 compat
1337
+ let stateVal = rawChild
1338
+ if (typeof rawChild === 'string' || typeof rawChild === 'number') {
1339
+ stateVal = { value: rawChild, text: rawChild }
1340
+ }
1341
+ childDef = childProps ? { ...childProps, state: stateVal } : { state: stateVal }
1342
+ } else if (childProps) {
1343
+ childDef = deepMergeChildProps(childProps, childDef)
1344
+ }
1345
+
1346
+ if (childExtends && !childDef.ignoreChildExtends) {
1347
+ if (childDef.extends) {
1348
+ const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
1349
+ const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
1350
+ childDef = { ...childDef, extends: [...parent, ...own] }
1351
+ } else {
1352
+ childDef = { extends: childExtends, ...childDef }
1353
+ }
1354
+ }
1355
+
1356
+ const childKey = childDef?.key || `${i}`
1357
+ const child = create(childDef, element, childKey, options)
1358
+ if (child?.node) fragment.appendChild(child.node)
1359
+ }
1360
+
1361
+ element.node.appendChild(fragment)
1362
+ return
1363
+ }
1364
+
1365
+ if (typeof children === 'string') {
1366
+ const stateVal = resolveStatePath(children, element)
1367
+ if (Array.isArray(stateVal)) {
1368
+ renderChildren(element, stateVal, options)
1369
+ return
1370
+ }
1371
+ }
1372
+
1373
+ if (typeof children === 'object') {
1374
+ for (const childKey in children) {
1375
+ if (REGISTRY.has(childKey)) continue
1376
+
1377
+ let childDef = children[childKey]
1378
+ if (childDef == null || childDef === false) continue
1379
+
1380
+ if (typeof childDef === 'string' || typeof childDef === 'number') {
1381
+ childDef = { text: childDef }
1382
+ }
1383
+
1384
+ if (element.childProps) {
1385
+ childDef = deepMergeChildProps(element.childProps, childDef)
1386
+ }
1387
+
1388
+ if (childExtends && !childDef.ignoreChildExtends) {
1389
+ if (childDef.extends) {
1390
+ const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
1391
+ const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
1392
+ childDef = { ...childDef, extends: [...parent, ...own] }
1393
+ } else {
1394
+ childDef = { extends: childExtends, ...childDef }
1395
+ }
1396
+ }
1397
+
1398
+ create(childDef, element, childKey, options)
1399
+ }
1400
+ }
1401
+ }
1402
+
1403
+ function createChildren(element, options) {
1404
+ let { children, content } = element
1405
+ const childExtends = element.childExtends
1406
+
1407
+ // Resolve function-valued children with reactive tracking
1408
+ if (typeof children === 'function') {
1409
+ const childrenFn = children
1410
+ children = childrenFn(element, element.state, element.context)
1411
+ element.children = children
1412
+
1413
+ // Reactive effect: keyed reconciliation on update.
1414
+ //
1415
+ // Previously we wiped every child and rebuilt from scratch whenever
1416
+ // the children factory produced a new result. That destroyed any
1417
+ // DOM state inside the subtree — most visibly, an `<input>` being
1418
+ // typed into would lose focus after every keystroke because its
1419
+ // node was detached and replaced.
1420
+ //
1421
+ // Now we diff the incoming array against `ref.__children`:
1422
+ // • keys still present → update the child's reactive state in
1423
+ // place so its subscribers re-render,
1424
+ // but the DOM node (and its focus /
1425
+ // selection state) is preserved.
1426
+ // • keys gone → remove + delete.
1427
+ // • new keys → create and append.
1428
+ //
1429
+ // Non-array results and non-`childrenAs: 'state'` children still
1430
+ // fall through to the full rebuild path since we can't safely
1431
+ // update their internals in place.
1432
+ const ref = element.__ref
1433
+ let initialized = false
1434
+ const effect = createEffect(() => {
1435
+ const result = childrenFn(element, element.state, element.context)
1436
+ if (!initialized) { initialized = true; return }
1437
+ if (result === element.children) return
1438
+
1439
+ element.children = result
1440
+
1441
+ // Fast path: keyed reconciliation for array children + childrenAs=state.
1442
+ // This is the only case where we can safely update in place, because
1443
+ // each child owns a reactive store whose fields we can patch.
1444
+ const isArr = Array.isArray(result)
1445
+ const canReconcile = isArr && element.childrenAs === 'state'
1446
+
1447
+ if (canReconcile) {
1448
+ // Compute the new key order. Key resolution mirrors renderChildren:
1449
+ // a `childProps` factory can lift `key` to the top level of the
1450
+ // synthesized childDef; otherwise fall back to `rawChild.key`,
1451
+ // finally falling back to the positional index.
1452
+ const childPropsRaw = element.childProps
1453
+ const newOrder = []
1454
+ const itemByKey = new Map()
1455
+ for (let i = 0; i < result.length; i++) {
1456
+ const rawChild = result[i]
1457
+ let childProps = childPropsRaw
1458
+ if (typeof childPropsRaw === 'function') {
1459
+ try { childProps = childPropsRaw(element, rawChild) } catch { childProps = null }
1460
+ }
1461
+ const key = (childProps && childProps.key) || (rawChild && rawChild.key) || `${i}`
1462
+ newOrder.push(key)
1463
+ itemByKey.set(key, { rawChild, childProps })
1464
+ }
1465
+
1466
+ const newKeySet = new Set(newOrder)
1467
+ // Remove children whose keys are no longer present.
1468
+ for (let i = ref.__children.length - 1; i >= 0; i--) {
1469
+ const k = ref.__children[i]
1470
+ if (!newKeySet.has(k)) {
1471
+ const child = element[k]
1472
+ if (child?.remove) child.remove()
1473
+ delete element[k]
1474
+ ref.__children.splice(i, 1)
1475
+ }
1476
+ }
1477
+
1478
+ // Upsert children in order. Existing keys reuse their node and
1479
+ // patch their state; new keys are created and appended.
1480
+ const childExtends = element.childExtends
1481
+ const doc = element.context?.document || globalThis.document
1482
+ const fragment = doc?.createDocumentFragment()
1483
+ for (const key of newOrder) {
1484
+ const entry = itemByKey.get(key)
1485
+ const rawChild = entry.rawChild
1486
+ const existing = element[key]
1487
+
1488
+ if (existing && existing.state) {
1489
+ // Patch the existing store with the new item's fields via
1490
+ // `state.replace(obj)` — unlike `update` (which only merges
1491
+ // new keys) `replace` also REMOVES any prior keys that
1492
+ // aren't on the new object. Critical for list reconcilers:
1493
+ // without replace semantics, a row that went from parent
1494
+ // summary → sub-row would inherit the old `_isParent: true`
1495
+ // flag and render both roles simultaneously.
1496
+ //
1497
+ // Signals fire for added / changed / removed keys, so the
1498
+ // child's `text` / `hide` / `style` gates re-evaluate and
1499
+ // the DOM node (+ any focused input inside) stays untouched.
1500
+ if (rawChild && typeof rawChild === 'object') {
1501
+ const st = existing.state
1502
+ if (typeof st.replace === 'function') {
1503
+ st.replace(rawChild)
1504
+ } else if (typeof st.update === 'function') {
1505
+ // Fallback for stores without replace(): emulate it by
1506
+ // nulling keys not in the new object, then update().
1507
+ for (const k2 in st) {
1508
+ if (k2 === '__element' || k2 === 'root' || k2 === 'parent' || k2 === 'update') continue
1509
+ if (!Object.prototype.hasOwnProperty.call(rawChild, k2)) {
1510
+ try { st[k2] = undefined } catch {}
1511
+ }
1512
+ }
1513
+ st.update(rawChild)
1514
+ } else {
1515
+ for (const k2 in rawChild) st[k2] = rawChild[k2]
1516
+ }
1517
+ }
1518
+ continue
1519
+ }
1520
+
1521
+ // New child — build the childDef the same way renderChildren does,
1522
+ // then create + append.
1523
+ let stateVal = rawChild
1524
+ if (typeof rawChild === 'string' || typeof rawChild === 'number') {
1525
+ stateVal = { value: rawChild, text: rawChild }
1526
+ }
1527
+ const childProps = entry.childProps
1528
+ let childDef = childProps ? { ...childProps, state: stateVal } : { state: stateVal }
1529
+
1530
+ if (childExtends && !childDef.ignoreChildExtends) {
1531
+ if (childDef.extends) {
1532
+ const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
1533
+ const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
1534
+ childDef = { ...childDef, extends: [...parent, ...own] }
1535
+ } else {
1536
+ childDef = { extends: childExtends, ...childDef }
1537
+ }
1538
+ }
1539
+
1540
+ const child = create(childDef, element, key, options)
1541
+ if (child?.node && fragment) fragment.appendChild(child.node)
1542
+ }
1543
+ if (fragment && fragment.childNodes.length && element.node) {
1544
+ element.node.appendChild(fragment)
1545
+ }
1546
+
1547
+ // Keep __children in the new order for stable future reconciliations.
1548
+ ref.__children = newOrder
1549
+ return
1550
+ }
1551
+
1552
+ // Fallback: full rebuild (non-array results, or childrenAs not set).
1553
+ for (const key of [...ref.__children]) {
1554
+ const child = element[key]
1555
+ if (child?.remove) child.remove()
1556
+ delete element[key]
1557
+ }
1558
+ ref.__children = []
1559
+ renderChildren(element, result, options)
1560
+ }, { depth: ref.path.length })
1561
+ ref.__effects.push(effect)
1562
+ }
1563
+
1564
+ renderChildren(element, children || content, options)
1565
+
1566
+ if (children || content) return
1567
+
1568
+ // Iterate element properties for child components (Uppercase keys)
1569
+ const components = element.context?.components || {}
1570
+
1571
+ // v3 compat: resolve XxxExtends shorthand
1572
+ // Pattern: StarIconExtends: 'Icon', name: 'star', fontSize: 'X2'
1573
+ // Becomes: StarIcon: { extends: 'Icon', name: 'star', fontSize: 'X2' }
1574
+ {
1575
+ const keys = Object.keys(element)
1576
+ for (let i = 0; i < keys.length; i++) {
1577
+ const prop = keys[i]
1578
+ if (!prop.endsWith('Extends') || typeof element[prop] !== 'string') continue
1579
+ const childName = prop.slice(0, -7)
1580
+ if (!childName || childName.charCodeAt(0) < 65 || childName.charCodeAt(0) > 90) continue
1581
+
1582
+ const childDef = { extends: element[prop] }
1583
+ delete element[prop]
1584
+
1585
+ // Collect subsequent lowercase properties until next Uppercase key or end
1586
+ for (let j = i + 1; j < keys.length; j++) {
1587
+ const nextKey = keys[j]
1588
+ const fc = nextKey.charCodeAt(0)
1589
+ if (fc >= 65 && fc <= 90) break // Stop at next child/component key
1590
+ if (REGISTRY.has(nextKey)) break // Stop at framework keys
1591
+ if (typeof element[nextKey] === 'function' && (nextKey.startsWith('on') || nextKey.startsWith('.'))) break
1592
+ const val = element[nextKey]
1593
+ if (val !== undefined) {
1594
+ childDef[nextKey] = val
1595
+ delete element[nextKey]
1596
+ }
1597
+ }
1598
+
1599
+ // Merge with existing child def if present
1600
+ if (element[childName] && typeof element[childName] === 'object') {
1601
+ element[childName] = { ...childDef, ...element[childName] }
1602
+ } else {
1603
+ element[childName] = childDef
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ for (const prop in element) {
1609
+ if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
1610
+ if (REGISTRY.has(prop)) continue
1611
+ if (typeof element[prop] !== 'object' || element[prop] === null) continue
1612
+
1613
+ const firstChar = prop.charCodeAt(0)
1614
+ if (firstChar < 65 || firstChar > 90) continue
1615
+
1616
+ let childDef = element[prop]
1617
+ if (childDef.__ref) continue // Already created
1618
+
1619
+ // Auto-extend from context component if key matches and no extends set
1620
+ if (!childDef.extends && !childDef.extend) {
1621
+ const comp = components[prop] || components[`smbls.${prop}`]
1622
+ if (comp) {
1623
+ childDef = { extends: comp, ...childDef }
1624
+ }
1625
+ }
1626
+
1627
+ // Apply childProps (e.g. fontSize, padding from parent's childProps)
1628
+ if (element.childProps) {
1629
+ childDef = { ...element.childProps, ...childDef }
1630
+ }
1631
+
1632
+ if (childExtends && !childDef.ignoreChildExtends) {
1633
+ if (childDef.extends) {
1634
+ const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
1635
+ const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
1636
+ childDef = { ...childDef, extends: [...parent, ...own] }
1637
+ } else {
1638
+ childDef = { extends: childExtends, ...childDef }
1639
+ }
1640
+ }
1641
+
1642
+ create(childDef, element, prop, options)
1643
+ }
1644
+ }
1645
+
1646
+ // --- DOM Assignment ---
1647
+
1648
+ function assignNode(element, parent, attachOptions) {
1649
+ const { node } = element
1650
+ if (!node) return
1651
+
1652
+ // Expose DOMQL ref on DOM node in dev mode (use $0.ref in DevTools)
1653
+ if (process.env.NODE_ENV !== 'production') {
1654
+ node.ref = element
1655
+ }
1656
+
1657
+ // Support both DOMQL elements (.node) and raw DOM nodes (root case)
1658
+ const parentNode = parent?.node || (parent?.nodeType ? parent : null)
1659
+ if (!parentNode) return
1660
+
1661
+ if (attachOptions?.position === 'before' && attachOptions.node) {
1662
+ parentNode.insertBefore(node, attachOptions.node)
1663
+ } else if (attachOptions?.position === 'after' && attachOptions.node) {
1664
+ const nextSib = attachOptions.node.nextSibling
1665
+ if (nextSib) {
1666
+ parentNode.insertBefore(node, nextSib)
1667
+ } else {
1668
+ parentNode.appendChild(node)
1669
+ }
1670
+ } else {
1671
+ parentNode.appendChild(node)
1672
+ }
1673
+ }
1674
+
1675
+ function triggerLifecycle(eventSuffix, element, options) {
1676
+ const hookName = eventSuffix.charAt(0).toLowerCase() + eventSuffix.slice(1)
1677
+ if (element.context?.plugins?.length) runPluginHook(hookName, element, options)
1678
+
1679
+ const key = 'on' + eventSuffix
1680
+ const handler = element[key]
1681
+ if (typeof handler === 'function') {
1682
+ // Scope config to this element's context so lifecycle handlers that
1683
+ // inject CSS or resolve tokens use the right app. The document is
1684
+ // stored on the config, so getActiveDocument() resolves it too.
1685
+ const ds = element.context?.designSystem
1686
+ if (ds) pushConfig(ds)
1687
+ try {
1688
+ const result = handler(element, element.state, element.context, options)
1689
+ if (result === false) return false
1690
+ } catch (err) {
1691
+ // Route the error through the plugin error hook so observers
1692
+ // (e.g. the analyze plugin) see it with full element context.
1693
+ // Falls back to console.error when no plugin captures it.
1694
+ const handled = element.context?.plugins?.length &&
1695
+ runPluginHook('error', element, { hook: key, error: err })
1696
+ if (!handled) console.error(`[DOMQL] Lifecycle "${key}" error:`, err)
1697
+ if (element.context?.strictMode) throw err
1698
+ } finally {
1699
+ if (ds) popConfig()
1700
+ }
1701
+ }
1702
+ return true
1703
+ }
1704
+
1705
+ export default create