@pyreon/runtime-dom 0.11.5 → 0.11.6

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/mount.ts CHANGED
@@ -6,7 +6,7 @@ import type {
6
6
  RefProp,
7
7
  VNode,
8
8
  VNodeChild,
9
- } from "@pyreon/core"
9
+ } from '@pyreon/core'
10
10
  import {
11
11
  dispatchToErrorBoundary,
12
12
  EMPTY_PROPS,
@@ -16,13 +16,13 @@ import {
16
16
  propagateError,
17
17
  reportError,
18
18
  runWithHooks,
19
- } from "@pyreon/core"
20
- import { effectScope, renderEffect, runUntracked, setCurrentScope } from "@pyreon/reactivity"
21
- import { registerComponent, unregisterComponent } from "./devtools"
22
- import { mountFor, mountKeyedList, mountReactive } from "./nodes"
23
- import { applyProps } from "./props"
19
+ } from '@pyreon/core'
20
+ import { effectScope, renderEffect, runUntracked, setCurrentScope } from '@pyreon/reactivity'
21
+ import { registerComponent, unregisterComponent } from './devtools'
22
+ import { mountFor, mountKeyedList, mountReactive } from './nodes'
23
+ import { applyProps } from './props'
24
24
 
25
- const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "production"
25
+ const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
26
26
 
27
27
  type Cleanup = () => void
