@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/shade.ts CHANGED
@@ -1,14 +1,21 @@
1
1
  import type { Constructable } from '@furystack/inject'
2
2
  import { hasInjectorReference, Injector } from '@furystack/inject'
3
3
  import { ObservableValue } from '@furystack/utils'
4
- import type { ChildrenList, PartialElement, RenderOptions } from './models/index.js'
4
+ import type { ChildrenList, CSSObject, PartialElement, RenderOptions } from './models/index.js'
5
+ import type { RefObject } from './models/render-options.js'
5
6
  import { LocationService } from './services/location-service.js'
6
7
  import { ResourceManager } from './services/resource-manager.js'
7
- import { attachProps, attachStyles } from './shade-component.js'
8
+ import { attachProps, attachStyles, setRenderMode } from './shade-component.js'
9
+ import { StyleManager } from './style-manager.js'
10
+ import type { VChild } from './vnode.js'
11
+ import { patchChildren, toVChildArray } from './vnode.js'
8
12
 
9
13
  export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
10
14
  /**
11
- * Explicit shadow dom name. Will fall back to 'shade-{guid}' if not provided
15
+ * The custom element tag name used to register the component.
16
+ * Must follow the Custom Elements naming convention (lowercase, must contain a hyphen).
17
+ *
18
+ * @example 'my-button', 'shade-dialog', 'app-header'
12
19
  */
13
20
  shadowDomName: string
14
21
 
@@ -17,23 +24,6 @@ export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
17
24
  */
18
25
  render: (options: RenderOptions<TProps, TElementBase>) => JSX.Element | string | null
19
26
 
20
- /**
21
- * Construct hook. Will be executed once when the element has been constructed and initialized
22
- */
23
- constructed?: (
24
- options: RenderOptions<TProps, TElementBase>,
25
- ) => void | undefined | (() => void) | Promise<void | undefined | (() => void)>
26
-
27
- /**
28
- * Will be executed when the element is attached to the DOM.
29
- */
30
- onAttach?: (options: RenderOptions<TProps, TElementBase>) => void
31
-
32
- /**
33
- * Will be executed when the element is detached from the DOM.
34
- */
35
- onDetach?: (options: RenderOptions<TProps, TElementBase>) => void
36
-
37
27
  /**
38
28
  * Name of the HTML Element's base class. Needs to be defined if the elementBase is set. E.g.: 'div', 'button', 'input'
39
29
  */
@@ -45,9 +35,29 @@ export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
45
35
  elementBase?: Constructable<TElementBase>
46
36
 
47
37
  /**
48
- * A default style that will be applied to the element. Can be overridden by external styles.
38
+ * Inline styles applied to each component instance.
39
+ * Use for per-instance dynamic overrides. Prefer `css` for component-level defaults.
49
40
  */
50
41
  style?: Partial<CSSStyleDeclaration>
42
+
43
+ /**
44
+ * CSS styles injected as a stylesheet during component registration.
45
+ * Supports pseudo-selectors (`&:hover`, `&:active`) and nested selectors (`& .class`).
46
+ *
47
+ * **Best practice:** Prefer `css` over `style` for component defaults -- styles are injected
48
+ * once per component type (better performance), and support pseudo-selectors and nesting.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * css: {
53
+ * display: 'flex',
54
+ * padding: '16px',
55
+ * '&:hover': { backgroundColor: '#f0f0f0' },
56
+ * '& .title': { fontWeight: 'bold' }
57
+ * }
58
+ * ```
59
+ */
60
+ css?: CSSObject
51
61
  }
52
62
 
53
63
  /**
@@ -58,11 +68,16 @@ export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
58
68
  export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
59
69
  o: ShadeOptions<TProps, TElementBase>,
60
70
  ) => {
61
- // register shadow-dom element
71
+ // Register custom element
62
72
  const customElementName = o.shadowDomName
63
73
 
64
74
  const existing = customElements.get(customElementName)
65
75
  if (!existing) {
76
+ // Register CSS styles if provided
77
+ if (o.css) {
78
+ StyleManager.registerComponentStyles(customElementName, o.css, o.elementBaseName)
79
+ }
80
+
66
81
  const ElementBase = o.elementBase || HTMLElement
67
82
 
68
83
  customElements.define(
@@ -79,15 +94,31 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
79
94
 
80
95
  public resourceManager = new ResourceManager()
81
96
 
97
+ /**
98
+ * Host props collected during the current render pass via `useHostProps`.
99
+ * Applied to the host element after each render.
100
+ */
101
+ private _pendingHostProps: Array<Record<string, unknown> & { style?: Record<string, string> }> = []
102
+
103
+ /**
104
+ * The host props that were applied in the previous render, used for diffing.
105
+ */
106
+ private _prevHostProps: Record<string, unknown> | null = null
107
+
108
+ /**
109
+ * Cached ref objects keyed by the user-provided key string.
110
+ */
111
+ private _refs = new Map<string, RefObject<Element>>()
112
+
82
113
  public connectedCallback() {
83
- o.onAttach?.(this.getRenderOptions())
84
- this.callConstructed()
114
+ this.updateComponent()
85
115
  }
