@furystack/shades 11.1.0 → 12.0.1

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 (166) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/README.md +13 -13
  3. package/esm/component-factory.spec.js +13 -5
  4. package/esm/component-factory.spec.js.map +1 -1
  5. package/esm/components/index.d.ts +4 -1
  6. package/esm/components/index.d.ts.map +1 -1
  7. package/esm/components/index.js +4 -1
  8. package/esm/components/index.js.map +1 -1
  9. package/esm/components/lazy-load.d.ts +2 -4
  10. package/esm/components/lazy-load.d.ts.map +1 -1
  11. package/esm/components/lazy-load.js +40 -24
  12. package/esm/components/lazy-load.js.map +1 -1
  13. package/esm/components/lazy-load.spec.js +57 -50
  14. package/esm/components/lazy-load.spec.js.map +1 -1
  15. package/esm/components/link-to-route.d.ts +2 -0
  16. package/esm/components/link-to-route.d.ts.map +1 -1
  17. package/esm/components/link-to-route.js +3 -2
  18. package/esm/components/link-to-route.js.map +1 -1
  19. package/esm/components/link-to-route.spec.js +13 -9
  20. package/esm/components/link-to-route.spec.js.map +1 -1
  21. package/esm/components/nested-route-link.d.ts +62 -0
  22. package/esm/components/nested-route-link.d.ts.map +1 -0
  23. package/esm/components/nested-route-link.js +66 -0
  24. package/esm/components/nested-route-link.js.map +1 -0
  25. package/esm/components/nested-route-link.spec.d.ts +2 -0
  26. package/esm/components/nested-route-link.spec.d.ts.map +1 -0
  27. package/esm/components/nested-route-link.spec.js +179 -0
  28. package/esm/components/nested-route-link.spec.js.map +1 -0
  29. package/esm/components/nested-route-types.d.ts +37 -0
  30. package/esm/components/nested-route-types.d.ts.map +1 -0
  31. package/esm/components/nested-route-types.js +2 -0
  32. package/esm/components/nested-route-types.js.map +1 -0
  33. package/esm/components/nested-router.d.ts +103 -0
  34. package/esm/components/nested-router.d.ts.map +1 -0
  35. package/esm/components/nested-router.js +183 -0
  36. package/esm/components/nested-router.js.map +1 -0
  37. package/esm/components/nested-router.spec.d.ts +2 -0
  38. package/esm/components/nested-router.spec.d.ts.map +1 -0
  39. package/esm/components/nested-router.spec.js +737 -0
  40. package/esm/components/nested-router.spec.js.map +1 -0
  41. package/esm/components/route-link.d.ts +4 -0
  42. package/esm/components/route-link.d.ts.map +1 -1
  43. package/esm/components/route-link.js +5 -5
  44. package/esm/components/route-link.js.map +1 -1
  45. package/esm/components/route-link.spec.js +16 -12
  46. package/esm/components/route-link.spec.js.map +1 -1
  47. package/esm/components/router.d.ts +20 -2
  48. package/esm/components/router.d.ts.map +1 -1
  49. package/esm/components/router.js +12 -7
  50. package/esm/components/router.js.map +1 -1
  51. package/esm/components/router.spec.js +141 -74
  52. package/esm/components/router.spec.js.map +1 -1
  53. package/esm/initialize.d.ts +11 -0
  54. package/esm/initialize.d.ts.map +1 -1
  55. package/esm/initialize.js +5 -0
  56. package/esm/initialize.js.map +1 -1
  57. package/esm/jsx.d.ts +83 -2
  58. package/esm/jsx.d.ts.map +1 -1
  59. package/esm/models/children-list.d.ts +5 -1
  60. package/esm/models/children-list.d.ts.map +1 -1
  61. package/esm/models/partial-element.d.ts +12 -2
  62. package/esm/models/partial-element.d.ts.map +1 -1
  63. package/esm/models/render-options.d.ts +89 -3
  64. package/esm/models/render-options.d.ts.map +1 -1
  65. package/esm/models/selection-state.d.ts +4 -0
  66. package/esm/models/selection-state.d.ts.map +1 -1
  67. package/esm/services/location-service.d.ts +11 -0
  68. package/esm/services/location-service.d.ts.map +1 -1
  69. package/esm/services/location-service.js +11 -0
  70. package/esm/services/location-service.js.map +1 -1
  71. package/esm/services/resource-manager.d.ts +24 -0
  72. package/esm/services/resource-manager.d.ts.map +1 -1
  73. package/esm/services/resource-manager.js +36 -1
  74. package/esm/services/resource-manager.js.map +1 -1
  75. package/esm/services/resource-manager.spec.js +102 -0
  76. package/esm/services/resource-manager.spec.js.map +1 -1
  77. package/esm/services/screen-service.d.ts +81 -4
  78. package/esm/services/screen-service.d.ts.map +1 -1
  79. package/esm/services/screen-service.js +75 -4
  80. package/esm/services/screen-service.js.map +1 -1
  81. package/esm/services/screen-service.spec.js +91 -7
  82. package/esm/services/screen-service.spec.js.map +1 -1
  83. package/esm/shade-component.d.ts +17 -4
  84. package/esm/shade-component.d.ts.map +1 -1
  85. package/esm/shade-component.js +67 -5
  86. package/esm/shade-component.js.map +1 -1
  87. package/esm/shade-host-props-ref.integration.spec.d.ts +2 -0
  88. package/esm/shade-host-props-ref.integration.spec.d.ts.map +1 -0
  89. package/esm/shade-host-props-ref.integration.spec.js +381 -0
  90. package/esm/shade-host-props-ref.integration.spec.js.map +1 -0
  91. package/esm/shade-resources.integration.spec.js +208 -39
  92. package/esm/shade-resources.integration.spec.js.map +1 -1
  93. package/esm/shade.d.ts +20 -17
  94. package/esm/shade.d.ts.map +1 -1
  95. package/esm/shade.js +172 -33
  96. package/esm/shade.js.map +1 -1
  97. package/esm/shade.spec.js +31 -30
  98. package/esm/shade.spec.js.map +1 -1
  99. package/esm/shades.integration.spec.js +135 -72
  100. package/esm/shades.integration.spec.js.map +1 -1
  101. package/esm/style-manager.d.ts +2 -2
  102. package/esm/style-manager.js +2 -2
  103. package/esm/svg-types.d.ts +389 -0
  104. package/esm/svg-types.d.ts.map +1 -0
  105. package/esm/svg-types.js +9 -0
  106. package/esm/svg-types.js.map +1 -0
  107. package/esm/svg.d.ts +15 -0
  108. package/esm/svg.d.ts.map +1 -0
  109. package/esm/svg.js +76 -0
  110. package/esm/svg.js.map +1 -0
  111. package/esm/svg.spec.d.ts +2 -0
  112. package/esm/svg.spec.d.ts.map +1 -0
  113. package/esm/svg.spec.js +80 -0
  114. package/esm/svg.spec.js.map +1 -0
  115. package/esm/vnode.d.ts +103 -0
  116. package/esm/vnode.d.ts.map +1 -0
  117. package/esm/vnode.integration.spec.d.ts +2 -0
  118. package/esm/vnode.integration.spec.d.ts.map +1 -0
  119. package/esm/vnode.integration.spec.js +494 -0
  120. package/esm/vnode.integration.spec.js.map +1 -0
  121. package/esm/vnode.js +453 -0
  122. package/esm/vnode.js.map +1 -0
  123. package/esm/vnode.spec.d.ts +2 -0
  124. package/esm/vnode.spec.d.ts.map +1 -0
  125. package/esm/vnode.spec.js +473 -0
  126. package/esm/vnode.spec.js.map +1 -0
  127. package/package.json +8 -9
  128. package/src/component-factory.spec.tsx +18 -5
  129. package/src/components/index.ts +4 -1
  130. package/src/components/lazy-load.spec.tsx +82 -75
  131. package/src/components/lazy-load.tsx +49 -27
  132. package/src/components/link-to-route.spec.tsx +25 -21
  133. package/src/components/link-to-route.tsx +4 -2
  134. package/src/components/nested-route-link.spec.tsx +303 -0
  135. package/src/components/nested-route-link.tsx +100 -0
  136. package/src/components/nested-route-types.ts +42 -0
  137. package/src/components/nested-router.spec.tsx +918 -0
  138. package/src/components/nested-router.tsx +260 -0
  139. package/src/components/route-link.spec.tsx +22 -18
  140. package/src/components/route-link.tsx +6 -5
  141. package/src/components/router.spec.tsx +196 -108
  142. package/src/components/router.tsx +21 -8
  143. package/src/initialize.ts +12 -0
  144. package/src/jsx.ts +129 -2
  145. package/src/models/children-list.ts +7 -1
  146. package/src/models/partial-element.ts +13 -2
  147. package/src/models/render-options.ts +90 -3
  148. package/src/models/selection-state.ts +4 -0
  149. package/src/services/location-service.tsx +11 -0
  150. package/src/services/resource-manager.spec.ts +128 -0
  151. package/src/services/resource-manager.ts +36 -1
  152. package/src/services/screen-service.spec.ts +109 -7
  153. package/src/services/screen-service.ts +81 -4
  154. package/src/shade-component.ts +72 -6
  155. package/src/shade-host-props-ref.integration.spec.tsx +460 -0
  156. package/src/shade-resources.integration.spec.tsx +276 -52
  157. package/src/shade.spec.tsx +40 -39
  158. package/src/shade.ts +186 -58
  159. package/src/shades.integration.spec.tsx +154 -80
  160. package/src/style-manager.ts +2 -2
  161. package/src/svg-types.ts +437 -0
  162. package/src/svg.spec.ts +89 -0
  163. package/src/svg.ts +78 -0
  164. package/src/vnode.integration.spec.tsx +657 -0
  165. package/src/vnode.spec.ts +579 -0
  166. package/src/vnode.ts +508 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,317 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.0.1] - 2026-02-11
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - Fixed `Router` and `NestedRouter` not abandoning stale navigations when routes change rapidly. Previously, a semaphore-based lock serialized navigations, allowing intermediate `onVisit`/`onLeave` callbacks to complete even after a newer navigation had been triggered. Now a version counter detects when a newer navigation has started and aborts the stale one, ensuring only the latest destination's lifecycle callbacks execute.
8
+ - Fixed `useState` setter throwing `ObservableAlreadyDisposedError` when called after component unmount (e.g. from async callbacks like image `onerror` or `fetch` responses that resolve after the component is removed from the DOM)
9
+
10
+ ### 🧪 Tests
11
+
12
+ - Added tests for `Router` verifying that rapid navigation (e.g. clicking route B then immediately route C) skips intermediate route callbacks
13
+ - Added tests for `NestedRouter` verifying that rapid navigation abandons stale `onVisit` callbacks
14
+ - Added test verifying `useState` setter silently ignores calls after disposal
15
+
16
+ ### ⬆️ Dependencies
17
+
18
+ - Bump `jsdom` from `^27.4.0` to `^28.0.0`
19
+ - Bump `vitest` from `^4.0.17` to `^4.0.18`
20
+ - Bump `@types/node` from `^25.0.10` to `^25.2.3`
21
+ - Removed `semaphore-async-await` dependency
22
+ - Updated internal dependencies
23
+
24
+ ## [12.0.0] - 2026-02-09
25
+
26
+ ### 📝 Documentation
27
+
28
+ ### ScreenService API Documentation
29
+
30
+ Improved JSDoc documentation for `ScreenService` with usage examples for responsive UI development.
31
+
32
+ **Documented APIs:**
33
+
34
+ - `screenSize.atLeast[size]` - Observable breakpoint detection
35
+ - `orientation` - Observable screen orientation tracking
36
+ - `breakpoints` - Breakpoint threshold definitions
37
+
38
+ **Breakpoint Thresholds:**
39
+
40
+ - `xs`: 0px+ (all sizes)
41
+ - `sm`: 600px+ (small tablets and up)
42
+ - `md`: 960px+ (tablets and up)
43
+ - `lg`: 1280px+ (desktops and up)
44
+ - `xl`: 1920px+ (large desktops)
45
+
46
+ ### useObservable Documentation
47
+
48
+ Enhanced `useObservable` JSDoc with examples for the `onChange` callback option.
49
+
50
+ ### 🧪 Tests
51
+
52
+ - Added integration tests for Shade resource management (`useObservable`, `useDisposable`)
53
+ - Added tests for `ScreenService` breakpoints, observables, and disposal
54
+ - Updated unit and integration tests to use `updateComponent()` instead of the removed `callConstructed()`
55
+ - Replaced `constructed` callback test with `useDisposable` cleanup test
56
+ - Updated integration tests to use `flushUpdates()` for asserting DOM state after microtask-based rendering
57
+ - Added test for batching multiple synchronous observable changes into a single render
58
+ - Added test for coalescing multiple `updateComponent()` calls into a single render pass
59
+ - Added tests for `NestedRouter` covering route matching, nested layouts, lifecycle hooks, `notFound` fallback, and URL parameter extraction
60
+ - Added tests for `NestedRouteLink` covering SPA navigation, parameterized route compilation, and `createNestedRouteLink` type constraints
61
+ - Refactored existing `Router`, `LazyLoad`, `LinkToRoute`, `RouteLink`, and integration tests to use `usingAsync` for proper `Injector` disposal
62
+ - Added `vnode.spec.ts` with unit tests for VNode creation, flattening, mounting, patching, prop diffing, and unmounting
63
+ - Added `vnode.integration.spec.tsx` with integration tests covering VNode reconciliation within Shade components
64
+ - Added `shade-host-props-ref.integration.spec.tsx` with tests for `useHostProps` and `useRef` behaviors
65
+ - Added tests for `ResourceManager.useObservable` observable switching behavior when a different observable reference is passed for the same key
66
+ - Updated existing integration tests to use `flushUpdates()` and the new API
67
+
68
+ ### 💥 Breaking Changes
69
+
70
+ ### Removed `constructed` callback from `Shade()`
71
+
72
+ The `constructed` option has been removed from the `Shade()` component definition. The `callConstructed()` method has also been removed from the `JSX.Element` interface. Any cleanup function returned by `constructed` is no longer supported.
73
+
74
+ **Migration:** Move initialization logic into `render` using `useDisposable()` for one-time setup that needs cleanup.
75
+
76
+ ```typescript
77
+ // ❌ Before
78
+ const MyComponent = Shade({
79
+ shadowDomName: 'my-component',
80
+ constructed: ({ element }) => {
81
+ const listener = () => { /* ... */ }
82
+ window.addEventListener('click', listener)
83
+ return () => window.removeEventListener('click', listener)
84
+ },
85
+ render: () => <div>Hello</div>,
86
+ })
87
+
88
+ // ✅ After
89
+ const MyComponent = Shade({
90
+ shadowDomName: 'my-component',
91
+ render: ({ element, useDisposable }) => {
92
+ useDisposable('click-handler', () => {
93
+ const listener = () => { /* ... */ }
94
+ window.addEventListener('click', listener)
95
+ return { [Symbol.dispose]: () => window.removeEventListener('click', listener) }
96
+ })
97
+ return <div>Hello</div>
98
+ },
99
+ })
100
+ ```
101
+
102
+ **Impact:** All components using the `constructed` callback must be updated.
103
+
104
+ ### Rendering Engine Replaced with VNode-Based Reconciliation
105
+
106
+ The rendering engine has been replaced with a lightweight VNode-based reconciler. Instead of creating real DOM elements during each render and diffing them, the JSX factory now produces VNode descriptors during render mode. A reconciler diffs the previous VNode tree against the new one and applies surgical DOM updates using tracked element references.
107
+
108
+ **Impact:** All components using the `element` parameter from `RenderOptions` need to be updated. Components using `onAttach` or `onDetach` lifecycle hooks need to migrate to `useDisposable`.
109
+
110
+ ### `element` Removed from `RenderOptions`
111
+
112
+ The `element` property (direct reference to the host custom element) has been removed from `RenderOptions`. Components should no longer imperatively mutate the host element.
113
+
114
+ **Migration:** Use the new `useHostProps` hook to declaratively set attributes and styles on the host element.
115
+
116
+ ```typescript
117
+ // ❌ Before
118
+ render: ({ element, props }) => {
119
+ element.setAttribute('data-variant', props.variant)
120
+ element.style.setProperty('--color', colors.main)
121
+ // ...
122
+ }
123
+
124
+ // ✅ After
125
+ render: ({ useHostProps, props }) => {
126
+ useHostProps({
127
+ 'data-variant': props.variant,
128
+ style: { '--color': colors.main },
129
+ })
130
+ // ...
131
+ }
132
+ ```
133
+
134
+ ### `onAttach` and `onDetach` Lifecycle Hooks Removed
135
+
136
+ The `onAttach` and `onDetach` component lifecycle hooks have been removed. Use `useDisposable` or `connectedCallback`/`disconnectedCallback` for setup and teardown logic.
137
+
138
+ ```typescript
139
+ // ❌ Before
140
+ Shade({
141
+ shadowDomName: 'my-component',
142
+ onAttach: ({ element }) => {
143
+ /* setup */
144
+ },
145
+ onDetach: ({ element }) => {
146
+ /* cleanup */
147
+ },
148
+ render: ({ props }) => {
149
+ /* ... */
150
+ },
151
+ })
152
+
153
+ // ✅ After
154
+ Shade({
155
+ shadowDomName: 'my-component',
156
+ render: ({ props, useDisposable }) => {
157
+ useDisposable('setup', () => {
158
+ /* setup */
159
+ return {
160
+ [Symbol.dispose]: () => {
161
+ /* cleanup */
162
+ },
163
+ }
164
+ })
165
+ // ...
166
+ },
167
+ })
168
+ ```
169
+
170
+ ### 📚 Documentation
171
+
172
+ - Updated README to remove references to `constructed` and `initialState`, and to recommend `useDisposable` for one-time setup with cleanup
173
+ - Removed outdated note from README about DOM morphing behavior
174
+
175
+ ### ⚠️ Changed
176
+
177
+ ### Behavioral change: `updateComponent()` is now asynchronous
178
+
179
+ `updateComponent()` no longer renders synchronously. Any code that calls `updateComponent()` (or triggers it via observable changes) and immediately inspects the DOM will now see stale state. Use `await flushUpdates()` to wait for pending renders to complete before reading the DOM.
180
+
181
+ ### ⚡ Performance
182
+
183
+ ### Microtask-based batched component updates
184
+
185
+ `updateComponent()` now schedules renders via `queueMicrotask()` instead of executing them synchronously. Multiple calls to `updateComponent()` within the same synchronous block (e.g. several observable changes) are coalesced into a single render pass, reducing unnecessary DOM updates.
186
+
187
+ - Component updates are batched via `queueMicrotask`, coalescing multiple `updateComponent()` calls into a single render pass
188
+ - VNode props are shallow-compared to skip unnecessary DOM updates
189
+ - Style diffing patches only changed properties instead of replacing the entire style
190
+
191
+ ### ✨ Features
192
+
193
+ ### New `NestedRouter` component
194
+
195
+ Added a `NestedRouter` component that supports hierarchical route definitions with parent/child relationships. Parent routes receive an `outlet` prop containing the rendered child route, enabling layout composition patterns (e.g. a shared layout wrapping page-specific content).
196
+
197
+ Routes are defined as a nested `Record` where keys are URL patterns (using `path-to-regexp`). The matching algorithm builds a chain from outermost to innermost route, then renders inside-out so each parent wraps its child.
198
+
199
+ **Usage:**
200
+
201
+ ```typescript
202
+ import { NestedRouter, createComponent } from '@furystack/shades'
203
+
204
+ const routes = {
205
+ '/': {
206
+ component: ({ outlet }) => (
207
+ <div>
208
+ <nav>Shared Navigation</nav>
209
+ {outlet}
210
+ </div>
211
+ ),
212
+ children: {
213
+ '/': { component: () => <div>Home</div> },
214
+ '/about': { component: () => <div>About</div> },
215
+ },
216
+ },
217
+ }
218
+
219
+ <NestedRouter routes={routes} notFound={<div>404</div>} />
220
+ ```
221
+
222
+ Key features:
223
+
224
+ - Hierarchical route matching with `buildMatchChain()` - matches from outermost to innermost route
225
+ - Lifecycle hooks (`onVisit`/`onLeave`) scoped per route level, only triggered for routes that actually change
226
+ - `findDivergenceIndex()` for efficient diffing - sibling navigation only triggers leave/visit for the changed subtree
227
+ - `notFound` fallback when no routes match
228
+
229
+ ### New `NestedRouteLink` and `createNestedRouteLink` components
230
+
231
+ Added `NestedRouteLink` for SPA navigation with type-safe parameterized routes. It intercepts clicks to use `history.pushState` and compiles URL parameters (e.g. `/users/:id`) automatically.
232
+
233
+ `createNestedRouteLink()` creates a narrowed version of `NestedRouteLink` constrained to a specific route tree, so TypeScript only accepts valid paths and requires `params` when the route has parameters.
234
+
235
+ **Usage:**
236
+
237
+ ```typescript
238
+ import { NestedRouteLink, createNestedRouteLink } from '@furystack/shades'
239
+
240
+ // Basic usage — params are inferred from the href pattern
241
+ <NestedRouteLink href="/users/:id" params={{ id: '123' }}>User</NestedRouteLink>
242
+
243
+ // Type-safe usage — constrained to a route tree
244
+ const AppLink = createNestedRouteLink<typeof appRoutes>()
245
+ <AppLink href="/buttons">Buttons</AppLink>
246
+ ```
247
+
248
+ ### Route type utilities
249
+
250
+ Added type-level utilities for working with nested route trees:
251
+
252
+ - `ExtractRoutePaths<T>` - recursively extracts all valid full URL paths from a nested route tree
253
+ - `ExtractRouteParams<T>` - extracts parameter names from a URL pattern into a typed record
254
+ - `ConcatPaths<Parent, Child>` - concatenates parent and child paths handling the `/` root
255
+ - `UrlTree<TPaths>` - validates URL constant objects against a set of valid paths
256
+
257
+ ### VNode-Based Reconciliation Engine
258
+
259
+ Introduced a new `vnode.ts` module that implements a VNode-based virtual DOM reconciler. The JSX factory produces lightweight VNode descriptors during component renders, which are then diffed against the previous tree to apply minimal DOM updates. This eliminates the overhead of creating and diffing real DOM elements on every render cycle.
260
+
261
+ ### `useHostProps` Hook
262
+
263
+ Added `useHostProps` to `RenderOptions`, enabling components to declaratively set attributes, data attributes, ARIA attributes, event handlers, and styles (including CSS custom properties) on the host custom element. It can be called multiple times per render; calls are merged and diffed against the previous render.
264
+
265
+ ### `useRef` Hook
266
+
267
+ Added `useRef` to `RenderOptions`, allowing components to create mutable ref objects that capture references to child DOM elements. Refs are cached by key and persist across renders. The ref's `current` property is set to the DOM element after mount and `null` on unmount.
268
+
269
+ ```typescript
270
+ const inputRef = useRef<HTMLInputElement>('input')
271
+ // In JSX:
272
+ <input ref={inputRef} />
273
+ // Later:
274
+ inputRef.current?.focus()
275
+ ```
276
+
277
+ ### `ref` Prop Support on Intrinsic Elements
278
+
279
+ Added a `ref` property to `PartialElement<T>`, enabling `ref` objects to be passed to any intrinsic JSX element (e.g., `<div ref={myRef} />`). The VNode reconciler handles mounting and unmounting refs automatically.
280
+
281
+ ### Native SVG Element Support
282
+
283
+ Added first-class SVG support with proper namespace handling. SVG elements are now created with `createElementNS` using the correct SVG namespace, and attributes are applied via `setAttribute` instead of property assignment. This includes:
284
+
285
+ - A new `svg.ts` module with SVG tag detection and namespace constants
286
+ - A new `svg-types.ts` module with typed SVG attribute interfaces for all standard SVG elements (shapes, gradients, filters, animations, etc.)
287
+ - Updated `IntrinsicElements` with proper typed SVG element definitions
288
+
289
+ ### `flushUpdates` Utility
290
+
291
+ Added `flushUpdates()` — a test utility that returns a promise resolving after the current microtask queue has been processed, enabling tests to await batched component updates before asserting DOM state.
292
+
293
+ - Exported `flushUpdates()` utility that returns a promise resolving after the current microtask queue is processed, allowing tests to reliably wait for batched renders to complete
294
+ - Extended `attachDataAttributes` to forward `aria-*` attributes from component props to the DOM element, enabling accessible components built with Shades
295
+
296
+ ### 🗑️ Deprecated
297
+
298
+ - Deprecated `Router`, `Route`, `RouterProps`, and `RouterState` in favor of `NestedRouter` and its types
299
+ - Deprecated `RouteLink` and `RouteLinkProps` in favor of `NestedRouteLink`
300
+ - Deprecated `LinkToRoute` and `LinkToRouteProps` in favor of `NestedRouteLink`
301
+
302
+ ### 🐛 Bug Fixes
303
+
304
+ - Fixed `onLeave` lifecycle hooks not firing correctly when navigating between nested routes
305
+
306
+ ### ♻️ Refactoring
307
+
308
+ - `appendChild` in `shade-component.ts` now accepts `Element | DocumentFragment` instead of `HTMLElement | DocumentFragment` for broader compatibility
309
+ - `createComponent` now acts as a render-mode switch: when in render mode it produces VNode descriptors, otherwise it creates real DOM elements as before
310
+
311
+ ### ⬆️ Dependencies
312
+
313
+ - Peer dependency on `@furystack/shades` updated to the new major version for downstream packages
314
+
3
315
  ## [11.1.0] - 2026-02-01