28
28
  const noop: Cleanup = () => {
@@ -51,7 +51,7 @@ export function mountChild(
51
51
  anchor: Node | null = null,
52
52
  ): Cleanup {
53
53
  // Reactive accessor — function that reads signals
54
- if (typeof child === "function") {
54
+ if (typeof child === 'function') {
55
55
  const sample = runUntracked(() => (child as () => VNodeChild | VNodeChild[])())
56
56
  if (isKeyedArray(sample)) {
57
57
  const prevDepth = _elementDepth
@@ -63,12 +63,12 @@ export function mountChild(
63
63
  return cleanup
64
64
  }
65
65
  // Text fast path: reactive string/number/boolean — update text.data in-place
66
- if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
67
- const text = document.createTextNode(sample == null || sample === false ? "" : String(sample))
66
+ if (typeof sample === 'string' || typeof sample === 'number' || typeof sample === 'boolean') {
67
+ const text = document.createTextNode(sample === false ? '' : String(sample))
68
68
  parent.insertBefore(text, anchor)
69
69
  const dispose = renderEffect(() => {
70
70
  const v = (child as () => unknown)()
71
- text.data = v == null || v === false ? "" : String(v as string | number)
71
+ text.data = v == null || v === false ? '' : String(v as string | number)
72
72
  })
73
73
  if (_elementDepth > 0) return dispose
74
74
  return () => {
@@ -91,7 +91,7 @@ export function mountChild(
91
91
  if (child == null || child === false) return noop
92
92
 
93
93
  // Primitive — text node (static, no reactive effects to tear down).
94
- if (typeof child !== "object") {
94
+ if (typeof child !== 'object') {
95
95
  parent.insertBefore(document.createTextNode(String(child)), anchor)
96
96
  return noop
97
97
  }
@@ -132,23 +132,23 @@ export function mountChild(
132
132
  if (vnode.type === (PortalSymbol as unknown as string)) {
133
133
  const { target, children } = vnode.props as unknown as PortalProps
134
134
  if (__DEV__ && !target) {
135
- console.warn("[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.")
135
+ console.warn('[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.')
136
136
  return noop
137
137
  }
138
138
  if (__DEV__ && !(target instanceof Node)) {
139
139
  console.warn(
140
140
  `[Pyreon] <Portal> target must be a DOM node. Received ${typeof target}. ` +
141
- "Use document.getElementById() or a ref to get the target element.",
141
+ 'Use document.getElementById() or a ref to get the target element.',
142
142
  )
143
143
  }
144
144
  return mountChild(children, target, null)
145
145
  }
146
146
 
147
- if (typeof vnode.type === "function") {
147
+ if (typeof vnode.type === 'function') {
148
148
  return mountComponent(vnode as VNode & { type: ComponentFn }, parent, anchor)
149
149
  }
150
150
 
151
- if (__DEV__ && typeof vnode.type !== "string") {
151
+ if (__DEV__ && typeof vnode.type !== 'string') {
152
152
  console.warn(
153
153
  `[Pyreon] Invalid VNode type: expected a string tag or component function, ` +
154
154
  `received ${typeof vnode.type} (${String(vnode.type)}). ` +
@@ -164,20 +164,20 @@ export function mountChild(
164
164
 
165
165
  // Void elements that cannot have children
166
166
  const VOID_ELEMENTS = new Set([
167
- "area",
168
- "base",
169
- "br",
170
- "col",
171
- "embed",
172
- "hr",
173
- "img",
174
- "input",
175
- "link",
176
- "meta",
177
- "param",
178
- "source",
179
- "track",
180
- "wbr",
167
+ 'area',
168
+ 'base',
169
+ 'br',
170
+ 'col',
171
+ 'embed',
172
+ 'hr',
173
+ 'img',
174
+ 'input',
175
+ 'link',
176
+ 'meta',
177
+ 'param',
178
+ 'source',
179
+ 'track',
180
+ 'wbr',
181
181
  ])
182
182
 
183
183
  function mountElement(vnode: VNode, parent: Node, anchor: Node | null): Cleanup {
@@ -186,7 +186,7 @@ function mountElement(vnode: VNode, parent: Node, anchor: Node | null): Cleanup
186
186
  if (__DEV__ && (vnode.children?.length ?? 0) > 0 && VOID_ELEMENTS.has(vnode.type as string)) {
187
187
  console.warn(
188
188
  `[Pyreon] <${vnode.type as string}> is a void element and cannot have children. ` +
189
- "Children passed to void elements will be ignored by the browser.",
189
+ 'Children passed to void elements will be ignored by the browser.',
190
190
  )
191
191
  }
192
192
 
@@ -204,7 +204,7 @@ function mountElement(vnode: VNode, parent: Node, anchor: Node | null): Cleanup
204
204
  // Populate ref after the element is in the DOM
205
205
  const ref = props.ref as RefProp<Element> | null | undefined
206
206
  if (ref) {
207
- if (typeof ref === "function") ref(el)
207
+ if (typeof ref === 'function') ref(el)
208
208
  else ref.current = el
209
209
  }
210
210
 
@@ -225,14 +225,14 @@ function mountElement(vnode: VNode, parent: Node, anchor: Node | null): Cleanup
225
225
  }
226
226
  const refToClean = ref
227
227
  return () => {
228
- if (refToClean && typeof refToClean === "object") refToClean.current = null
228
+ if (refToClean && typeof refToClean === 'object') refToClean.current = null
229
229
  if (propCleanup) propCleanup()
230
230
  childCleanup()
231
231
  }
232
232
  }
233
233
 
234
234
  return () => {
235
- if (ref && typeof ref === "object") ref.current = null
235
+ if (ref && typeof ref === 'object') ref.current = null
236
236
  if (propCleanup) propCleanup()
237
237
  childCleanup()
238
238
  const p = el.parentNode
@@ -250,10 +250,10 @@ function mountComponent(
250
250
  const scope = effectScope()
251
251
  setCurrentScope(scope)
252
252
 
253
- let hooks: ReturnType<typeof runWithHooks>["hooks"]
253
+ let hooks: ReturnType<typeof runWithHooks>['hooks']
254
254
  let output: VNodeChild
255
255
 
256
- const componentName = (vnode.type.name || "Anonymous") as string
256
+ const componentName = (vnode.type.name || 'Anonymous') as string
257
257
  const compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`
258
258
  const parentId = _mountingStack[_mountingStack.length - 1] ?? null
259
259
  _mountingStack.push(compId)
@@ -278,7 +278,7 @@ function mountComponent(
278
278
  scope.stop()
279
279
  reportError({
280
280
  component: componentName,
281
- phase: "setup",
281
+ phase: 'setup',
282
282
  error: err,
283
283
  timestamp: Date.now(),
284
284
  props: vnode.props as Record<string, unknown>,
@@ -292,14 +292,14 @@ function mountComponent(
292
292
  setCurrentScope(null)
293
293
  }
294
294
 
295
- if (__DEV__ && output != null && typeof output === "object") {
295
+ if (__DEV__ && output != null && typeof output === 'object') {
296
296
  if (output instanceof Promise) {
297
297
  console.warn(
298
298
  `[Pyreon] Component <${componentName}> returned a Promise. ` +
299
- "Components must be synchronous — use lazy() + Suspense for async loading, " +
300
- "or fetch data in onMount and store it in a signal.",
299
+ 'Components must be synchronous — use lazy() + Suspense for async loading, ' +
300
+ 'or fetch data in onMount and store it in a signal.',
301
301
  )
302
- } else if (!("type" in output)) {
302
+ } else if (!('type' in output)) {
303
303
  console.warn(
304
304
  `[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, or function.`,
305
305
  )
@@ -320,7 +320,7 @@ function mountComponent(
320
320
  if (!handled) {
321
321
  reportError({
322
322
  component: componentName,
323
- phase: "render",
323
+ phase: 'render',
324
324
  error: err,
325
325
  timestamp: Date.now(),
326
326
  props: vnode.props as Record<string, unknown>,
@@ -346,7 +346,7 @@ function mountComponent(
346
346
  if (cleanup) mountCleanups.push(cleanup)
347
347
  } catch (err) {
348
348
  console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err)
349
- reportError({ component: componentName, phase: "mount", error: err, timestamp: Date.now() })
349
+ reportError({ component: componentName, phase: 'mount', error: err, timestamp: Date.now() })
350
350
  }
351
351
  }
352
352
 
@@ -361,7 +361,7 @@ function mountComponent(
361
361
  console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err)
362
362
  reportError({
363
363
  component: componentName,
364
- phase: "unmount",
364
+ phase: 'unmount',
365
365
  error: err,
366
366
  timestamp: Date.now(),
367
367
  })
@@ -380,7 +380,7 @@ function mountChildren(children: VNodeChild[], parent: Node, anchor: Node | null
380
380
  if (children.length === 1) {
381
381
  const c = children[0] as VNodeChild
382
382
  if (c !== undefined) {
383
- if (anchor === null && (typeof c === "string" || typeof c === "number")) {
383
+ if (anchor === null && (typeof c === 'string' || typeof c === 'number')) {
384
384
  ;(parent as HTMLElement).textContent = String(c)
385
385
  return noop
386
386
  }
@@ -419,7 +419,7 @@ function isKeyedArray(value: unknown): value is VNode[] {
419
419
  return value.every(
420
420
  (v) =>
421
421
  v !== null &&
422
- typeof v === "object" &&
422
+ typeof v === 'object' &&
423
423
  !Array.isArray(v) &&
424
424
  (v as VNode).key !== null &&
425
425
  (v as VNode).key !== undefined,
package/src/nodes.ts CHANGED
@@ -1,10 +1,11 @@
1
- import type { VNode, VNodeChild } from "@pyreon/core"
1
+ import type { VNode, VNodeChild } from '@pyreon/core'
2
+ import { captureContextStack, restoreContextStack } from '@pyreon/core'
2
3
 
3
4
  type MountFn = (child: VNodeChild, parent: Node, anchor: Node | null) => Cleanup
4
5
 
5
- import { effect, runUntracked } from "@pyreon/reactivity"
6
+ import { effect, runUntracked } from '@pyreon/reactivity'
6
7
 
7
- const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "production"
8
+ const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
8
9
 
9
10
  type Cleanup = () => void
10
11
 
@@ -43,9 +44,15 @@ export function mountReactive(
43
44
  anchor: Node | null,
44
45
  mount: (child: VNodeChild, p: Node, a: Node | null) => Cleanup,
45
46
  ): Cleanup {
46
- const marker = document.createComment("pyreon")
47
+ const marker = document.createComment('pyreon')
47
48
  parent.insertBefore(marker, anchor)
48
49
 
50
+ // Capture the context stack at creation time — ancestor provide() calls
51
+ // have already run, so this snapshot contains all parent contexts.
52
+ // When the effect re-mounts children later (e.g. Show toggling on),
53
+ // we restore this snapshot so children see ancestor providers.
54
+ const contextSnapshot = captureContextStack()
55
+
49
56
  let currentCleanup: Cleanup = () => {
50
57
  /* noop */
51
58
  }
@@ -61,13 +68,18 @@ export function mountReactive(
61
68
  /* noop */
62
69
  }
63
70
  const value = accessor()
64
- if (__DEV__ && typeof value === "function") {
71
+ if (__DEV__ && typeof value === 'function') {
65
72
  console.warn(
66
- "[Pyreon] Reactive accessor returned a function instead of a value. Did you forget to call the signal?",
73
+ '[Pyreon] Reactive accessor returned a function instead of a value. Did you forget to call the signal?',
67
74
  )
68
75
  }
69
76
  if (value != null && value !== false) {
70
- const cleanup = mount(value, parent, marker)
77
+ // Restore ancestor context so children mounted here can read
78
+ // provider values via useContext() — without this, <Show> children
79
+ // wouldn't inherit context from components above the Show boundary.
80
+ const cleanup = restoreContextStack(contextSnapshot, () =>
81
+ mount(value, parent, marker),
82
+ )
71
83
  // Guard: a re-entrant signal update (e.g. ErrorBoundary catching a child
72
84
  // throw) may have already re-run this effect and updated currentCleanup.
73
85
  // In that case, discard our stale cleanup rather than overwriting the one
@@ -209,8 +221,8 @@ export function mountKeyedList(
209
221
  listAnchor: Node | null,
210
222
  mountVNode: (vnode: VNode, p: Node, a: Node | null) => Cleanup,
211
223
  ): Cleanup {
212
- const startMarker = document.createComment("")
213
- const tailMarker = document.createComment("")
224
+ const startMarker = document.createComment('')
225
+ const tailMarker = document.createComment('')
214
226
  parent.insertBefore(startMarker, listAnchor)
215
227
  parent.insertBefore(tailMarker, listAnchor)
216
228
 
@@ -255,7 +267,7 @@ export function mountKeyedList(
255
267
  const key = vnode.key
256
268
  if (key === null || key === undefined) continue
257
269
  if (cache.has(key)) continue
258
- const anchor = document.createComment("")
270
+ const anchor = document.createComment('')
259
271
  _keyedAnchors.add(anchor)
260
272
  parent.insertBefore(anchor, tailMarker)
261
273
  const cleanup = mountVNode(vnode, parent, tailMarker)
@@ -427,13 +439,13 @@ function forLisReorder(
427
439
  export function mountFor<T>(
428
440
  source: () => T[],
429
441
  getKey: (item: T) => string | number,
430
- renderItem: (item: T) => import("@pyreon/core").VNode | import("@pyreon/core").NativeItem,
442
+ renderItem: (item: T) => import('@pyreon/core').VNode | import('@pyreon/core').NativeItem,
431
443
  parent: Node,
432
444
  anchor: Node | null,
433
445
  mountChild: MountFn,
434
446
  ): Cleanup {
435
- const startMarker = document.createComment("")
436
- const tailMarker = document.createComment("")
447
+ const startMarker = document.createComment('')
448
+ const tailMarker = document.createComment('')
437
449
  parent.insertBefore(startMarker, anchor)
438
450
  parent.insertBefore(tailMarker, anchor)
439
451
 
@@ -453,8 +465,8 @@ export function mountFor<T>(
453
465
  if (!__DEV__ || !seen) return
454
466
  if (key == null) {
455
467
  console.warn(
456
- "[Pyreon] <For> `by` function returned null/undefined. " +
457
- "Keys must be strings or numbers. Check your `by` prop.",
468
+ '[Pyreon] <For> `by` function returned null/undefined. ' +
469
+ 'Keys must be strings or numbers. Check your `by` prop.',
458
470
  )
459
471
  }
460
472
  if (seen.has(key)) {
@@ -472,18 +484,18 @@ export function mountFor<T>(
472
484
  before: Node | null,
473
485
  ) => {
474
486
  const result = renderItem(item)
475
- if ((result as import("@pyreon/core").NativeItem).__isNative) {
476
- const native = result as import("@pyreon/core").NativeItem
487
+ if ((result as import('@pyreon/core').NativeItem).__isNative) {
488
+ const native = result as import('@pyreon/core').NativeItem
477
489
  container.insertBefore(native.el, before)
478
490
  cache.set(key, { anchor: native.el, cleanup: native.cleanup, pos })
479
491
  if (native.cleanup) cleanupCount++
480
492
  return
481
493
  }
482
494
  const priorLast = before ? before.previousSibling : container.lastChild
483
- const cl = mountChild(result as import("@pyreon/core").VNode, container, before)
495
+ const cl = mountChild(result as import('@pyreon/core').VNode, container, before)
484
496
  const firstMounted = priorLast ? priorLast.nextSibling : container.firstChild
485
497
  if (!firstMounted || firstMounted === before) {
486
- const ph = document.createComment("")
498
+ const ph = document.createComment('')
487
499
  container.insertBefore(ph, before)
488
500
  cache.set(key, { anchor: ph, cleanup: cl, pos })
489
501
  } else {
package/src/props.ts CHANGED
@@ -1,12 +1,12 @@
1
- import type { ClassValue, Props } from "@pyreon/core"
2
- import { cx, normalizeStyleValue, toKebabCase } from "@pyreon/core"
1
+ import type { ClassValue, Props } from '@pyreon/core'
2
+ import { cx, normalizeStyleValue, toKebabCase } from '@pyreon/core'
3
3
 
4
- import { batch, renderEffect } from "@pyreon/reactivity"
5
- import { DELEGATED_EVENTS, delegatedPropName } from "./delegate"
4
+ import { batch, renderEffect } from '@pyreon/reactivity'
5
+ import { DELEGATED_EVENTS, delegatedPropName } from './delegate'
6
6
 
7
7
  type Cleanup = () => void
8
8
 
9
- const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "production"
9
+ const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
10
10
 
11
11
  // ─── Configurable sanitizer ──────────────────────────────────────────────────
12
12
 
@@ -36,75 +36,75 @@ export function setSanitizer(fn: SanitizeFn | null): void {
36
36
 
37
37
  // Safe HTML tags allowed by the fallback sanitizer (block + inline, no scripts/embeds/forms)
38
38
  const SAFE_TAGS = new Set([
39
- "a",
40
- "abbr",
41
- "address",
42
- "article",
43
- "aside",
44
- "b",
45
- "bdi",
46
- "bdo",
47
- "blockquote",
48
- "br",
49
- "caption",
50
- "cite",
51
- "code",
52
- "col",
53
- "colgroup",
54
- "dd",
55
- "del",
56
- "details",
57
- "dfn",
58
- "div",
59
- "dl",
60
- "dt",
61
- "em",
62
- "figcaption",
63
- "figure",
64
- "footer",
65
- "h1",
66
- "h2",
67
- "h3",
68
- "h4",
69
- "h5",
70
- "h6",
71
- "header",
72
- "hr",
73
- "i",
74
- "ins",
75
- "kbd",
76
- "li",
77
- "main",
78
- "mark",
79
- "nav",
80
- "ol",
81
- "p",
82
- "pre",
83
- "q",
84
- "rp",
85
- "rt",
86
- "ruby",
87
- "s",
88
- "samp",
89
- "section",
90
- "small",
91
- "span",
92
- "strong",
93
- "sub",
94
- "summary",
95
- "sup",
96
- "table",
97
- "tbody",
98
- "td",
99
- "tfoot",
100
- "th",
101
- "thead",
102
- "time",
103
- "tr",
104
- "u",
105
- "ul",
106
- "var",
107
- "wbr",
39
+ 'a',
40
+ 'abbr',
41
+ 'address',
42
+ 'article',
43
+ 'aside',
44
+ 'b',
45
+ 'bdi',
46
+ 'bdo',
47
+ 'blockquote',
48
+ 'br',
49
+ 'caption',
50
+ 'cite',
51
+ 'code',
52
+ 'col',
53
+ 'colgroup',
54
+ 'dd',
55
+ 'del',
56
+ 'details',
57
+ 'dfn',
58
+ 'div',
59
+ 'dl',
60
+ 'dt',
61
+ 'em',
62
+ 'figcaption',
63
+ 'figure',
64
+ 'footer',
65
+ 'h1',
66
+ 'h2',
67
+ 'h3',
68
+ 'h4',
69
+ 'h5',
70
+ 'h6',
71
+ 'header',
72
+ 'hr',
73
+ 'i',
74
+ 'ins',
75
+ 'kbd',
76
+ 'li',
77
+ 'main',
78
+ 'mark',
79
+ 'nav',
80
+ 'ol',
81
+ 'p',
82
+ 'pre',
83
+ 'q',
84
+ 'rp',
85
+ 'rt',
86
+ 'ruby',
87
+ 's',
88
+ 'samp',
89
+ 'section',
90
+ 'small',
91
+ 'span',
92
+ 'strong',
93
+ 'sub',
94
+ 'summary',
95
+ 'sup',
96
+ 'table',
97
+ 'tbody',
98
+ 'td',
99
+ 'tfoot',
100
+ 'th',
101
+ 'thead',
102
+ 'time',
103
+ 'tr',
104
+ 'u',
105
+ 'ul',
106
+ 'var',
107
+ 'wbr',
108
108
  ])
109
109
 
110
110
  // Attributes that can carry executable code
@@ -116,7 +116,7 @@ const UNSAFE_ATTR_RE = /^on/i
116
116
  * and blocks javascript:/data: URLs in href/src/action attributes.
117
117
  */
118
118
  function fallbackSanitize(html: string): string {
119
- const doc = new DOMParser().parseFromString(html, "text/html")
119
+ const doc = new DOMParser().parseFromString(html, 'text/html')
120
120
  sanitizeNode(doc.body)
121
121
  return doc.body.innerHTML
122
122
  }
@@ -173,7 +173,7 @@ export function applyProps(el: Element, props: Props): Cleanup | null {
173
173
  let first: Cleanup | null = null
174
174
  let cleanups: Cleanup[] | null = null
175
175
  for (const key in props) {
176
- if (key === "key" || key === "ref" || key === "children") continue
176
+ if (key === 'key' || key === 'ref' || key === 'children') continue
177
177
  const c = applyProp(el, key, props[key])
178
178
  if (c) {
179
179
  if (!first) {
@@ -203,7 +203,7 @@ export function applyProps(el: Element, props: Props): Cleanup | null {
203
203
  * Bind an event handler (onClick → "click") with batching + delegation support.
204
204
  */
205
205
  function applyEventProp(el: Element, key: string, value: unknown): Cleanup | null {
206
- if (typeof value !== "function") {
206
+ if (typeof value !== 'function') {
207
207
  if (__DEV__) {
208
208
  console.warn(
209
209
  `[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). ` +
@@ -233,8 +233,8 @@ export function applyProp(el: Element, key: string, value: unknown): Cleanup | n
233
233
  if (EVENT_RE.test(key)) return applyEventProp(el, key, value)
234
234
 
235
235
  // innerHTML — sanitized via Sanitizer API or fallback allowlist sanitizer
236
- if (key === "innerHTML") {
237
- if (typeof (el as HTMLElement & { setHTML?: (h: string) => void }).setHTML === "function") {
236
+ if (key === 'innerHTML') {
237
+ if (typeof (el as HTMLElement & { setHTML?: (h: string) => void }).setHTML === 'function') {
238
238
  ;(el as HTMLElement & { setHTML: (h: string) => void }).setHTML(value as string)
239
239
  } else {
240
240
  ;(el as HTMLElement).innerHTML = sanitizeHtml(value as string)
@@ -242,10 +242,10 @@ export function applyProp(el: Element, key: string, value: unknown): Cleanup | n
242
242
  return null
243
243
  }
244
244
  // dangerouslySetInnerHTML — intentionally raw, developer owns sanitization (same as React)
245
- if (key === "dangerouslySetInnerHTML") {
245
+ if (key === 'dangerouslySetInnerHTML') {
246
246
  if (__DEV__) {
247
247
  console.warn(
248
- "[Pyreon] dangerouslySetInnerHTML bypasses sanitization. Ensure the HTML is trusted.",
248
+ '[Pyreon] dangerouslySetInnerHTML bypasses sanitization. Ensure the HTML is trusted.',
249
249
  )
250
250
  }
251
251
  ;(el as HTMLElement).innerHTML = (value as { __html: string }).__html
@@ -255,7 +255,7 @@ export function applyProp(el: Element, key: string, value: unknown): Cleanup | n
255
255
  // Reactive prop — function that returns the actual value
256
256
  // Uses renderEffect (lighter than effect — no scope registration, no WeakMap)
257
257
  // since lifecycle is managed by mountElement's cleanup array.
258
- if (typeof value === "function") {
258
+ if (typeof value === 'function') {
259
259
  const dispose = renderEffect(() => setStaticProp(el, key, (value as () => unknown)()))
260
260
  return dispose
261
261
  }
@@ -265,42 +265,42 @@ export function applyProp(el: Element, key: string, value: unknown): Cleanup | n
265
265
  }
266
266
 
267
267
  // Attributes that carry URLs and must be guarded against javascript:/data: injection.
268
- const URL_ATTRS = new Set(["href", "src", "action", "formaction", "poster", "cite", "data"])
268
+ const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'poster', 'cite', 'data'])
269
269
  const UNSAFE_URL_RE = /^\s*(?:javascript|data):/i
270
270
 
271
271
  /** Apply a style prop (string or object). */
272
272
  function applyStyleProp(el: HTMLElement, value: unknown): void {
273
- if (typeof value === "string") {
273
+ if (typeof value === 'string') {
274
274
  el.style.cssText = value
275
- } else if (value != null && typeof value === "object") {
275
+ } else if (value != null && typeof value === 'object') {
276
276
  const obj = value as Record<string, unknown>
277
277
  for (const k in obj) {
278
278
  const css = normalizeStyleValue(k, obj[k])
279
- el.style.setProperty(k.startsWith("--") ? k : toKebabCase(k), css)
279
+ el.style.setProperty(k.startsWith('--') ? k : toKebabCase(k), css)
280
280
  }
281
281
  }
282
282
  }
283
283
 
284
284
  function applyClassProp(el: Element, value: unknown): void {
285
- const resolved = typeof value === "string" ? value : cx(value as ClassValue)
286
- el.setAttribute("class", resolved || "")
285
+ const resolved = typeof value === 'string' ? value : cx(value as ClassValue)
286
+ el.setAttribute('class', resolved || '')
287
287
  }
288
288
 
289
289
  function setStaticProp(el: Element, key: string, value: unknown): void {
290
290
  // Block javascript:/data: URI injection in URL-bearing attributes.
291
- if (URL_ATTRS.has(key) && typeof value === "string" && UNSAFE_URL_RE.test(value)) {
291
+ if (URL_ATTRS.has(key) && typeof value === 'string' && UNSAFE_URL_RE.test(value)) {
292
292
  if (__DEV__) {
293
293
  console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`)
294
294
  }
295
295
  return
296
296
  }
297
297
 
298
- if (key === "class" || key === "className") {
298
+ if (key === 'class' || key === 'className') {
299
299
  applyClassProp(el, value)
300
300
  return
301
301
  }
302
302
 
303
- if (key === "style") {
303
+ if (key === 'style') {
304
304
  applyStyleProp(el as HTMLElement, value)
305
305
  return
306
306
  }
@@ -310,8 +310,8 @@ function setStaticProp(el: Element, key: string, value: unknown): void {
310
310
  return
311
311
  }
312
312
 
313
- if (typeof value === "boolean") {
314
- if (value) el.setAttribute(key, "")
313
+ if (typeof value === 'boolean') {
314
+ if (value) el.setAttribute(key, '')
315
315
  else el.removeAttribute(key)
316
316
  return
317
317
  }