@furystack/shades 11.0.35 → 12.0.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.
Files changed (215) hide show
  1. package/CHANGELOG.md +337 -0
  2. package/README.md +99 -13
  3. package/esm/compile-route.spec.d.ts +2 -0
  4. package/esm/compile-route.spec.d.ts.map +1 -0
  5. package/esm/compile-route.spec.js +34 -0
  6. package/esm/compile-route.spec.js.map +1 -0
  7. package/esm/component-factory.spec.js +13 -5
  8. package/esm/component-factory.spec.js.map +1 -1
  9. package/esm/components/index.d.ts +4 -1
  10. package/esm/components/index.d.ts.map +1 -1
  11. package/esm/components/index.js +4 -1
  12. package/esm/components/index.js.map +1 -1
  13. package/esm/components/lazy-load.d.ts +2 -4
  14. package/esm/components/lazy-load.d.ts.map +1 -1
  15. package/esm/components/lazy-load.js +40 -24
  16. package/esm/components/lazy-load.js.map +1 -1
  17. package/esm/components/lazy-load.spec.js +57 -50
  18. package/esm/components/lazy-load.spec.js.map +1 -1
  19. package/esm/components/link-to-route.d.ts +2 -0
  20. package/esm/components/link-to-route.d.ts.map +1 -1
  21. package/esm/components/link-to-route.js +3 -2
  22. package/esm/components/link-to-route.js.map +1 -1
  23. package/esm/components/link-to-route.spec.js +13 -9
  24. package/esm/components/link-to-route.spec.js.map +1 -1
  25. package/esm/components/nested-route-link.d.ts +62 -0
  26. package/esm/components/nested-route-link.d.ts.map +1 -0
  27. package/esm/components/nested-route-link.js +66 -0
  28. package/esm/components/nested-route-link.js.map +1 -0
  29. package/esm/components/nested-route-link.spec.d.ts +2 -0
  30. package/esm/components/nested-route-link.spec.d.ts.map +1 -0
  31. package/esm/components/nested-route-link.spec.js +179 -0
  32. package/esm/components/nested-route-link.spec.js.map +1 -0
  33. package/esm/components/nested-route-types.d.ts +37 -0
  34. package/esm/components/nested-route-types.d.ts.map +1 -0
  35. package/esm/components/nested-route-types.js +2 -0
  36. package/esm/components/nested-route-types.js.map +1 -0
  37. package/esm/components/nested-router.d.ts +103 -0
  38. package/esm/components/nested-router.d.ts.map +1 -0
  39. package/esm/components/nested-router.js +178 -0
  40. package/esm/components/nested-router.js.map +1 -0
  41. package/esm/components/nested-router.spec.d.ts +2 -0
  42. package/esm/components/nested-router.spec.d.ts.map +1 -0
  43. package/esm/components/nested-router.spec.js +659 -0
  44. package/esm/components/nested-router.spec.js.map +1 -0
  45. package/esm/components/route-link.d.ts +4 -0
  46. package/esm/components/route-link.d.ts.map +1 -1
  47. package/esm/components/route-link.js +9 -10
  48. package/esm/components/route-link.js.map +1 -1
  49. package/esm/components/route-link.spec.js +16 -12
  50. package/esm/components/route-link.spec.js.map +1 -1
  51. package/esm/components/router.d.ts +20 -2
  52. package/esm/components/router.d.ts.map +1 -1
  53. package/esm/components/router.js +3 -0
  54. package/esm/components/router.js.map +1 -1
  55. package/esm/components/router.spec.js +75 -74
  56. package/esm/components/router.spec.js.map +1 -1
  57. package/esm/css-generator.d.ts +50 -0
  58. package/esm/css-generator.d.ts.map +1 -0
  59. package/esm/css-generator.js +107 -0
  60. package/esm/css-generator.js.map +1 -0
  61. package/esm/css-generator.spec.d.ts +2 -0
  62. package/esm/css-generator.spec.d.ts.map +1 -0
  63. package/esm/css-generator.spec.js +162 -0
  64. package/esm/css-generator.spec.js.map +1 -0
  65. package/esm/index.d.ts +2 -0
  66. package/esm/index.d.ts.map +1 -1
  67. package/esm/index.js +2 -0
  68. package/esm/index.js.map +1 -1
  69. package/esm/initialize.d.ts +11 -0
  70. package/esm/initialize.d.ts.map +1 -1
  71. package/esm/initialize.js +5 -0
  72. package/esm/initialize.js.map +1 -1
  73. package/esm/jsx.d.ts +83 -2
  74. package/esm/jsx.d.ts.map +1 -1
  75. package/esm/models/children-list.d.ts +5 -1
  76. package/esm/models/children-list.d.ts.map +1 -1
  77. package/esm/models/css-object.d.ts +33 -0
  78. package/esm/models/css-object.d.ts.map +1 -0
  79. package/esm/models/css-object.js +2 -0
  80. package/esm/models/css-object.js.map +1 -0
  81. package/esm/models/index.d.ts +1 -0
  82. package/esm/models/index.d.ts.map +1 -1
  83. package/esm/models/index.js +1 -0
  84. package/esm/models/index.js.map +1 -1
  85. package/esm/models/partial-element.d.ts +12 -2
  86. package/esm/models/partial-element.d.ts.map +1 -1
  87. package/esm/models/render-options.d.ts +89 -3
  88. package/esm/models/render-options.d.ts.map +1 -1
  89. package/esm/models/selection-state.d.ts +4 -0
  90. package/esm/models/selection-state.d.ts.map +1 -1
  91. package/esm/services/location-service.d.ts +11 -0
  92. package/esm/services/location-service.d.ts.map +1 -1
  93. package/esm/services/location-service.js +11 -0
  94. package/esm/services/location-service.js.map +1 -1
  95. package/esm/services/resource-manager.d.ts +24 -0
  96. package/esm/services/resource-manager.d.ts.map +1 -1
  97. package/esm/services/resource-manager.js +30 -0
  98. package/esm/services/resource-manager.js.map +1 -1
  99. package/esm/services/resource-manager.spec.js +93 -0
  100. package/esm/services/resource-manager.spec.js.map +1 -1
  101. package/esm/services/screen-service.d.ts +81 -4
  102. package/esm/services/screen-service.d.ts.map +1 -1
  103. package/esm/services/screen-service.js +75 -4
  104. package/esm/services/screen-service.js.map +1 -1
  105. package/esm/services/screen-service.spec.js +91 -7
  106. package/esm/services/screen-service.spec.js.map +1 -1
  107. package/esm/shade-component.d.ts +17 -4
  108. package/esm/shade-component.d.ts.map +1 -1
  109. package/esm/shade-component.js +67 -5
  110. package/esm/shade-component.js.map +1 -1
  111. package/esm/shade-host-props-ref.integration.spec.d.ts +2 -0
  112. package/esm/shade-host-props-ref.integration.spec.d.ts.map +1 -0
  113. package/esm/shade-host-props-ref.integration.spec.js +381 -0
  114. package/esm/shade-host-props-ref.integration.spec.js.map +1 -0
  115. package/esm/shade-resources.integration.spec.js +208 -39
  116. package/esm/shade-resources.integration.spec.js.map +1 -1
  117. package/esm/shade.d.ts +34 -15
  118. package/esm/shade.d.ts.map +1 -1
  119. package/esm/shade.js +180 -33
  120. package/esm/shade.js.map +1 -1
  121. package/esm/shade.spec.d.ts +2 -0
  122. package/esm/shade.spec.d.ts.map +1 -0
  123. package/esm/shade.spec.js +198 -0
  124. package/esm/shade.spec.js.map +1 -0
  125. package/esm/shades.integration.spec.js +135 -72
  126. package/esm/shades.integration.spec.js.map +1 -1
  127. package/esm/style-manager.d.ts +65 -0
  128. package/esm/style-manager.d.ts.map +1 -0
  129. package/esm/style-manager.js +95 -0
  130. package/esm/style-manager.js.map +1 -0
  131. package/esm/style-manager.spec.d.ts +2 -0
  132. package/esm/style-manager.spec.d.ts.map +1 -0
  133. package/esm/style-manager.spec.js +179 -0
  134. package/esm/style-manager.spec.js.map +1 -0
  135. package/esm/styled-element.spec.d.ts +2 -0
  136. package/esm/styled-element.spec.d.ts.map +1 -0
  137. package/esm/styled-element.spec.js +86 -0
  138. package/esm/styled-element.spec.js.map +1 -0
  139. package/esm/styled-shade.spec.d.ts +2 -0
  140. package/esm/styled-shade.spec.d.ts.map +1 -0
  141. package/esm/styled-shade.spec.js +66 -0
  142. package/esm/styled-shade.spec.js.map +1 -0
  143. package/esm/svg-types.d.ts +389 -0
  144. package/esm/svg-types.d.ts.map +1 -0
  145. package/esm/svg-types.js +9 -0
  146. package/esm/svg-types.js.map +1 -0
  147. package/esm/svg.d.ts +15 -0
  148. package/esm/svg.d.ts.map +1 -0
  149. package/esm/svg.js +76 -0
  150. package/esm/svg.js.map +1 -0
  151. package/esm/svg.spec.d.ts +2 -0
  152. package/esm/svg.spec.d.ts.map +1 -0
  153. package/esm/svg.spec.js +80 -0
  154. package/esm/svg.spec.js.map +1 -0
  155. package/esm/vnode.d.ts +103 -0
  156. package/esm/vnode.d.ts.map +1 -0
  157. package/esm/vnode.integration.spec.d.ts +2 -0
  158. package/esm/vnode.integration.spec.d.ts.map +1 -0
  159. package/esm/vnode.integration.spec.js +494 -0
  160. package/esm/vnode.integration.spec.js.map +1 -0
  161. package/esm/vnode.js +453 -0
  162. package/esm/vnode.js.map +1 -0
  163. package/esm/vnode.spec.d.ts +2 -0
  164. package/esm/vnode.spec.d.ts.map +1 -0
  165. package/esm/vnode.spec.js +473 -0
  166. package/esm/vnode.spec.js.map +1 -0
  167. package/package.json +3 -3
  168. package/src/compile-route.spec.ts +39 -0
  169. package/src/component-factory.spec.tsx +18 -5
  170. package/src/components/index.ts +4 -1
  171. package/src/components/lazy-load.spec.tsx +82 -75
  172. package/src/components/lazy-load.tsx +49 -27
  173. package/src/components/link-to-route.spec.tsx +25 -21
  174. package/src/components/link-to-route.tsx +4 -2
  175. package/src/components/nested-route-link.spec.tsx +303 -0
  176. package/src/components/nested-route-link.tsx +100 -0
  177. package/src/components/nested-route-types.ts +42 -0
  178. package/src/components/nested-router.spec.tsx +817 -0
  179. package/src/components/nested-router.tsx +256 -0
  180. package/src/components/route-link.spec.tsx +22 -18
  181. package/src/components/route-link.tsx +10 -10
  182. package/src/components/router.spec.tsx +109 -108
  183. package/src/components/router.tsx +15 -2
  184. package/src/css-generator.spec.ts +183 -0
  185. package/src/css-generator.ts +117 -0
  186. package/src/index.ts +2 -0
  187. package/src/initialize.ts +12 -0
  188. package/src/jsx.ts +129 -2
  189. package/src/models/children-list.ts +7 -1
  190. package/src/models/css-object.ts +34 -0
  191. package/src/models/index.ts +1 -0
  192. package/src/models/partial-element.ts +13 -2
  193. package/src/models/render-options.ts +90 -3
  194. package/src/models/selection-state.ts +4 -0
  195. package/src/services/location-service.tsx +11 -0
  196. package/src/services/resource-manager.spec.ts +116 -0
  197. package/src/services/resource-manager.ts +30 -0
  198. package/src/services/screen-service.spec.ts +109 -7
  199. package/src/services/screen-service.ts +81 -4
  200. package/src/shade-component.ts +72 -6
  201. package/src/shade-host-props-ref.integration.spec.tsx +460 -0
  202. package/src/shade-resources.integration.spec.tsx +276 -52
  203. package/src/shade.spec.tsx +239 -0
  204. package/src/shade.ts +211 -56
  205. package/src/shades.integration.spec.tsx +154 -80
  206. package/src/style-manager.spec.ts +229 -0
  207. package/src/style-manager.ts +104 -0
  208. package/src/styled-element.spec.tsx +117 -0
  209. package/src/styled-shade.spec.ts +86 -0
  210. package/src/svg-types.ts +437 -0
  211. package/src/svg.spec.ts +89 -0
  212. package/src/svg.ts +78 -0
  213. package/src/vnode.integration.spec.tsx +657 -0
  214. package/src/vnode.spec.ts +579 -0
  215. package/src/vnode.ts +508 -0
