@furystack/shades 13.2.2 → 15.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 (146) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/esm/component-factory.spec.d.ts.map +1 -1
  3. package/esm/component-factory.spec.js +2 -2
  4. package/esm/component-factory.spec.js.map +1 -1
  5. package/esm/components/define-nested-routes.d.ts +27 -0
  6. package/esm/components/define-nested-routes.d.ts.map +1 -0
  7. package/esm/components/define-nested-routes.js +26 -0
  8. package/esm/components/define-nested-routes.js.map +1 -0
  9. package/esm/components/define-nested-routes.spec.d.ts +2 -0
  10. package/esm/components/define-nested-routes.spec.d.ts.map +1 -0
  11. package/esm/components/define-nested-routes.spec.js +81 -0
  12. package/esm/components/define-nested-routes.spec.js.map +1 -0
  13. package/esm/components/index.d.ts +3 -3
  14. package/esm/components/index.d.ts.map +1 -1
  15. package/esm/components/index.js +3 -3
  16. package/esm/components/index.js.map +1 -1
  17. package/esm/components/nested-hooks.d.ts +62 -0
  18. package/esm/components/nested-hooks.d.ts.map +1 -0
  19. package/esm/components/nested-hooks.js +73 -0
  20. package/esm/components/nested-hooks.js.map +1 -0
  21. package/esm/components/nested-hooks.spec.d.ts +2 -0
  22. package/esm/components/nested-hooks.spec.d.ts.map +1 -0
  23. package/esm/components/nested-hooks.spec.js +211 -0
  24. package/esm/components/nested-hooks.spec.js.map +1 -0
  25. package/esm/components/nested-navigate.d.ts +52 -21
  26. package/esm/components/nested-navigate.d.ts.map +1 -1
  27. package/esm/components/nested-navigate.js +32 -17
  28. package/esm/components/nested-navigate.js.map +1 -1
  29. package/esm/components/nested-navigate.spec.js +109 -19
  30. package/esm/components/nested-navigate.spec.js.map +1 -1
  31. package/esm/components/nested-replace.d.ts +36 -0
  32. package/esm/components/nested-replace.d.ts.map +1 -0
  33. package/esm/components/nested-replace.js +38 -0
  34. package/esm/components/nested-replace.js.map +1 -0
  35. package/esm/components/nested-replace.spec.d.ts +2 -0
  36. package/esm/components/nested-replace.spec.d.ts.map +1 -0
  37. package/esm/components/nested-replace.spec.js +59 -0
  38. package/esm/components/nested-replace.spec.js.map +1 -0
  39. package/esm/components/nested-route-link.d.ts +36 -26
  40. package/esm/components/nested-route-link.d.ts.map +1 -1
  41. package/esm/components/nested-route-link.js +18 -20
  42. package/esm/components/nested-route-link.js.map +1 -1
  43. package/esm/components/nested-route-link.spec.js +67 -17
  44. package/esm/components/nested-route-link.spec.js.map +1 -1
  45. package/esm/components/nested-route-types.d.ts +79 -2
  46. package/esm/components/nested-route-types.d.ts.map +1 -1
  47. package/esm/components/nested-router.d.ts +77 -7
  48. package/esm/components/nested-router.d.ts.map +1 -1
  49. package/esm/components/nested-router.js +139 -13
  50. package/esm/components/nested-router.js.map +1 -1
  51. package/esm/components/nested-router.spec.js +283 -52
  52. package/esm/components/nested-router.spec.js.map +1 -1
  53. package/esm/services/location-service.d.ts +39 -42
  54. package/esm/services/location-service.d.ts.map +1 -1
  55. package/esm/services/location-service.js +115 -134
  56. package/esm/services/location-service.js.map +1 -1
  57. package/esm/services/location-service.spec.js +59 -24
  58. package/esm/services/location-service.spec.js.map +1 -1
  59. package/esm/services/route-match-service.d.ts +3 -2
  60. package/esm/services/route-match-service.d.ts.map +1 -1
  61. package/esm/services/route-match-service.js +13 -22
  62. package/esm/services/route-match-service.js.map +1 -1
  63. package/esm/services/route-match-service.spec.js +42 -49
  64. package/esm/services/route-match-service.spec.js.map +1 -1
  65. package/esm/services/route-meta-utils.d.ts +28 -6
  66. package/esm/services/route-meta-utils.d.ts.map +1 -1
  67. package/esm/services/route-meta-utils.js +18 -4
  68. package/esm/services/route-meta-utils.js.map +1 -1
  69. package/esm/services/route-meta-utils.spec.js +60 -5
  70. package/esm/services/route-meta-utils.spec.js.map +1 -1
  71. package/esm/services/screen-service.d.ts +13 -72
  72. package/esm/services/screen-service.d.ts.map +1 -1
  73. package/esm/services/screen-service.js +45 -137
  74. package/esm/services/screen-service.js.map +1 -1
  75. package/esm/services/screen-service.spec.js +19 -18
  76. package/esm/services/screen-service.spec.js.map +1 -1
  77. package/esm/services/spatial-navigation-service.d.ts +17 -39
  78. package/esm/services/spatial-navigation-service.d.ts.map +1 -1
  79. package/esm/services/spatial-navigation-service.js +280 -313
  80. package/esm/services/spatial-navigation-service.js.map +1 -1
  81. package/esm/services/spatial-navigation-service.spec.js +163 -158
  82. package/esm/services/spatial-navigation-service.spec.js.map +1 -1
  83. package/esm/shade.d.ts +1 -1
  84. package/esm/shade.d.ts.map +1 -1
  85. package/esm/shade.js +10 -7
  86. package/esm/shade.js.map +1 -1
  87. package/esm/vnode.d.ts +1 -1
  88. package/esm/vnode.js +1 -1
  89. package/package.json +5 -4
  90. package/src/component-factory.spec.tsx +3 -2
  91. package/src/components/define-nested-routes.spec.ts +91 -0
  92. package/src/components/define-nested-routes.ts +27 -0
  93. package/src/components/index.ts +3 -3
  94. package/src/components/nested-hooks.spec.ts +254 -0
  95. package/src/components/nested-hooks.ts +109 -0
  96. package/src/components/nested-navigate.spec.ts +136 -32
  97. package/src/components/nested-navigate.ts +75 -26
  98. package/src/components/nested-replace.spec.ts +89 -0
  99. package/src/components/nested-replace.ts +46 -0
  100. package/src/components/nested-route-link.spec.tsx +90 -22
  101. package/src/components/nested-route-link.tsx +54 -29
  102. package/src/components/nested-route-types.ts +82 -2
  103. package/src/components/nested-router.spec.tsx +334 -51
  104. package/src/components/nested-router.tsx +184 -24
  105. package/src/services/location-service.spec.ts +68 -25
  106. package/src/services/location-service.tsx +156 -122
  107. package/src/services/route-match-service.spec.ts +41 -28
  108. package/src/services/route-match-service.ts +17 -8
  109. package/src/services/route-meta-utils.spec.ts +78 -10
  110. package/src/services/route-meta-utils.ts +44 -15
  111. package/src/services/screen-service.spec.ts +19 -18
  112. package/src/services/screen-service.ts +65 -110
  113. package/src/services/spatial-navigation-service.spec.ts +165 -160
  114. package/src/services/spatial-navigation-service.ts +299 -287
  115. package/src/shade.ts +11 -8
  116. package/src/vnode.ts +1 -1
  117. package/esm/components/link-to-route.d.ts +0 -10
  118. package/esm/components/link-to-route.d.ts.map +0 -1
  119. package/esm/components/link-to-route.js +0 -16
  120. package/esm/components/link-to-route.js.map +0 -1
  121. package/esm/components/link-to-route.spec.d.ts +0 -2
  122. package/esm/components/link-to-route.spec.d.ts.map +0 -1
  123. package/esm/components/link-to-route.spec.js +0 -30
  124. package/esm/components/link-to-route.spec.js.map +0 -1
  125. package/esm/components/route-link.d.ts +0 -10
  126. package/esm/components/route-link.d.ts.map +0 -1
  127. package/esm/components/route-link.js +0 -25
  128. package/esm/components/route-link.js.map +0 -1
  129. package/esm/components/route-link.spec.d.ts +0 -2
  130. package/esm/components/route-link.spec.d.ts.map +0 -1
  131. package/esm/components/route-link.spec.js +0 -34
  132. package/esm/components/route-link.spec.js.map +0 -1
  133. package/esm/components/router.d.ts +0 -44
  134. package/esm/components/router.d.ts.map +0 -1
  135. package/esm/components/router.js +0 -66
  136. package/esm/components/router.js.map +0 -1
  137. package/esm/components/router.spec.d.ts +0 -2
  138. package/esm/components/router.spec.d.ts.map +0 -1
  139. package/esm/components/router.spec.js +0 -161
  140. package/esm/components/router.spec.js.map +0 -1
  141. package/src/components/link-to-route.spec.tsx +0 -45
  142. package/src/components/link-to-route.tsx +0 -26
  143. package/src/components/route-link.spec.tsx +0 -44
  144. package/src/components/route-link.tsx +0 -29
  145. package/src/components/router.spec.tsx +0 -217
  146. package/src/components/router.tsx +0 -103