86
116
 
87
117
  public async disconnectedCallback() {
88
- o.onDetach?.(this.getRenderOptions())
118
+ this._refs.clear()
119
+ this._prevVTree = null
120
+ this._prevHostProps = null
89
121
  await this.resourceManager[Symbol.asyncDispose]()
90
- this.cleanup?.()
91
122
  }
92
123
 
93
124
  /**
@@ -117,8 +148,17 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
117
148
  props: this.props,
118
149
  injector: this.injector,
119
150
  children: this.shadeChildren,
120
- element: this,
121
151
  renderCount: this._renderCount,
152
+ useHostProps: (hostProps) => {
153
+ this._pendingHostProps.push(hostProps)
154
+ },
155
+ useRef: <T extends Element = HTMLElement>(key: string): RefObject<T> => {
156
+ const existingRef = this._refs.get(key) as RefObject<T> | undefined
157
+ if (existingRef) return existingRef
158
+ const refObject = { current: null } as { current: T | null }
159
+ this._refs.set(key, refObject as unknown as RefObject<Element>)
160
+ return refObject as RefObject<T>
161
+ },
122
162
  useObservable: (key, obesrvable, options) => {
123
163
  const onChange = options?.onChange || (() => this.updateComponent())
124
164
  return this.resourceManager.useObservable(key, obesrvable, onChange, options)
@@ -193,47 +233,148 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
193
233
  return renderOptions
194
234
  }
195
235
 
236
+ private _updateScheduled = false
237
+
196
238
  /**
197
- * Updates the component in the DOM.
239
+ * The VChild array from the previous render, with `_el` references
240
+ * pointing to the real DOM nodes. Used to diff against the next render.
198
241
  */
