@furystack/shades 13.0.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## [13.1.0] - 2026-03-10
4
+
5
+ ### ✨ Features
6
+
7
+ ### Spatial Navigation Service
8
+
9
+ Added `SpatialNavigationService` for D-pad / arrow-key spatial navigation across interactive elements. The service intercepts arrow key events and moves focus spatially based on element geometry, supporting section boundaries via `data-nav-section` attributes and optional cross-section navigation.
10
+
11
+ **Key capabilities:**
12
+
13
+ - Arrow key focus movement based on Euclidean distance between element centers
14
+ - Section-scoped navigation with `data-nav-section` DOM attributes
15
+ - Cross-section navigation with focus memory (restores last-focused element per section)
16
+ - Input passthrough — arrow keys work normally inside text inputs, textareas, selects, and contenteditable elements
17
+ - Configurable `Backspace` → `history.back()` and `Escape` → parent section behaviors
18
+ - Runtime enable/disable via `enabled` observable
19
+ - `configureSpatialNavigation()` helper to set options before the singleton is first resolved
20
+
21
+ **Usage:**
22
+
23
+ ```typescript
24
+ import { SpatialNavigationService, configureSpatialNavigation } from '@furystack/shades'
25
+
26
+ // Configure before first use
27
+ configureSpatialNavigation(injector, {
28
+ initiallyEnabled: true,
29
+ crossSectionNavigation: true,
30
+ })
31
+
32
+ // Or resolve directly with defaults
33
+ const spatialNav = injector.getInstance(SpatialNavigationService)
34
+
35
+ // Toggle at runtime
36
+ spatialNav.enabled.setValue(false)
37
+ ```
38
+
39
+ ### `useDisposable` deps parameter
40
+
41
+ Added optional `deps` parameter to `useDisposable` — when provided, the resource is re-created (and the old one disposed) whenever the serialized deps value changes.
42
+
43
+ ```typescript
44
+ useDisposable('my-resource', () => createResource(value), [value])
45
+ ```
46
+
47
+ ### 🧪 Tests
48
+
49
+ - Added tests for `SpatialNavigationService` covering directional movement, section boundaries, cross-section navigation, input passthrough, focus memory, and disposal
50
+
51
+ ### ⬆️ Dependencies
52
+
53
+ - Updated `@furystack/core` dependency to the new major version
54
+
3
55
  ## [13.0.0] - 2026-03-07
4
56
 
5
57
  ### ⬆️ Dependencies
@@ -77,12 +77,15 @@ export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement
77
77
  */
78
78
  useRef: <T extends Element = HTMLElement>(key: string) => RefObject<T>;
79
79
  /**
80
- * Creates and disposes a resource after the component has been detached from the DOM
80
+ * Creates and disposes a resource after the component has been detached from the DOM.
81
+ * When `deps` is provided, the resource is re-created (and the old one disposed) whenever
82
+ * the serialized deps value changes.
81
83
  * @param key The key for caching the disposable resource
82
84
  * @param factory A factory method for creating the disposable resource
85
+ * @param deps Optional dependency array — when deps change, the old resource is disposed and a new one is created
83
86
  * @returns The Disposable instance
84
87
  */