@@ -1,5 +1,6 @@
1
1
  import type { Injector } from '@furystack/inject'
2
2
 
3
+ import type { ExtractRoutePaths } from '../components/nested-route-types.js'
3
4
  import type { MatchChainEntry, NestedRoute, NestedRouteMeta } from '../components/nested-router.js'
4
5
 
5
6
  /**
@@ -57,29 +58,57 @@ export const buildDocumentTitle = (
57
58
 
58
59
  /**
59
60
  * A node in a navigation tree extracted from route definitions.
61
+ *
62
+ * When {@link extractNavTree} is called with an explicit route tree type
63
+ * parameter (e.g. `extractNavTree<typeof appRoutes>(...)`), `pattern` narrows
64
+ * to the declared route keys and `fullPath` narrows to the union of all
65
+ * composed paths in the subtree. Without a type parameter the generic falls
66
+ * back to a widened shape so ad-hoc consumers keep compiling.
67
+ *
68
+ * @typeParam TRoutes - The route tree the node was extracted from
60
69
  */
61
- export type NavTreeNode = {
62
- pattern: string
63
- fullPath: string
70
+ export type NavTreeNode<
71
+ TRoutes extends Record<string, NestedRoute<any, any, any>> = Record<string, NestedRoute<any, any, any>>,
72
+ > = {
73
+ pattern: keyof TRoutes & string
74
+ fullPath: ExtractRoutePaths<TRoutes> & string
64
75
  meta?: NestedRouteMeta
65
- children?: NavTreeNode[]
76
+ children?: Array<NavTreeNode<TRoutes>>
66
77
  }
67
78
 
68
79
  /**
69
80
  * Extracts a navigation tree from route definitions.
70
- * Useful for rendering sidebar navigation or sitemap-like structures.
81
+ *
82
+ * The returned nodes preserve the route tree's static typing: `pattern` is
83
+ * narrowed to the declared keys of `TRoutes` and `fullPath` is narrowed to
84
+ * the union of composed paths reachable from `TRoutes` (as produced by
85
+ * {@link ExtractRoutePaths}). This makes the output a single, typed source
86
+ * of truth for sidebar navigation, breadcrumbs and other route-tree-aware
87
+ * UIs — without requiring `as` casts at the call site.
88
+ *
89
+ * Child nodes share their parent's `TRoutes` type parameter; the resulting
90
+ * union is a safe upper bound of the actual paths at any depth, which keeps
91
+ * the helper simple while still accepted by `createNestedRouteLink`,
92
+ * `createNestedNavigate` and `createNestedReplace` factories bound to the
93
+ * same route tree.
94
+ *
71
95
  * @param routes - The route definitions to extract from
72
96
  * @param parentPath - The parent path prefix (used internally for recursion)
73
97
  * @returns An array of navigation tree nodes
74
98
  */
75
- export const extractNavTree = (routes: Record<string, NestedRoute<unknown>>, parentPath?: string): NavTreeNode[] => {
76
- return Object.entries(routes).map(([pattern, route]) => {
77
- const fullPath = parentPath ? `${parentPath === '/' ? '' : parentPath}${pattern}` : pattern
78
- return {
79
- pattern,
80
- fullPath,
81
- meta: route.meta,
82
- children: route.children ? extractNavTree(route.children, fullPath) : undefined,
83
- }
84
- })
99
+ export const extractNavTree = <TRoutes extends Record<string, NestedRoute<any, any, any>>>(
100
+ routes: TRoutes,
101
+ parentPath?: string,
102
+ ): Array<NavTreeNode<TRoutes>> => {
103
+ const build = (rs: Record<string, NestedRoute<any, any, any>>, pp?: string): Array<NavTreeNode<TRoutes>> =>
104
+ Object.entries(rs).map(([pattern, route]) => {
105
+ const fullPath = pp ? `${pp === '/' ? '' : pp}${pattern}` : pattern
106
+ return {
107
+ pattern,
108
+ fullPath,
109
+ meta: route.meta,
110
+ children: route.children ? build(route.children, fullPath) : undefined,
111
+ } as NavTreeNode<TRoutes>
112
+ })
113
+ return build(routes, parentPath)
85
114
  }
@@ -1,4 +1,4 @@
1
- import { Injector } from '@furystack/inject'
1
+ import { createInjector } from '@furystack/inject'
2
2
  import { usingAsync } from '@furystack/utils'
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
4
  import { ScreenService, ScreenSizes } from './screen-service.js'
@@ -13,16 +13,17 @@ describe('ScreenService', () => {
13
13
  })
14
14
 
15
15
  it('Should be constructed', async () => {
16
- await usingAsync(new Injector(), async (i) => {
17
- const s = i.getInstance(ScreenService)
18
- expect(s).toBeInstanceOf(ScreenService)
16
+ await usingAsync(createInjector(), async (i) => {
17
+ const s = i.get(ScreenService)
18
+ expect(s).toBeDefined()
19
+ expect(s.breakpoints).toBeDefined()
19
20
  })
20
21
  })
21
22
 
22
23
  describe('breakpoints', () => {
23
24
  it('Should have correct breakpoint definitions', async () => {
24
- await usingAsync(new Injector(), async (i) => {
25
- const s = i.getInstance(ScreenService)
25
+ await usingAsync(createInjector(), async (i) => {
26
+ const s = i.get(ScreenService)
26
27
 
27
28
  expect(s.breakpoints.xs.minSize).toBe(0)
28
29
  expect(s.breakpoints.sm.minSize).toBe(600)
@@ -35,8 +36,8 @@ describe('ScreenService', () => {
35
36
 
36
37
  describe('screenSize.atLeast', () => {
37
38
  it('Should have observable for each screen size', async () => {
38
- await usingAsync(new Injector(), async (i) => {
39
- const s = i.getInstance(ScreenService)
39
+ await usingAsync(createInjector(), async (i) => {
40
+ const s = i.get(ScreenService)
40
41
 
41
42
  for (const size of ScreenSizes) {
42
43
  expect(s.screenSize.atLeast[size]).toBeDefined()
@@ -46,16 +47,16 @@ describe('ScreenService', () => {
46
47
  })
47
48
 
48
49
  it('Should return true for xs on any screen size', async () => {
49
- await usingAsync(new Injector(), async (i) => {
50
- const s = i.getInstance(ScreenService)
50
+ await usingAsync(createInjector(), async (i) => {
51
+ const s = i.get(ScreenService)
51
52
  // xs has minSize 0, so it should always be true
52
53
  expect(s.screenSize.atLeast.xs.getValue()).toBe(true)
53
54
  })
54
55
  })
55
56
 
56
57
  it('Should update screenSize observables on window resize', async () => {
57
- await usingAsync(new Injector(), async (i) => {
58
- const s = i.getInstance(ScreenService)
58
+ await usingAsync(createInjector(), async (i) => {
59
+ const s = i.get(ScreenService)
59
60
 
60
61
  // Mock window.innerWidth to simulate a large screen
61
62
  vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1920)
@@ -86,8 +87,8 @@ describe('ScreenService', () => {
86
87
 
87
88
  describe('orientation', () => {
88
89
  it('Should have an orientation observable', async () => {
89
- await usingAsync(new Injector(), async (i) => {
90
- const s = i.getInstance(ScreenService)
90
+ await usingAsync(createInjector(), async (i) => {
91
+ const s = i.get(ScreenService)
91
92
  const orientation = s.orientation.getValue()
92
93
  expect(['landscape', 'portrait']).toContain(orientation)
93
94
  })
@@ -98,10 +99,10 @@ describe('ScreenService', () => {
98
99
  const matchMediaMock = vi.fn()
99
100
  window.matchMedia = matchMediaMock
100
101
 
101
- await usingAsync(new Injector(), async (i) => {
102
+ await usingAsync(createInjector(), async (i) => {
102
103
  // Set initial orientation to landscape
103
104
  matchMediaMock.mockReturnValue({ matches: true } as MediaQueryList)
104
- const s = i.getInstance(ScreenService)
105
+ const s = i.get(ScreenService)
105
106
 
106
107
  // Verify initial landscape
107
108
  window.dispatchEvent(new Event('resize'))
@@ -119,8 +120,8 @@ describe('ScreenService', () => {
119
120
  it('Should remove resize event listener on dispose', async () => {
120
121
  const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
121
122
 
122
- await usingAsync(new Injector(), async (i) => {
123
- i.getInstance(ScreenService)
123
+ await usingAsync(createInjector(), async (i) => {
124
+ i.get(ScreenService)
124
125
  })
125
126
 
126
127
  expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function))
@@ -1,4 +1,5 @@
1
- import { Injectable } from '@furystack/inject'
1
+ import type { Token } from '@furystack/inject'
2
+ import { defineService } from '@furystack/inject'
2
3
  import { ObservableValue } from '@furystack/utils'
3
4
 
4
5
  /**
@@ -19,134 +20,88 @@ export type Breakpoint = { name: ScreenSize; minSize: number; maxSize?: number }
19
20
  /**
20
21
  * Service for detecting and subscribing to screen size and orientation changes.
21
22
  *
22
- * This service provides reactive observables for responsive design decisions.
23
23
  * Use `screenSize.atLeast[size]` to check if the viewport is at least a certain size.
24
24
  *
25
25
  * **Breakpoint Thresholds:**
26
- * - `xs`: 0px+ (all sizes)
27
- * - `sm`: 600px+ (small tablets and up)
28
- * - `md`: 960px+ (tablets and up)
29
- * - `lg`: 1280px+ (desktops and up)
30
- * - `xl`: 1920px+ (large desktops)
26
+ * - `xs`: 0px+
27
+ * - `sm`: 600px+
28
+ * - `md`: 960px+
29
+ * - `lg`: 1280px+
30
+ * - `xl`: 1920px+
31
31
  *
32
32
  * @example
33
33
  * ```typescript
34
- * const screenService = injector.getInstance(ScreenService);
34
+ * const screenService = injector.get(ScreenService)
35
35
  *
36
- * // Check if screen is at least medium size
37
36
  * if (screenService.screenSize.atLeast.md.getValue()) {
38
- * // Show desktop layout
37
+ * // Desktop layout
39
38
  * }
40
39
  *
41
- * // Subscribe to size changes for responsive behavior
42
40
  * screenService.screenSize.atLeast.md.subscribe((isAtLeastMd) => {
43
- * if (isAtLeastMd) {
44
- * console.log('Desktop or tablet view');
45
- * } else {
46
- * console.log('Mobile view');
47
- * }
48
- * });
49
- *
50
- * // Get current breakpoint by checking from largest to smallest
51
- * const getCurrentBreakpoint = (): ScreenSize => {
52
- * if (screenService.screenSize.atLeast.xl.getValue()) return 'xl';
53
- * if (screenService.screenSize.atLeast.lg.getValue()) return 'lg';
54
- * if (screenService.screenSize.atLeast.md.getValue()) return 'md';
55
- * if (screenService.screenSize.atLeast.sm.getValue()) return 'sm';
56
- * return 'xs';
57
- * };
58
- *
59
- * // Subscribe to orientation changes
60
- * screenService.orientation.subscribe((orientation) => {
61
- * console.log(`Screen is now in ${orientation} mode`);
62
- * });
41
+ * // react to breakpoint changes
42
+ * })
63
43
  * ```
64
44
  */
65
- @Injectable({ lifetime: 'singleton' })
66
- export class ScreenService implements Disposable {
67
- private getOrientation = () => (window.matchMedia?.('(orientation:landscape').matches ? 'landscape' : 'portrait')
68
-
69
- /**
70
- * The definitions of the breakpoint thresholds in pixels.
71
- * Each breakpoint represents the minimum width for that size category.
72
- */
73
- public readonly breakpoints: { [K in ScreenSize]: { minSize: number } } = {
74
- xl: { minSize: 1920 },
75
- lg: { minSize: 1280 },
76
- md: { minSize: 960 },
77
- sm: { minSize: 600 },
78
- xs: { minSize: 0 },
45
+ export interface ScreenService {
46
+ readonly breakpoints: { [K in ScreenSize]: { minSize: number } }
47
+ readonly screenSize: {
48
+ atLeast: { [K in ScreenSize]: ObservableValue<boolean> }
79
49
  }
50
+ readonly orientation: ObservableValue<'landscape' | 'portrait'>
51
+ }
80
52
 
81
- /**
82
- * Cleans up event listeners when the service is disposed.
83
- */
84
- public [Symbol.dispose]() {
85
- window.removeEventListener('resize', this.onResizeListener)
86
- this.orientation[Symbol.dispose]()
87
- Object.values(this.screenSize.atLeast).forEach((observable) => {
88
- observable[Symbol.dispose]()
89
- })
90
- }
53
+ export const ScreenService: Token<ScreenService, 'singleton'> = defineService({
54
+ name: '@furystack/shades/ScreenService',
55
+ lifetime: 'singleton',
56
+ factory: ({ onDispose }) => {
57
+ const getOrientation = (): 'landscape' | 'portrait' =>
58
+ window.matchMedia?.('(orientation:landscape').matches ? 'landscape' : 'portrait'
91
59
 
92
- /**
93
- * Observable values for checking if the screen is at least a certain size.
94
- *
95
- * Each observable emits `true` when the viewport width is >= the breakpoint threshold,
96
- * and `false` otherwise. Values update automatically on window resize.
97
- *
98
- * @example
99
- * ```typescript
100
- * // Hide sidebar on small screens
101
- * screenService.screenSize.atLeast.md.subscribe((isAtLeastMd) => {
102
- * sidebarVisible.setValue(isAtLeastMd);
103
- * });
104
- * ```
105
- */
106
- public readonly screenSize: {
107
- atLeast: { [K in ScreenSize]: ObservableValue<boolean> }
108
- } = {
109
- atLeast: {
110
- xs: new ObservableValue<boolean>(this.screenSizeAtLeast('xs')),
111
- sm: new ObservableValue<boolean>(this.screenSizeAtLeast('sm')),
112
- md: new ObservableValue<boolean>(this.screenSizeAtLeast('md')),
113
- lg: new ObservableValue<boolean>(this.screenSizeAtLeast('lg')),
114
- xl: new ObservableValue<boolean>(this.screenSizeAtLeast('xl')),
115
- },
116
- }
60
+ const breakpoints: { [K in ScreenSize]: { minSize: number } } = {
61
+ xl: { minSize: 1920 },
62
+ lg: { minSize: 1280 },
63
+ md: { minSize: 960 },
64
+ sm: { minSize: 600 },
65
+ xs: { minSize: 0 },
66
+ }
117
67
 
118
- private screenSizeAtLeast(size: ScreenSize) {
119
- return window.innerWidth >= this.breakpoints[size].minSize
120
- }
68
+ const screenSizeAtLeast = (size: ScreenSize): boolean => window.innerWidth >= breakpoints[size].minSize
121
69
 
122
- /**
123
- * Observable value for tracking the screen orientation.
124
- * Emits 'landscape' or 'portrait' based on the current viewport dimensions.
125
- *
126
- * @example
127
- * ```typescript
128
- * screenService.orientation.subscribe((orientation) => {
129
- * if (orientation === 'landscape') {
130
- * // Adjust layout for landscape mode
131
- * }
132
- * });
133
- * ```
134
- */
135
- public orientation = new ObservableValue<'landscape' | 'portrait'>(this.getOrientation())
70
+ const atLeast: { [K in ScreenSize]: ObservableValue<boolean> } = {
71
+ xs: new ObservableValue<boolean>(screenSizeAtLeast('xs')),
72
+ sm: new ObservableValue<boolean>(screenSizeAtLeast('sm')),
73
+ md: new ObservableValue<boolean>(screenSizeAtLeast('md')),
74
+ lg: new ObservableValue<boolean>(screenSizeAtLeast('lg')),
75
+ xl: new ObservableValue<boolean>(screenSizeAtLeast('xl')),
76
+ }
136
77
 
137
- private onResizeListener = () => {
138
- this.refreshValues()
139
- }
78
+ const orientation = new ObservableValue<'landscape' | 'portrait'>(getOrientation())
79
+
80
+ const refreshValues = (): void => {
81
+ orientation.setValue(getOrientation())
82
+ for (const size of ScreenSizes) {
83
+ atLeast[size].setValue(screenSizeAtLeast(size))
84
+ }
85
+ }
86
+
87
+ const onResizeListener = (): void => {
88
+ refreshValues()
89
+ }
140
90
 
141
- private refreshValues() {
142
- this.orientation.setValue(this.getOrientation())
143
- ScreenSizes.forEach((size) => {
144
- this.screenSize.atLeast[size].setValue(this.screenSizeAtLeast(size))
91
+ window.addEventListener('resize', onResizeListener)
92
+ refreshValues()
93
+
94
+ onDispose(() => {
95
+ window.removeEventListener('resize', onResizeListener)
96
+ // eslint-disable-next-line furystack/prefer-using-wrapper -- Disposal is deferred to the injector's onDispose hook.
97
+ orientation[Symbol.dispose]()
98
+ Object.values(atLeast).forEach((observable) => observable[Symbol.dispose]())
145
99
  })
146
- }
147
100
 
148
- constructor() {
149
- window.addEventListener('resize', this.onResizeListener)
150
- this.refreshValues()
151
- }
152
- }
101
+ return {
102
+ breakpoints,
103
+ screenSize: { atLeast },
104
+ orientation,
105
+ }
106
+ },
107
+ })