199
- public updateComponent() {
200
- const renderResult = this.render(this.getRenderOptions())
242
+ private _prevVTree: VChild[] | null = null
201
243
 
202
- if (renderResult === null || renderResult === undefined) {
203
- this.innerHTML = ''
244
+ /**
245
+ * Schedules a component update via microtask. Multiple calls before the microtask
246
+ * runs are coalesced into a single render pass.
247
+ */
248
+ public updateComponent() {
249
+ if (!this._updateScheduled) {
250
+ this._updateScheduled = true
251
+ queueMicrotask(() => {
252
+ this._updateScheduled = false
253
+ this._performUpdate()
254
+ })
204
255
  }
256
+ }
205
257
 
206
- if (typeof renderResult === 'string' || typeof renderResult === 'number') {
207
- this.innerHTML = renderResult
258
+ private _performUpdate() {
259
+ this._pendingHostProps = []
260
+ let renderResult: unknown
261
+ setRenderMode(true)
262
+ try {
263
+ renderResult = this.render(this.getRenderOptions())
264
+ } finally {
265
+ setRenderMode(false)
208
266
  }
209
267
 
210
- if (renderResult instanceof HTMLElement) {
211
- this.replaceChildren(renderResult)
212
- }
213
- if (renderResult instanceof DocumentFragment) {
214
- this.replaceChildren(renderResult)
215
- }
268
+ const newVTree = toVChildArray(renderResult)
269
+ patchChildren(this, this._prevVTree || [], newVTree)
270
+ this._prevVTree = newVTree
271
+
272
+ this._applyHostProps()
216
273
  }
217
274
 
218
275
  /**
219
- * Finalize the component initialization after it gets the Props. Called by the framework internally
276
+ * Merges all pending host props from the render pass and applies them
277
+ * to the host element, diffing against the previously applied host props.
220
278
  */
221
- public callConstructed() {
222
- this.updateComponent()
223
- const cleanupResult = o.constructed && o.constructed(this.getRenderOptions())
224
- if (cleanupResult instanceof Promise) {
225
- cleanupResult
226
- .then((cleanup) => (this.cleanup = cleanup))
227
- .catch(() => {
228
- /** */
229
- })
230
- } else {
231
- // construct is not async
232
- this.cleanup = cleanupResult
279
+ private _applyHostProps() {
280
+ if (this._pendingHostProps.length === 0) {
281
+ if (this._prevHostProps) {
282
+ // All host props were removed — clean up
283
+ for (const key of Object.keys(this._prevHostProps)) {
284
+ if (key === 'style') continue
285
+ this.removeAttribute(key)
286
+ }
287
+ if (this._prevHostProps.style) {
288
+ for (const sk of Object.keys(this._prevHostProps.style as Record<string, string>)) {
289
+ if (sk.startsWith('--')) {
290
+ this.style.removeProperty(sk)
291
+ } else {
292
+ ;(this.style as unknown as Record<string, string>)[sk] = ''
293
+ }
294
+ }
295
+ }
296
+ this._prevHostProps = null
297
+ }
298
+ return
299
+ }
300
+
301
+ // Merge all pending host prop calls into a single object
302
+ const merged: Record<string, unknown> = {}
303
+ let mergedStyle: Record<string, string> | undefined
304
+
305
+ for (const hp of this._pendingHostProps) {
306
+ for (const [key, value] of Object.entries(hp)) {
307
+ if (key === 'style' && typeof value === 'object' && value !== null) {
308
+ mergedStyle = { ...mergedStyle, ...(value as Record<string, string>) }
309
+ } else {
310
+ merged[key] = value
311
+ }
312
+ }
313
+ }
314
+
315
+ if (mergedStyle) {
316
+ merged.style = mergedStyle
317
+ }
318
+
319
+ const oldHP = this._prevHostProps || {}
320
+ const newHP = merged
321
+
322
+ // Remove attributes no longer present
323
+ for (const key of Object.keys(oldHP)) {
324
+ if (key === 'style') continue
325
+ if (!(key in newHP)) {
326
+ if (key.startsWith('on') && typeof oldHP[key] === 'function') {
327
+ ;(this as unknown as Record<string, unknown>)[key] = null
328
+ } else {
329
+ this.removeAttribute(key)
330
+ }
331
+ }
332
+ }
333
+
334
+ // Apply new/changed attributes
335
+ for (const [key, value] of Object.entries(newHP)) {
336
+ if (key === 'style') continue
337
+ if (oldHP[key] !== value) {
338
+ if (typeof value === 'function' || (typeof value === 'object' && value !== null)) {
339
+ ;(this as unknown as Record<string, unknown>)[key] = value
340
+ } else if (value === null || value === undefined || value === false) {
341
+ if (key in this) {
342
+ ;(this as unknown as Record<string, unknown>)[key] = undefined
343
+ }
344
+ this.removeAttribute(key)
345
+ } else {
346
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
347
+ this.setAttribute(key, String(value))
348
+ }
349
+ }
350
+ }
351
+
352
+ // Diff styles
353
+ const oldStyle = (oldHP.style as Record<string, string>) || {}
354
+ const newStyle = (mergedStyle as Record<string, string>) || {}
355
+
356
+ for (const sk of Object.keys(oldStyle)) {
357
+ if (!(sk in newStyle)) {
358
+ if (sk.startsWith('--')) {
359
+ this.style.removeProperty(sk)
360
+ } else {
361
+ ;(this.style as unknown as Record<string, string>)[sk] = ''
362
+ }
363
+ }
233
364
  }
234
- }
235
365
 
236
- private cleanup: void | (() => void) = undefined
366
+ for (const [sk, sv] of Object.entries(newStyle)) {
367
+ if (oldStyle[sk] !== sv) {
368
+ if (sk.startsWith('--')) {
369
+ this.style.setProperty(sk, sv)
370
+ } else {
371
+ ;(this.style as unknown as Record<string, string>)[sk] = sv
372
+ }
373
+ }
374
+ }
375
+
376
+ this._prevHostProps = merged
377
+ }
237
378
 
238
379
  private _injector?: Injector
239
380
 
@@ -273,7 +414,7 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
273
414
  o.elementBaseName ? { extends: o.elementBaseName } : undefined,
274
415
  )
275
416
  } else {
276
- throw Error(`A custom shade with shadow DOM name '${o.shadowDomName}' has already been registered!`)
417
+ throw Error(`A custom shade with name '${o.shadowDomName}' has already been registered!`)
277
418
  }
278
419
 
279
420
  return (props: TProps & PartialElement<TElementBase>, children?: ChildrenList) => {
@@ -285,9 +426,23 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
285
426
  el.props = props || ({} as TProps & PartialElement<TElementBase>)
286
427
  el.shadeChildren = children
287
428
 
429
+ if (o.elementBaseName) {
430
+ el.setAttribute('is', customElementName)
431
+ }
432
+
288
433
  attachStyles(el, { style: o.style })
289
434
  attachProps(el, props)
290
435
 
291
436
  return el as JSX.Element
292
437
  }
293
438
  }
439
+
440
+ /**
441
+ * Flushes any pending microtask-based component updates.
442
+ * Useful in tests to wait for batched renders to complete before asserting DOM state.
443
+ *
444
+ * Note: this flushes one level of pending updates. If a render itself triggers new
445
+ * `updateComponent()` calls, an additional `await flushUpdates()` may be needed.
446
+ * @returns a promise that resolves after the current microtask queue has been processed
447
+ */
448
+ export const flushUpdates = (): Promise<void> => new Promise<void>((resolve) => queueMicrotask(resolve))