package/src/vnode.ts ADDED
@@ -0,0 +1,508 @@
1
+ /**
2
+ * VNode-based reconciliation for Shades.
3
+ *
4
+ * Instead of creating real DOM elements during each render and then diffing them,
5
+ * the JSX factory produces lightweight VNode descriptors. A reconciler diffs the
6
+ * previous VNode tree against the new one and applies surgical DOM updates using
7
+ * tracked `_el` references.
8
+ */
9
+
10
+ import type { ChildrenList } from './models/children-list.js'
11
+ import type { RefObject } from './models/render-options.js'
12
+ import { SVG_NS, isSvgTag } from './svg.js'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Brands & sentinels
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const VNODE_BRAND = 'vnode' as const
19
+ const VTEXT_BRAND = 'vtext' as const
20
+
21
+ /**
22
+ * Sentinel type used as VNode.type for JSX fragments (`<>...</>`).
23
+ */
24
+ export const FRAGMENT: unique symbol = Symbol('fragment')
25
+
26
+ /**
27
+ * Sentinel type for VNodes that wrap a pre-existing real DOM node.
28
+ * Used when shadeChildren (created outside render mode) flow into a VNode render.
29
+ */
30
+ export const EXISTING_NODE: unique symbol = Symbol('existing-node')
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Types
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * A lightweight descriptor for a DOM element or Shade component.
38
+ */
39
+ export type VNode = {
40
+ _brand: typeof VNODE_BRAND
41
+ type: string | ((...args: unknown[]) => unknown) | typeof FRAGMENT | typeof EXISTING_NODE
42
+ props: Record<string, unknown> | null
43
+ children: VChild[]
44
+ _el?: Node
45
+ }
46
+
47
+ /**
48
+ * A lightweight descriptor for a DOM text node.
49
+ */
50
+ export type VTextNode = {
51
+ _brand: typeof VTEXT_BRAND
52
+ text: string
53
+ _el?: Text
54
+ }
55
+
56
+ /**
57
+ * A single child in a VNode tree -- either an element/component or a text node.
58
+ */
59
+ export type VChild = VNode | VTextNode
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Type guards
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export const isVNode = (v: unknown): v is VNode =>
66
+ typeof v === 'object' && v !== null && (v as VNode)._brand === VNODE_BRAND
67
+
68
+ export const isVTextNode = (v: unknown): v is VTextNode =>
69
+ typeof v === 'object' && v !== null && (v as VTextNode)._brand === VTEXT_BRAND
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // VNode creation
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Recursively flattens raw JSX children into a flat VChild array.
77
+ * - Strings and numbers become VTextNodes.
78
+ * - Fragment VNodes are inlined (their children are spliced in).
79
+ * - Nullish / boolean values are skipped.
80
+ */
81
+ export const flattenVChildren = (raw: unknown[]): VChild[] => {
82
+ const result: VChild[] = []
83
+ for (const child of raw) {
84
+ if (child === null || child === undefined || child === false || child === true) continue
85
+ if (typeof child === 'string') {
86
+ result.push({ _brand: VTEXT_BRAND, text: child })
87
+ } else if (typeof child === 'number') {
88
+ result.push({ _brand: VTEXT_BRAND, text: String(child) })
89
+ } else if (Array.isArray(child)) {
90
+ result.push(...flattenVChildren(child))
91
+ } else if (isVNode(child)) {
92
+ if (child.type === FRAGMENT) {
93
+ result.push(...child.children)
94
+ } else {
95
+ result.push(child)
96
+ }
97
+ } else if (isVTextNode(child)) {
98
+ result.push(child)
99
+ } else if (child instanceof Node) {
100
+ // Real DOM node from shadeChildren (created outside render mode).
101
+ // Wrap it so the reconciler can track it.
102
+ result.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props: null, children: [], _el: child })
103
+ }
104
+ }
105
+ return result
106
+ }
107
+
108
+ /**
109
+ * Creates a VNode descriptor. Used as the JSX factory during renders.
110
+ *
111
+ * For intrinsic elements (string type), the returned VNode includes DOM-shim
112
+ * methods (`setAttribute`, `appendChild`, etc.) so that component code which
113
+ * creates intermediate JSX and calls DOM methods on it continues to work.
114
+ *
115
+ * @param type Tag name, Shade factory function, or null (fragment)
116
+ * @param props Element props / component props
117
+ * @param rawChildren Varargs children (strings, VNodes, arrays, etc.)
118
+ */
119
+ export const createVNode = (
120
+ type: string | ((...args: unknown[]) => unknown) | null,
121
+ props: Record<string, unknown> | null,
122
+ ...rawChildren: unknown[]
123
+ ): VNode => {
124
+ const children = flattenVChildren(rawChildren)
125
+ const vnode: VNode = {
126
+ _brand: VNODE_BRAND,
127
+ type: type === null ? FRAGMENT : type,
128
+ props: props ? { ...props } : null,
129
+ children,
130
+ }
131
+
132
+ // For intrinsic elements, add DOM-shim methods so that component render code
133
+ // which does `const el = <div/>; el.setAttribute(...)` still works in VNode mode.
134
+ if (typeof type === 'string') {
135
+ const v = vnode as unknown as Record<string, unknown>
136
+ v.setAttribute = (name: string, value: string) => {
137
+ if (!vnode.props) vnode.props = {}
138
+ vnode.props[name] = value
139
+ }
140
+ v.removeAttribute = (name: string) => {
141
+ if (vnode.props) delete vnode.props[name]
142
+ }
143
+ v.getAttribute = (name: string) => {
144
+ return (vnode.props?.[name] as string) ?? null
145
+ }
146
+ v.hasAttribute = (name: string) => {
147
+ return vnode.props ? name in vnode.props : false
148
+ }
149
+ v.appendChild = (child: unknown) => {
150
+ if (child instanceof Node) {
151
+ vnode.children.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props: null, children: [], _el: child })
152
+ } else if (isVNode(child) || isVTextNode(child)) {
153
+ vnode.children.push(child)
154
+ }
155
+ return child
156
+ }
157
+ v.tagName = type.toUpperCase()
158
+ v.nodeName = type.toUpperCase()
159
+ }
160
+
161
+ return vnode
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Helpers
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Shallow-compares two props objects. Returns true if all keys and values match.
170
+ */
171
+ export const shallowEqual = (a: Record<string, unknown> | null, b: Record<string, unknown> | null): boolean => {
172
+ if (a === b) return true
173
+ if (!a || !b) return false
174
+ const keysA = Object.keys(a)
175
+ const keysB = Object.keys(b)
176
+ if (keysA.length !== keysB.length) return false
177
+ for (const key of keysA) {
178
+ if (a[key] !== b[key]) return false
179
+ }
180
+ return true
181
+ }
182
+
183
+ /**
184
+ * Converts a render result (VNode | HTMLElement | string | null) into a flat
185
+ * VChild array suitable for `patchChildren`.
186
+ *
187
+ * Real DOM elements can appear here when component state stores elements created
188
+ * outside renderMode (e.g. in async callbacks like Router's `updateUrl`).
189
+ */
190
+ export const toVChildArray = (renderResult: unknown): VChild[] => {
191
+ if (renderResult === null || renderResult === undefined) return []
192
+ if (typeof renderResult === 'string' || typeof renderResult === 'number') {
193
+ return [{ _brand: VTEXT_BRAND, text: String(renderResult) }]
194
+ }
195
+ if (isVNode(renderResult)) {
196
+ if (renderResult.type === FRAGMENT) return renderResult.children
197
+ return [renderResult]
198
+ }
199
+ // Real DOM element (from async code that ran outside renderMode)
200
+ if (renderResult instanceof DocumentFragment) {
201
+ return Array.from(renderResult.childNodes).map((node) => ({
202
+ _brand: VNODE_BRAND as typeof VNODE_BRAND,
203
+ type: EXISTING_NODE,
204
+ props: null,
205
+ children: [] as VChild[],
206
+ _el: node,
207
+ }))
208
+ }
209
+ if (renderResult instanceof Node) {
210
+ return [{ _brand: VNODE_BRAND, type: EXISTING_NODE, props: null, children: [], _el: renderResult }]
211
+ }
212
+ return []
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Props / style application
217
+ // ---------------------------------------------------------------------------
218
+
219
+ const setProp = (el: Element, key: string, value: unknown): void => {
220
+ if (key === 'ref') return
221
+ if (key === 'style' && typeof value === 'object' && value !== null) {
222
+ for (const [sk, sv] of Object.entries(value as Record<string, string>)) {
223
+ ;((el as HTMLElement).style as unknown as Record<string, string>)[sk] = sv
224
+ }
225
+ return
226
+ }
227
+
228
+ if (el instanceof SVGElement) {
229
+ if (key === 'className') {
230
+ el.setAttribute('class', String(value))
231
+ } else if (key.startsWith('on') && typeof value === 'function') {
232
+ ;(el as unknown as Record<string, unknown>)[key] = value
233
+ } else if (value === null || value === undefined || value === false) {
234
+ el.removeAttribute(key)
235
+ } else {
236
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
237
+ el.setAttribute(key, String(value))
238
+ }
239
+ return
240
+ }
241
+
242
+ if (key.startsWith('data-') || key.startsWith('aria-')) {
243
+ el.setAttribute(key, typeof value === 'string' ? value : '')
244
+ } else {
245
+ ;(el as unknown as Record<string, unknown>)[key] = value
246
+ }
247
+ }
248
+
249
+ const removeProp = (el: Element, key: string): void => {
250
+ if (key === 'ref') return
251
+ if (el instanceof SVGElement) {
252
+ el.removeAttribute(key === 'className' ? 'class' : key)
253
+ return
254
+ }
255
+
256
+ if (key === 'style') {
257
+ el.removeAttribute('style')
258
+ } else if (key.startsWith('data-') || key.startsWith('aria-')) {
259
+ el.removeAttribute(key)
260
+ } else if (key.startsWith('on')) {
261
+ ;(el as unknown as Record<string, unknown>)[key] = null
262
+ } else {
263
+ try {
264
+ ;(el as unknown as Record<string, unknown>)[key] = ''
265
+ } catch {
266
+ // Some properties are read-only
267
+ }
268
+ }
269
+ }
270
+
271
+ const patchStyle = (
272
+ el: Element,
273
+ oldStyle: Record<string, string> | undefined,
274
+ newStyle: Record<string, string> | undefined,
275
+ ): void => {
276
+ const style = (el as HTMLElement).style as unknown as Record<string, string>
277
+ const oldS = oldStyle || {}
278
+ const newS = newStyle || {}
279
+ for (const key of Object.keys(oldS)) {
280
+ if (!(key in newS)) {
281
+ style[key] = ''
282
+ }
283
+ }
284
+ for (const [key, value] of Object.entries(newS)) {
285
+ if (oldS[key] !== value) {
286
+ style[key] = value
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Applies all props to a freshly created element (initial mount).
293
+ */
294
+ const applyProps = (el: Element, props: Record<string, unknown> | null): void => {
295
+ if (!props) return
296
+ for (const [key, value] of Object.entries(props)) {
297
+ setProp(el, key, value)
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Diffs old and new props and applies minimal updates to a live DOM element.
303
+ */
304
+ export const patchProps = (
305
+ el: Element,
306
+ oldProps: Record<string, unknown> | null,
307
+ newProps: Record<string, unknown> | null,
308
+ ): void => {
309
+ const oldP = oldProps || {}
310
+ const newP = newProps || {}
311
+
312
+ // Remove props that no longer exist
313
+ for (const key of Object.keys(oldP)) {
314
+ if (!(key in newP)) {
315
+ removeProp(el, key)
316
+ }
317
+ }
318
+
319
+ // Add / update props
320
+ for (const [key, value] of Object.entries(newP)) {
321
+ if (key === 'style') {
322
+ patchStyle(el, oldP.style as Record<string, string> | undefined, value as Record<string, string> | undefined)
323
+ } else if (oldP[key] !== value) {
324
+ setProp(el, key, value)
325
+ }
326
+ }
327
+ }
328
+
329
+ // ---------------------------------------------------------------------------
330
+ // Mount (VNode tree → real DOM)
331
+ // ---------------------------------------------------------------------------
332
+
333
+ /**
334
+ * Creates real DOM nodes from a VChild and optionally appends to a parent.
335
+ * Sets `_el` on the VChild so subsequent patches can find the DOM node.
336
+ * @returns The created DOM node.
337
+ */
338
+ export const mountChild = (child: VChild, parent: Node | null): Node => {
339
+ if (child._brand === VTEXT_BRAND) {
340
+ const text = document.createTextNode(child.text)
341
+ child._el = text
342
+ if (parent) parent.appendChild(text)
343
+ return text
344
+ }
345
+
346
+ // Pre-existing real DOM node (from shadeChildren)
347
+ if (child.type === EXISTING_NODE) {
348
+ if (parent && child._el) parent.appendChild(child._el)
349
+ return child._el as Node
350
+ }
351
+
352
+ // Shade component
353
+ if (typeof child.type === 'function') {
354
+ const factory = child.type as (props: unknown, children?: ChildrenList) => JSX.Element
355
+ const el = factory(child.props || {}, child.children as unknown as ChildrenList)
356
+ child._el = el
357
+ if (parent) parent.appendChild(el)
358
+ return el
359
+ }
360
+
361
+ // Intrinsic element
362
+ const tag = child.type as string
363
+ const el = isSvgTag(tag) ? document.createElementNS(SVG_NS, tag) : document.createElement(tag)
364
+ applyProps(el, child.props)
365
+ child._el = el
366
+
367
+ for (const c of child.children) {
368
+ mountChild(c, el)
369
+ }
370
+
371
+ if (parent) parent.appendChild(el)
372
+
373
+ // Set ref after the element is fully created and appended
374
+ const ref = child.props?.ref as RefObject<Element> | undefined
375
+ if (ref) {
376
+ ;(ref as { current: Element | null }).current = el
377
+ }
378
+
379
+ return el
380
+ }
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Unmount (remove real DOM)
384
+ // ---------------------------------------------------------------------------
385
+
386
+ /**
387
+ * Removes the DOM node associated with a VChild from its parent.
388
+ */
389
+ export const unmountChild = (child: VChild): void => {
390
+ // Clear ref before removing from DOM
391
+ if (child._brand === VNODE_BRAND && child.props?.ref) {
392
+ ;(child.props.ref as { current: Element | null }).current = null
393
+ }
394
+
395
+ const node = child._el
396
+ if (node?.parentNode) {
397
+ node.parentNode.removeChild(node)
398
+ }
399
+ }
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // Patch (diff old VNode tree vs new VNode tree → DOM updates)
403
+ // ---------------------------------------------------------------------------
404
+
405
+ /**
406
+ * Patches a single old/new VChild pair. Updates the real DOM in place when
407
+ * possible, or replaces the DOM node when types differ.
408
+ */
409
+ const patchChild = (_parentEl: Node, oldChild: VChild, newChild: VChild): void => {
410
+ // Both text nodes
411
+ if (oldChild._brand === VTEXT_BRAND && newChild._brand === VTEXT_BRAND) {
412
+ if (oldChild.text !== newChild.text && oldChild._el) {
413
+ oldChild._el.textContent = newChild.text
414
+ }
415
+ newChild._el = oldChild._el
416
+ return
417
+ }
418
+
419
+ // Both element/component VNodes with the same type
420
+ if (oldChild._brand === VNODE_BRAND && newChild._brand === VNODE_BRAND && oldChild.type === newChild.type) {
421
+ if (oldChild.type === EXISTING_NODE) {
422
+ // --- Pre-existing DOM node ---
423
+ newChild._el = newChild._el || oldChild._el
424
+ if (oldChild._el !== newChild._el && oldChild._el?.parentNode) {
425
+ oldChild._el.parentNode.replaceChild(newChild._el!, oldChild._el)
426
+ }
427
+ return
428
+ }
429
+
430
+ if (typeof oldChild.type === 'function') {
431
+ // --- Shade component boundary ---
432
+ const el = oldChild._el as JSX.Element
433
+ newChild._el = el
434
+
435
+ const propsChanged = !shallowEqual(oldChild.props, newChild.props)
436
+ // For children, reference check is enough -- if the parent re-rendered,
437
+ // the children VNodes are always fresh objects, so we compare lengths
438
+ // and item identity as a fast heuristic.
439
+ const childrenChanged =
440
+ oldChild.children.length !== newChild.children.length ||
441
+ oldChild.children.some((c, i) => c !== newChild.children[i])
442
+
443
+ if (propsChanged || childrenChanged) {
444
+ if (propsChanged) {
445
+ el.props = newChild.props
446
+ patchProps(el, oldChild.props, newChild.props)
447
+ }
448
+ el.shadeChildren = newChild.children as unknown as ChildrenList
449
+ el.updateComponent()
450
+ }
451
+ return
452
+ }
453
+
454
+ // --- Intrinsic element ---
455
+ const el = oldChild._el as Element
456
+ newChild._el = el
457
+ patchProps(el, oldChild.props, newChild.props)
458
+ patchChildren(el, oldChild.children, newChild.children)
459
+
460
+ // Update refs: clear old ref if different, set new ref
461
+ const oldRef = oldChild.props?.ref as RefObject<Element> | undefined
462
+ const newRef = newChild.props?.ref as RefObject<Element> | undefined
463
+ if (oldRef !== newRef) {
464
+ if (oldRef) (oldRef as { current: Element | null }).current = null
465
+ if (newRef) (newRef as { current: Element | null }).current = el
466
+ }
467
+ return
468
+ }
469
+
470
+ // Types differ → replace
471
+ const oldNode = oldChild._el
472
+ if (oldNode && oldNode.parentNode) {
473
+ const newNode = mountChild(newChild, null)
474
+ oldNode.parentNode.replaceChild(newNode, oldNode)
475
+ } else {
476
+ mountChild(newChild, _parentEl)
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Reconciles an array of old VChildren against new VChildren inside a parent
482
+ * DOM element. Patches matching pairs, removes excess old children, and
483
+ * mounts excess new children.
484
+ *
485
+ * **Note:** This uses positional (index-based) matching, not key-based
486
+ * reconciliation. Reordering list items will cause all children from the
487
+ * reorder point onward to be patched/replaced rather than moved. For
488
+ * dynamic lists where order changes frequently, wrap each item in its own
489
+ * Shade component so that the component boundary prevents unnecessary
490
+ * inner-DOM churn.
491
+ */
492
+ export const patchChildren = (parentEl: Node, oldChildren: VChild[], newChildren: VChild[]): void => {
493
+ const commonLen = Math.min(oldChildren.length, newChildren.length)
494
+
495
+ for (let i = 0; i < commonLen; i++) {
496
+ patchChild(parentEl, oldChildren[i], newChildren[i])
497
+ }
498
+
499
+ // Remove excess old children (iterate backwards to avoid index issues)
500
+ for (let i = oldChildren.length - 1; i >= commonLen; i--) {
501
+ unmountChild(oldChildren[i])
502
+ }
503
+
504
+ // Mount excess new children
505
+ for (let i = commonLen; i < newChildren.length; i++) {
506
+ mountChild(newChildren[i], parentEl)
507
+ }
508
+ }