85
- useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T) => T;
88
+ useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T, deps?: readonly unknown[]) => T;
86
89
  /**
87
90
  * Creates a state object from an existing observable value.
88
91
  *
@@ -1 +1 @@
1
- {"version":3,"file":"render-options.d.ts","sourceRoot":"","sources":["../../src/models/render-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,IAAI;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,EAAE,YAAY,SAAS,WAAW,GAAG,WAAW,IAAI;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,YAAY,CAAC,CAAA;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAA;IAEtE;;;;;OAKG;IACH,aAAa,EAAE,CAAC,CAAC,SAAS,UAAU,GAAG,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IAE3F;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,aAAa,EAAE,CAAC,CAAC,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAA;KAAE,KACrE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAEhD;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAE1F;;;;;OAKG;IACH,cAAc,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAEhG;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,CAAC,EAChB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,WAAW,CAAC,EAAE,OAAO,KAClB,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;CACjD,CAAA"}
1
+ {"version":3,"file":"render-options.d.ts","sourceRoot":"","sources":["../../src/models/render-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,IAAI;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,EAAE,YAAY,SAAS,WAAW,GAAG,WAAW,IAAI;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,YAAY,CAAC,CAAA;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAA;IAEtE;;;;;;;;OAQG;IACH,aAAa,EAAE,CAAC,CAAC,SAAS,UAAU,GAAG,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,KAAK,CAAC,CAAA;IAEtH;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,aAAa,EAAE,CAAC,CAAC,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAA;KAAE,KACrE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAEhD;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAE1F;;;;;OAKG;IACH,cAAc,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAEhG;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,CAAC,EAChB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,WAAW,CAAC,EAAE,OAAO,KAClB,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;CACjD,CAAA"}
@@ -2,4 +2,5 @@ export * from './location-service.js';
2
2
  export * from './route-match-service.js';
3
3
  export * from './route-meta-utils.js';
4
4
  export * from './screen-service.js';
5
+ export * from './spatial-navigation-service.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA"}
@@ -2,4 +2,5 @@ export * from './location-service.js';
2
2
  export * from './route-match-service.js';
3
3
  export * from './route-meta-utils.js';
4
4
  export * from './screen-service.js';
5
+ export * from './spatial-navigation-service.js';
5
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA"}
@@ -0,0 +1,88 @@
1
+ import { type Injector } from '@furystack/inject';
2
+ import { ObservableValue } from '@furystack/utils';
3
+ /**
4
+ * Direction for spatial navigation movement.
5
+ */
6
+ export type SpatialDirection = 'up' | 'down' | 'left' | 'right';
7
+ /**
8
+ * Configuration options for the SpatialNavigationService.
9
+ */
10
+ export type SpatialNavigationOptions = {
11
+ /** Whether spatial navigation is enabled on startup. Default: true */
12
+ initiallyEnabled?: boolean;
13
+ /** Whether to allow cross-section navigation. Default: true */
14
+ crossSectionNavigation?: boolean;
15
+ /** Custom focusable selector override */
16
+ focusableSelector?: string;
17
+ /** Whether Backspace triggers history.back(). Default: false */
18
+ backspaceGoesBack?: boolean;
19
+ /** Whether Escape moves focus to parent section. Default: false */
20
+ escapeGoesToParentSection?: boolean;
21
+ };
22
+ /**
23
+ * Service for D-pad / arrow-key spatial navigation across interactive elements.
24
+ *
25
+ * Intercepts arrow key events and moves focus spatially based on element geometry.
26
+ * Supports section boundaries via `data-nav-section` attributes and optional
27
+ * cross-section navigation.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // Opt in to spatial navigation
32
+ * const spatialNav = injector.getInstance(SpatialNavigationService)
33
+ *
34
+ * // Disable during video playback
35
+ * spatialNav.enabled.setValue(false)
36
+ *
37
+ * // Re-enable
38
+ * spatialNav.enabled.setValue(true)
39
+ * ```
40
+ */
41
+ export declare class SpatialNavigationService implements Disposable {
42
+ /** Toggle spatial navigation on/off at runtime */
43
+ readonly enabled: ObservableValue<boolean>;
44
+ /** The currently active section name (from data-nav-section), or null if none */
45
+ readonly activeSection: ObservableValue<string | null>;
46
+ /** Remembered last-focused element per section for focus restoration */
47
+ private readonly focusMemory;
48
+ private readonly focusTrapStack;
49
+ private readonly focusableSelector;
50
+ private readonly crossSectionNavigation;
51
+ private readonly backspaceGoesBack;
52
+ private readonly escapeGoesToParentSection;
53
+ constructor(options?: SpatialNavigationOptions);
54
+ [Symbol.dispose](): void;
55
+ /**
56
+ * Push a focus trap onto the stack. While the trap is active, cross-section
57
+ * navigation is blocked and `activeSection` is locked to `sectionName`.
58
+ * Supports nesting — only the topmost trap is enforced.
59
+ */
60
+ pushFocusTrap(sectionName: string): void;
61
+ /**
62
+ * Remove a focus trap from the stack. If other traps remain, the topmost
63
+ * one becomes active. Otherwise `activeSection` reverts to `previousSection`.
64
+ */
65
+ popFocusTrap(sectionName: string, previousSection?: string | null): void;
66
+ private get activeTrap();
67
+ /** Programmatically move focus in a direction */
68
+ moveFocus(direction: SpatialDirection): void;
69
+ /** Programmatically activate (click) the currently focused element */
70
+ activateFocused(): void;
71
+ private handleKeyDown;
72
+ private focusFirstElement;
73
+ private findContainingSection;
74
+ private isVisibleInScrollContainers;
75
+ private getFocusableCandidates;
76
+ private findNearestInDirection;
77
+ private navigateCrossSection;
78
+ private storeFocusMemory;
79
+ private moveToParentSection;
80
+ }
81
+ /**
82
+ * Configures spatial navigation options before the service is first instantiated.
83
+ * Must be called **before** `SpatialNavigationService` is first resolved from the injector.
84
+ * @param injector The root injector
85
+ * @param options Configuration options for spatial navigation
86
+ */
87
+ export declare const configureSpatialNavigation: (injector: Injector, options: SpatialNavigationOptions) => void;
88
+ //# sourceMappingURL=spatial-navigation-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatial-navigation-service.d.ts","sourceRoot":"","sources":["../../src/services/spatial-navigation-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAE/D;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,+DAA+D;IAC/D,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,yCAAyC;IACzC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,mEAAmE;IACnE,yBAAyB,CAAC,EAAE,OAAO,CAAA;CACpC,CAAA;AAoND;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,wBAAyB,YAAW,UAAU;IACzD,kDAAkD;IAClD,SAAgB,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,CAAA;IAEjD,iFAAiF;IACjF,SAAgB,aAAa,iCAA2C;IAExE,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAElE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAe;IAE9C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAS;gBAEvC,OAAO,GAAE,wBAA6B;IAU3C,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;IAQ/B;;;;OAIG;IACI,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAK/C;;;OAGG;IACI,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAS/E,OAAO,KAAK,UAAU,GAErB;IAED,iDAAiD;IAC1C,SAAS,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI;IAuCnD,sEAAsE;IAC/D,eAAe,IAAI,IAAI;IAO9B,OAAO,CAAC,aAAa,CAmDpB;IAED,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,2BAA2B;IAuBnC,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,sBAAsB;IAwB9B,OAAO,CAAC,oBAAoB;IA8C5B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,mBAAmB;CAgB5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,GAAI,UAAU,QAAQ,EAAE,SAAS,wBAAwB,KAAG,IAOlG,CAAA"}