4
316
 
5
317
  ### ✨ Features
package/README.md CHANGED
@@ -18,12 +18,8 @@ You can check the [@furystack/boilerplate](https://github.com/furystack/boilerpl
18
18
 
19
19
  A shade (component) can be constructed from the following properties:
20
20
 
21
- - `render:(options: RenderOptions)=>JSX.Element` – A required method that will be executed on each render.
22
- - `initialState` – A default state that can be updated during the component lifecycle.
23
- - `shadowDomName` – Can be specified as the custom element's name in the DOM.
24
- - `constructed: (options: RenderOptions)=>void` – Optional callback executed after component construction. It can return a cleanup method (e.g., free up resources, dispose value observers, etc.).
25
- - `onAttach: (options: RenderOptions)=>void` – Executed when the component is attached to the DOM.
26
- - `onDetach: (options: RenderOptions)=>void` – Executed when the component is detached from the DOM.
21
+ - `render:(options: RenderOptions)=>JSX.Element` – A required method that will be executed on each render. Use `useDisposable` within render for one-time setup that needs cleanup.
22
+ - `shadowDomName` – The custom element tag name. Must follow Custom Elements naming convention (lowercase, must contain a hyphen).
27
23
  - `style` – Optional inline styles applied to each component instance. Use for per-instance overrides.
28
24
  - `css` – Optional CSS styles injected as a stylesheet during component registration. Supports pseudo-selectors and nested selectors.
29
25
 
@@ -113,15 +109,19 @@ Both properties can be used together. Inline `style` will override `css` due to
113
109
 
114
110
  ### Render Options
115
111
 
116
- The lifecycle methods receive the following options as a parameter:
112
+ The `render` function receives a `RenderOptions` object with these hooks:
117
113
 
118
- - `props` – The current readonly props object for the element. As props are passed from the parent, it is read-only.
119
- - `getState()` – Returns the current state. The state object is also read-only and immutable and can be updated only with a corresponding method.
120
- - `updateState(newState: TState, skipRender?: boolean)` – Updates (patches) the component state. An optional flag can indicate that this state change shouldn't trigger a re-render (e.g., form input fields change, etc.).
121
- - `injector` – An injector instance. It can be retrieved from the closest parent or specified on the state or props.
114
+ - `props` – The current readonly props object. Passed from the parent, treat as immutable.
115
+ - `injector` – The injector instance, inherited from the closest parent or set explicitly.
122
116
  - `children` – The children element(s) of the component.
123
- - `element` – A reference to the current component's custom element (root).
124
- - `logger` – A specified logger instance with a pre-defined scope.
117
+ - `renderCount` – How many times this component has rendered.
118
+ - `useState(key, initialValue)` – Local state that triggers re-renders on change.
119
+ - `useObservable(key, observable, options?)` – Subscribes to an `ObservableValue`; re-renders on change by default. Provide a custom `onChange` to skip re-renders.
120
+ - `useSearchState(key, initialValue)` – State synced with URL search parameters.
121
+ - `useStoredState(key, initialValue, storageArea?)` – State persisted to `localStorage` or `sessionStorage`.
122
+ - `useDisposable(key, factory)` – Creates a resource that is automatically disposed when the component unmounts.
123
+ - `useHostProps(hostProps)` – Declaratively sets attributes, styles, and CSS variables on the host element. Prefer this over direct DOM manipulation.
124
+ - `useRef(key)` – Creates a ref object for imperative access to child DOM elements.
125
125
 
126
126
  ### Bundled Goodies
127
127
 
@@ -2,7 +2,7 @@ import { Injector } from '@furystack/inject';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
  import './jsx';
4
4
  import { createComponent } from './shade-component.js';
5
- import { Shade } from './shade.js';
5
+ import { flushUpdates, Shade } from './shade.js';
6
6
  describe('Shades Component Factory', () => {
7
7
  describe('HTML Elements', () => {
8
8
  it('Should create a simple component', () => {
@@ -21,6 +21,12 @@ describe('Shades Component Factory', () => {
21
21
  expect(component).toBeInstanceOf(HTMLDivElement);
22
22
  expect(component.getAttribute('data-testid')).toBe('asd-123');
23
23
  });
24
+ it('Should apply aria attributes', () => {
25
+ const component = (createComponent("div", { "aria-label": "My label", "aria-hidden": "true" }, "a"));
26
+ expect(component).toBeInstanceOf(HTMLDivElement);
27
+ expect(component.getAttribute('aria-label')).toBe('My label');
28
+ expect(component.getAttribute('aria-hidden')).toBe('true');
29
+ });
24
30
  it('Should create a nested component', () => {
25
31
  const component = (createComponent("div", { style: { display: 'flex' } },
26
32
  createComponent("h1", null, "Hi, I'm a header"),
@@ -47,7 +53,7 @@ describe('Shades Component Factory', () => {
47
53
  expect(shade.props).toEqual({});
48
54
  expect(shade.shadeChildren).toEqual([]);
49
55
  });
50
- it('Should render a component with props', () => {
56
+ it('Should render a component with props', async () => {
51
57
  const Example = Shade({
52
58
  shadowDomName: 'example-with-props',
53
59
  render: ({ props }) => createComponent("div", null, props.foo),
@@ -55,12 +61,13 @@ describe('Shades Component Factory', () => {
55
61
  const component = (createComponent("div", null,
56
62
  createComponent(Example, { foo: "example", injector: new Injector() })));
57
63
  const shade = component.firstElementChild;
58
- shade.callConstructed();
64
+ shade.updateComponent();
65
+ await flushUpdates();
59
66
  expect(shade.props.foo).toEqual('example');
60
67
  expect(shade.shadeChildren).toEqual([]);
61
68
  expect(shade.innerHTML).toBe('<div>example</div>');
62
69
  });
63
- it('Should render a component with state', () => {
70
+ it('Should render a component with state', async () => {
64
71
  const Example = Shade({
65
72
  shadowDomName: 'example-with-state',
66
73
  render: ({ useState }) => {
@@ -71,7 +78,8 @@ describe('Shades Component Factory', () => {
71
78
  const component = (createComponent("div", null,
72
79
  createComponent(Example, null)));
73
80
  const shade = component.firstElementChild;
74
- shade.callConstructed();
81
+ shade.updateComponent();
82
+ await flushUpdates();
75
83
  expect(shade.resourceManager.stateObservers.get('foo')?.getValue()).toEqual('example');
76
84
  expect(shade.shadeChildren).toEqual([]);
77
85
  });
@@ -1 +1 @@
1
- {"version":3,"file":"component-factory.spec.js","sourceRoot":"","sources":["../src/component-factory.spec.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"component-factory.spec.js","sourceRoot":"","sources":["../src/component-factory.spec.tsx"],"names":[],"mappings":"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"}
@@ -1,5 +1,8 @@
1
1
  export * from './lazy-load.js';
2
2
  export * from './link-to-route.js';
3
- export * from './router.js';
3
+ export * from './nested-route-link.js';
4
+ export * from './nested-route-types.js';
5
+ export * from './nested-router.js';
4
6
  export * from './route-link.js';
7
+ export * from './router.js';
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA"}
@@ -1,5 +1,8 @@
1
1
  export * from './lazy-load.js';
2
2
  export * from './link-to-route.js';
3
- export * from './router.js';
3
+ export * from './nested-route-link.js';
4
+ export * from './nested-route-types.js';
5
+ export * from './nested-router.js';
4
6
  export * from './route-link.js';
7
+ export * from './router.js';
5
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA"}
@@ -3,11 +3,9 @@ export interface LazyLoadProps {
3
3
  error?: (error: unknown, retry: () => Promise<void>) => JSX.Element;
4
4
  component: () => Promise<JSX.Element>;
5
5
  }
6
- export interface LazyLoadState {
7
- component?: JSX.Element;
8
- error?: unknown;
9
- }
10
6
  export declare const LazyLoad: (props: LazyLoadProps & Omit<Partial<HTMLElement>, "style"> & {
11
7
  style?: Partial<CSSStyleDeclaration>;
8
+ } & {
9
+ ref?: import("../index.js").RefObject<Element>;
12
10
  }, children?: import("../index.js").ChildrenList) => JSX.Element;
13
11
  //# sourceMappingURL=lazy-load.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lazy-load.d.ts","sourceRoot":"","sources":["../../src/components/lazy-load.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,GAAG,CAAC,OAAO,CAAA;IACnB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;IACnE,SAAS,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;CACtC;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACvB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,eAAO,MAAM,QAAQ;;gEAyCnB,CAAA"}
1
+ {"version":3,"file":"lazy-load.d.ts","sourceRoot":"","sources":["../../src/components/lazy-load.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,GAAG,CAAC,OAAO,CAAA;IACnB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;IACnE,SAAS,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;CACtC;AAED,eAAO,MAAM,QAAQ;;;;gEAoEnB,CAAA"}
@@ -1,39 +1,55 @@
1
1
  import { Shade } from '../shade.js';
2
2
  export const LazyLoad = Shade({
3
3
  shadowDomName: 'lazy-load',
4
- constructed: async ({ props, useState, element }) => {
5
- const [_component, setComponent] = useState('component', undefined);
6
- const [_errorState, setErrorState] = useState('error', undefined);
7
- try {
8
- const loaded = await props.component();
9
- if (element.isConnected) {
10
- setComponent(loaded);
11
- }
12
- }
13
- catch (error) {
14
- if (props.error) {
15
- if (element.isConnected) {
16
- setErrorState(error);
17
- }
18
- }
19
- else {
20
- throw error;
21
- }
22
- }
23
- },
24
- render: ({ props, useState }) => {
4
+ render: ({ props, useState, useDisposable }) => {
25
5
  const [error, setError] = useState('error', undefined);
26
6
  const [component, setComponent] = useState('component', undefined);
7
+ const tracker = useDisposable('loadTracker', () => {
8
+ const state = {
9
+ factory: null,
10
+ active: true,
11
+ [Symbol.dispose]() {
12
+ state.active = false;
13
+ },
14
+ };
15
+ return state;
16
+ });
17
+ const isNewFactory = tracker.factory !== props.component;
18
+ if (isNewFactory) {
19
+ tracker.factory = props.component;
20
+ const factory = props.component;
21
+ factory()
22
+ .then((loaded) => {
23
+ if (tracker.active && tracker.factory === factory) {
24
+ setError(undefined);
25
+ setComponent(loaded);
26
+ }
27
+ })
28
+ .catch((err) => {
29
+ if (tracker.active && tracker.factory === factory) {
30
+ setComponent(undefined);
31
+ if (props.error) {
32
+ setError(err);
33
+ }
34
+ }
35
+ });
36
+ return props.loader;
37
+ }
27
38
  if (error && props.error) {
28
39
  return props.error(error, async () => {
40
+ const factory = props.component;
29
41
  try {
30
42
  setError(undefined);
31
43
  setComponent(undefined);
32
- const loaded = await props.component();
33
- setComponent(loaded);
44
+ const loaded = await factory();
45
+ if (tracker.active && tracker.factory === factory) {
46
+ setComponent(loaded);
47
+ }
34
48
  }
35
49
  catch (e) {
36
- setError(e);
50
+ if (tracker.active && tracker.factory === factory) {
51
+ setError(e);
52
+ }
37
53
  }
38
54
  });
39
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lazy-load.js","sourceRoot":"","sources":["../../src/components/lazy-load.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAanC,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAgB;IAC3C,aAAa,EAAE,WAAW;IAC1B,WAAW,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE;QAClD,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,GAAG,QAAQ,CAA0B,WAAW,EAAE,SAAS,CAAC,CAAA;QAC5F,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAU,OAAO,EAAE,SAAS,CAAC,CAAA;QAC1E,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,CAAA;YACtC,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;gBACxB,YAAY,CAAC,MAAM,CAAC,CAAA;YACtB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBAChB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;oBACxB,aAAa,CAAC,KAAK,CAAC,CAAA;gBACtB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC9B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAU,OAAO,EAAE,SAAS,CAAC,CAAA;QAC/D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAA0B,WAAW,EAAE,SAAS,CAAC,CAAA;QAE3F,IAAI,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;gBACnC,IAAI,CAAC;oBACH,QAAQ,CAAC,SAAS,CAAC,CAAA;oBACnB,YAAY,CAAC,SAAS,CAAC,CAAA;oBACvB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,CAAA;oBACtC,YAAY,CAAC,MAAM,CAAC,CAAA;gBACtB,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,QAAQ,CAAC,CAAC,CAAC,CAAA;gBACb,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,OAAO,KAAK,CAAC,MAAM,CAAA;IACrB,CAAC;CACF,CAAC,CAAA"}
1
+ {"version":3,"file":"lazy-load.js","sourceRoot":"","sources":["../../src/components/lazy-load.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAQnC,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAgB;IAC3C,aAAa,EAAE,WAAW;IAC1B,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE;QAC7C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAU,OAAO,EAAE,SAAS,CAAC,CAAA;QAC/D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAA0B,WAAW,EAAE,SAAS,CAAC,CAAA;QAE3F,MAAM,OAAO,GAAG,aAAa,CAAC,aAAa,EAAE,GAAG,EAAE;YAChD,MAAM,KAAK,GAIP;gBACF,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,IAAI;gBACZ,CAAC,MAAM,CAAC,OAAO,CAAC;oBACd,KAAK,CAAC,MAAM,GAAG,KAAK,CAAA;gBACtB,CAAC;aACF,CAAA;YACD,OAAO,KAAK,CAAA;QACd,CAAC,CAAC,CAAA;QAEF,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;QAExD,IAAI,YAAY,EAAE,CAAC;YACjB,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,SAAS,CAAA;YACjC,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAA;YAE/B,OAAO,EAAE;iBACN,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;gBACf,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;oBAClD,QAAQ,CAAC,SAAS,CAAC,CAAA;oBACnB,YAAY,CAAC,MAAM,CAAC,CAAA;gBACtB,CAAC;YACH,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;gBACtB,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;oBAClD,YAAY,CAAC,SAAS,CAAC,CAAA;oBACvB,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;wBAChB,QAAQ,CAAC,GAAG,CAAC,CAAA;oBACf,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAA;YAEJ,OAAO,KAAK,CAAC,MAAM,CAAA;QACrB,CAAC;QAED,IAAI,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;gBACnC,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAA;gBAC/B,IAAI,CAAC;oBACH,QAAQ,CAAC,SAAS,CAAC,CAAA;oBACnB,YAAY,CAAC,SAAS,CAAC,CAAA;oBACvB,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;oBAC9B,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;wBAClD,YAAY,CAAC,MAAM,CAAC,CAAA;oBACtB,CAAC;gBACH,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;wBAClD,QAAQ,CAAC,CAAC,CAAC,CAAA;oBACb,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,OAAO,KAAK,CAAC,MAAM,CAAA;IACrB,CAAC;CACF,CAAC,CAAA"}