@furystack/shades-common-components 13.0.1 → 13.2.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 (150) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/esm/components/app-bar.d.ts.map +1 -1
  3. package/esm/components/app-bar.js +12 -3
  4. package/esm/components/app-bar.js.map +1 -1
  5. package/esm/components/avatar.d.ts.map +1 -1
  6. package/esm/components/avatar.js +3 -5
  7. package/esm/components/avatar.js.map +1 -1
  8. package/esm/components/cache-view.d.ts +19 -27
  9. package/esm/components/cache-view.d.ts.map +1 -1
  10. package/esm/components/cache-view.js +2 -20
  11. package/esm/components/cache-view.js.map +1 -1
  12. package/esm/components/cache-view.spec.js +44 -0
  13. package/esm/components/cache-view.spec.js.map +1 -1
  14. package/esm/components/command-palette/command-palette-input.d.ts +1 -2
  15. package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
  16. package/esm/components/command-palette/command-palette-input.js +14 -36
  17. package/esm/components/command-palette/command-palette-input.js.map +1 -1
  18. package/esm/components/command-palette/command-palette-input.spec.js +14 -116
  19. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -1
  20. package/esm/components/command-palette/index.d.ts.map +1 -1
  21. package/esm/components/command-palette/index.js +3 -0
  22. package/esm/components/command-palette/index.js.map +1 -1
  23. package/esm/components/drawer/index.d.ts.map +1 -1
  24. package/esm/components/drawer/index.js +4 -0
  25. package/esm/components/drawer/index.js.map +1 -1
  26. package/esm/components/drawer/index.spec.js +47 -0
  27. package/esm/components/drawer/index.spec.js.map +1 -1
  28. package/esm/components/noty-list.d.ts.map +1 -1
  29. package/esm/components/noty-list.js +1 -3
  30. package/esm/components/noty-list.js.map +1 -1
  31. package/esm/services/css-variable-theme.d.ts +1 -1
  32. package/esm/services/css-variable-theme.d.ts.map +1 -1
  33. package/esm/services/css-variable-theme.js +5 -5
  34. package/esm/services/css-variable-theme.js.map +1 -1
  35. package/esm/services/css-variable-theme.spec.js +29 -1
  36. package/esm/services/css-variable-theme.spec.js.map +1 -1
  37. package/esm/services/layout-service.d.ts +8 -0
  38. package/esm/services/layout-service.d.ts.map +1 -1
  39. package/esm/services/layout-service.js +16 -0
  40. package/esm/services/layout-service.js.map +1 -1
  41. package/esm/services/layout-service.spec.js +55 -0
  42. package/esm/services/layout-service.spec.js.map +1 -1
  43. package/esm/services/theme-provider-service.d.ts +11 -10
  44. package/esm/services/theme-provider-service.d.ts.map +1 -1
  45. package/esm/services/theme-provider-service.js +3 -2
  46. package/esm/services/theme-provider-service.js.map +1 -1
  47. package/esm/services/theme-provider-service.spec.js +35 -1
  48. package/esm/services/theme-provider-service.spec.js.map +1 -1
  49. package/esm/themes/architect-theme.d.ts +1 -0
  50. package/esm/themes/architect-theme.d.ts.map +1 -1
  51. package/esm/themes/architect-theme.js +1 -0
  52. package/esm/themes/architect-theme.js.map +1 -1
  53. package/esm/themes/auditore-theme.d.ts +1 -0
  54. package/esm/themes/auditore-theme.d.ts.map +1 -1
  55. package/esm/themes/auditore-theme.js +1 -0
  56. package/esm/themes/auditore-theme.js.map +1 -1
  57. package/esm/themes/black-mesa-theme.d.ts +1 -0
  58. package/esm/themes/black-mesa-theme.d.ts.map +1 -1
  59. package/esm/themes/black-mesa-theme.js +1 -0
  60. package/esm/themes/black-mesa-theme.js.map +1 -1
  61. package/esm/themes/default-dark-theme.d.ts +1 -0
  62. package/esm/themes/default-dark-theme.d.ts.map +1 -1
  63. package/esm/themes/default-dark-theme.js +1 -0
  64. package/esm/themes/default-dark-theme.js.map +1 -1
  65. package/esm/themes/default-light-theme.d.ts +1 -0
  66. package/esm/themes/default-light-theme.d.ts.map +1 -1
  67. package/esm/themes/default-light-theme.js +1 -0
  68. package/esm/themes/default-light-theme.js.map +1 -1
  69. package/esm/themes/dragonborn-theme.d.ts +1 -0
  70. package/esm/themes/dragonborn-theme.d.ts.map +1 -1
  71. package/esm/themes/dragonborn-theme.js +1 -0
  72. package/esm/themes/dragonborn-theme.js.map +1 -1
  73. package/esm/themes/hawkins-theme.d.ts +1 -0
  74. package/esm/themes/hawkins-theme.d.ts.map +1 -1
  75. package/esm/themes/hawkins-theme.js +1 -0
  76. package/esm/themes/hawkins-theme.js.map +1 -1
  77. package/esm/themes/jedi-theme.d.ts +1 -0
  78. package/esm/themes/jedi-theme.d.ts.map +1 -1
  79. package/esm/themes/jedi-theme.js +1 -0
  80. package/esm/themes/jedi-theme.js.map +1 -1
  81. package/esm/themes/neon-runner-theme.d.ts +1 -0
  82. package/esm/themes/neon-runner-theme.d.ts.map +1 -1
  83. package/esm/themes/neon-runner-theme.js +1 -0
  84. package/esm/themes/neon-runner-theme.js.map +1 -1
  85. package/esm/themes/plumber-theme.d.ts +1 -0
  86. package/esm/themes/plumber-theme.d.ts.map +1 -1
  87. package/esm/themes/plumber-theme.js +1 -0
  88. package/esm/themes/plumber-theme.js.map +1 -1
  89. package/esm/themes/replicant-theme.d.ts +1 -0
  90. package/esm/themes/replicant-theme.d.ts.map +1 -1
  91. package/esm/themes/replicant-theme.js +1 -0
  92. package/esm/themes/replicant-theme.js.map +1 -1
  93. package/esm/themes/sandworm-theme.d.ts +1 -0
  94. package/esm/themes/sandworm-theme.d.ts.map +1 -1
  95. package/esm/themes/sandworm-theme.js +1 -0
  96. package/esm/themes/sandworm-theme.js.map +1 -1
  97. package/esm/themes/shadow-broker-theme.d.ts +1 -0
  98. package/esm/themes/shadow-broker-theme.d.ts.map +1 -1
  99. package/esm/themes/shadow-broker-theme.js +1 -0
  100. package/esm/themes/shadow-broker-theme.js.map +1 -1
  101. package/esm/themes/sith-theme.d.ts +1 -0
  102. package/esm/themes/sith-theme.d.ts.map +1 -1
  103. package/esm/themes/sith-theme.js +1 -0
  104. package/esm/themes/sith-theme.js.map +1 -1
  105. package/esm/themes/vault-dweller-theme.d.ts +1 -0
  106. package/esm/themes/vault-dweller-theme.d.ts.map +1 -1
  107. package/esm/themes/vault-dweller-theme.js +1 -0
  108. package/esm/themes/vault-dweller-theme.js.map +1 -1
  109. package/esm/themes/wild-hunt-theme.d.ts +1 -0
  110. package/esm/themes/wild-hunt-theme.d.ts.map +1 -1
  111. package/esm/themes/wild-hunt-theme.js +1 -0
  112. package/esm/themes/wild-hunt-theme.js.map +1 -1
  113. package/esm/themes/xenomorph-theme.d.ts +1 -0
  114. package/esm/themes/xenomorph-theme.d.ts.map +1 -1
  115. package/esm/themes/xenomorph-theme.js +1 -0
  116. package/esm/themes/xenomorph-theme.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/components/app-bar.tsx +12 -3
  119. package/src/components/avatar.tsx +20 -5
  120. package/src/components/cache-view.spec.tsx +63 -0
  121. package/src/components/cache-view.tsx +41 -9
  122. package/src/components/command-palette/command-palette-input.spec.tsx +14 -156
  123. package/src/components/command-palette/command-palette-input.tsx +13 -45
  124. package/src/components/command-palette/index.tsx +4 -0
  125. package/src/components/drawer/index.spec.tsx +64 -0
  126. package/src/components/drawer/index.tsx +5 -0
  127. package/src/components/noty-list.tsx +1 -3
  128. package/src/services/css-variable-theme.spec.ts +43 -1
  129. package/src/services/css-variable-theme.ts +5 -5
  130. package/src/services/layout-service.spec.ts +74 -0
  131. package/src/services/layout-service.ts +18 -0
  132. package/src/services/theme-provider-service.spec.ts +49 -1
  133. package/src/services/theme-provider-service.ts +12 -11
  134. package/src/themes/architect-theme.ts +1 -0
  135. package/src/themes/auditore-theme.ts +1 -0
  136. package/src/themes/black-mesa-theme.ts +1 -0
  137. package/src/themes/default-dark-theme.ts +1 -0
  138. package/src/themes/default-light-theme.ts +1 -0
  139. package/src/themes/dragonborn-theme.ts +1 -0
  140. package/src/themes/hawkins-theme.ts +1 -0
  141. package/src/themes/jedi-theme.ts +1 -0
  142. package/src/themes/neon-runner-theme.ts +1 -0
  143. package/src/themes/plumber-theme.ts +1 -0
  144. package/src/themes/replicant-theme.ts +1 -0
  145. package/src/themes/sandworm-theme.ts +1 -0
  146. package/src/themes/shadow-broker-theme.ts +1 -0
  147. package/src/themes/sith-theme.ts +1 -0
  148. package/src/themes/vault-dweller-theme.ts +1 -0
  149. package/src/themes/wild-hunt-theme.ts +1 -0
  150. package/src/themes/xenomorph-theme.ts +1 -0
@@ -718,6 +718,70 @@ describe('Drawer component', () => {
718
718
  })
719
719
  })
720
720
 
721
+ describe('cleanup on disposal', () => {
722
+ it('should call removeDrawer on LayoutService when the component is removed from DOM', async () => {
723
+ await usingAsync(new Injector(), async (injector) => {
724
+ const layoutService = new LayoutService(createMockElement())
725
+ injector.setExplicitInstance(layoutService, LayoutService)
726
+ const rootElement = document.getElementById('root') as HTMLDivElement
727
+
728
+ initializeShadeRoot({
729
+ injector,
730
+ rootElement,
731
+ jsxElement: (
732
+ <Drawer position="left" variant="collapsible">
733
+ <div>Drawer</div>
734
+ </Drawer>
735
+ ),
736
+ })
737
+
738
+ await flushUpdates()
739
+ expect(layoutService.drawerState.getValue().left).toBeDefined()
740
+
741
+ const removeDrawerSpy = vi.spyOn(layoutService, 'removeDrawer')
742
+ const drawer = document.querySelector('shade-drawer') as HTMLElement
743
+ drawer.remove()
744
+ await flushUpdates()
745
+ await new Promise((resolve) => setTimeout(resolve, 0))
746
+
747
+ expect(removeDrawerSpy).toHaveBeenCalledWith('left')
748
+ })
749
+ })
750
+
751
+ it('should only clean up its own drawer position on disposal', async () => {
752
+ await usingAsync(new Injector(), async (injector) => {
753
+ const layoutService = new LayoutService(createMockElement())
754
+ injector.setExplicitInstance(layoutService, LayoutService)
755
+ const rootElement = document.getElementById('root') as HTMLDivElement
756
+
757
+ layoutService.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
758
+
759
+ initializeShadeRoot({
760
+ injector,
761
+ rootElement,
762
+ jsxElement: (
763
+ <Drawer position="right" variant="temporary">
764
+ <div>Right Drawer</div>
765
+ </Drawer>
766
+ ),
767
+ })
768
+
769
+ await flushUpdates()
770
+ expect(layoutService.drawerState.getValue().right).toBeDefined()
771
+ expect(layoutService.drawerState.getValue().left).toBeDefined()
772
+
773
+ const removeDrawerSpy = vi.spyOn(layoutService, 'removeDrawer')
774
+ const drawer = document.querySelector('shade-drawer') as HTMLElement
775
+ drawer.remove()
776
+ await flushUpdates()
777
+ await new Promise((resolve) => setTimeout(resolve, 0))
778
+
779
+ expect(removeDrawerSpy).toHaveBeenCalledWith('right')
780
+ expect(removeDrawerSpy).not.toHaveBeenCalledWith('left')
781
+ })
782
+ })
783
+ })
784
+
721
785
  describe('preserving user interactions', () => {
722
786
  it('should not reset drawer state if already initialized', async () => {
723
787
  await usingAsync(new Injector(), async (injector) => {
@@ -143,6 +143,11 @@ export const Drawer = Shade<DrawerProps>({
143
143
  layoutService.setDrawerWidth(position, width)
144
144
  }
145
145
 
146
+ // Clean up drawer state from LayoutService when this component is disposed
147
+ useDisposable('drawer-cleanup', () => ({
148
+ [Symbol.dispose]: () => layoutService.removeDrawer(position),
149
+ }))
150
+
146
151
  // Subscribe to drawer state
147
152
  const [drawerState] = useObservable('drawerState', layoutService.drawerState)
148
153
  const isOpen = drawerState[position]?.open ?? false
@@ -1,6 +1,5 @@
1
1
  import { createComponent, Shade } from '@furystack/shades'
2
2
  import { cssVariableTheme } from '../services/css-variable-theme.js'
3
- import { getTextColor } from '../services/get-text-color.js'
4
3
  import type { NotyModel } from '../services/noty-service.js'
5
4
  import { NotyService } from '../services/noty-service.js'
6
5
  import { ThemeProviderService } from '../services/theme-provider-service.js'
@@ -97,7 +96,6 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
97
96
 
98
97
  const themeProvider = injector.getInstance(ThemeProviderService)
99
98
  const colors = themeProvider.theme.palette[props.model.type]
100
- const textColor = getTextColor(colors.main)
101
99
 
102
100
  const removeSelf = async () => {
103
101
  const hostEl = wrapperRef.current?.closest('shade-noty') as HTMLElement | null
@@ -123,7 +121,7 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
123
121
 
124
122
  useHostProps({
125
123
  'data-noty-type': props.model.type,
126
- style: { '--noty-bg': colors.main, '--noty-text': textColor },
124
+ style: { '--noty-bg': colors.main, '--noty-text': colors.mainContrast },
127
125
  })
128
126
 
129
127
  return (
@@ -67,7 +67,7 @@ describe('css-variable-theme', () => {
67
67
  expect(cssVariableTheme.typography.fontWeight.bold).toBe('var(--shades-theme-typography-font-weight-bold)')
68
68
  expect(cssVariableTheme.typography.lineHeight.tight).toBe('var(--shades-theme-typography-line-height-tight)')
69
69
  expect(cssVariableTheme.typography.lineHeight.normal).toBe('var(--shades-theme-typography-line-height-normal)')
70
- expect(cssVariableTheme.typography.textShadow).toBe('var(--shades-theme-typography-text-shadow, none)')
70
+ expect(cssVariableTheme.typography.textShadow).toBe('var(--shades-theme-typography-text-shadow)')
71
71
  })
72
72
 
73
73
  it('should have transition properties with CSS variable references', () => {
@@ -446,6 +446,48 @@ describe('css-variable-theme', () => {
446
446
  expect(root.style.getPropertyValue('--shades-theme-spacing-md')).toBe('16px')
447
447
  expect(root.style.getPropertyValue('--shades-theme-spacing-xl')).toBe('32px')
448
448
  })
449
+
450
+ it('should set CSS variables on a custom root element instead of :root', () => {
451
+ const customRoot = document.createElement('div')
452
+ document.body.appendChild(customRoot)
453
+
454
+ useThemeCssVariables(
455
+ {
456
+ text: { primary: '#ff0000' },
457
+ divider: '#00ff00',
458
+ },
459
+ customRoot,
460
+ )
461
+
462
+ expect(customRoot.style.getPropertyValue('--shades-theme-text-primary')).toBe('#ff0000')
463
+ expect(customRoot.style.getPropertyValue('--shades-theme-divider')).toBe('#00ff00')
464
+ expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('')
465
+
466
+ customRoot.remove()
467
+ })
468
+
469
+ it('should scope nested palette variables to custom root element', () => {
470
+ const customRoot = document.createElement('div')
471
+ document.body.appendChild(customRoot)
472
+
473
+ useThemeCssVariables(
474
+ {
475
+ palette: {
476
+ primary: {
477
+ main: '#1976d2',
478
+ mainContrast: '#ffffff',
479
+ },
480
+ },
481
+ },
482
+ customRoot,
483
+ )
484
+
485
+ expect(customRoot.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('#1976d2')
486
+ expect(customRoot.style.getPropertyValue('--shades-theme-palette-primary-main-contrast')).toBe('#ffffff')
487
+ expect(root.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('')
488
+
489
+ customRoot.remove()
490
+ })
449
491
  })
450
492
 
451
493
  describe('buildTransition', () => {
@@ -17,7 +17,7 @@ export const cssVariableTheme = {
17
17
  background: {
18
18
  default: 'var(--shades-theme-background-default)',
19
19
  paper: 'var(--shades-theme-background-paper)',
20
- paperImage: 'var(--shades-theme-background-paper-image, none)',
20
+ paperImage: 'var(--shades-theme-background-paper-image)',
21
21
  },
22
22
  palette: {
23
23
  primary: {
@@ -87,7 +87,7 @@ export const cssVariableTheme = {
87
87
  lg: 'var(--shades-theme-shape-border-radius-lg)',
88
88
  full: 'var(--shades-theme-shape-border-radius-full)',
89
89
  },
90
- borderWidth: 'var(--shades-theme-shape-border-width, 0px)',
90
+ borderWidth: 'var(--shades-theme-shape-border-width)',
91
91
  },
92
92
  shadows: {
93
93
  none: 'var(--shades-theme-shadows-none)',
@@ -127,7 +127,7 @@ export const cssVariableTheme = {
127
127
  wider: 'var(--shades-theme-typography-letter-spacing-wider)',
128
128
  widest: 'var(--shades-theme-typography-letter-spacing-widest)',
129
129
  },
130
- textShadow: 'var(--shades-theme-typography-text-shadow, none)',
130
+ textShadow: 'var(--shades-theme-typography-text-shadow)',
131
131
  },
132
132
  transitions: {
133
133
  duration: {
@@ -222,8 +222,8 @@ const assignValue = <T extends object>(
222
222
  }
223
223
  })
224
224
  }
225
- export const useThemeCssVariables = (theme: DeepPartial<Theme>) => {
226
- const root = document.querySelector(':root') as HTMLElement
225
+ export const useThemeCssVariables = (theme: DeepPartial<Theme>, root?: HTMLElement) => {
226
+ root ??= document.querySelector(':root') as HTMLElement
227
227
  assignValue(cssVariableTheme, theme, root)
228
228
 
229
229
  if (window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) {
@@ -221,6 +221,15 @@ describe('LayoutService', () => {
221
221
  })
222
222
  })
223
223
 
224
+ it('should overwrite existing drawer config', () => {
225
+ using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
226
+ service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
227
+ service.initDrawer('left', { open: false, width: '300px', variant: 'permanent' })
228
+
229
+ expect(service.drawerState.getValue().left).toEqual({ open: false, width: '300px', variant: 'permanent' })
230
+ })
231
+ })
232
+
224
233
  it('should initialize drawer with all variant types', () => {
225
234
  using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
226
235
  service.initDrawer('left', { open: true, width: '240px', variant: 'permanent' })
@@ -234,6 +243,71 @@ describe('LayoutService', () => {
234
243
  })
235
244
  })
236
245
  })
246
+
247
+ describe('removeDrawer', () => {
248
+ it('should remove left drawer state', () => {
249
+ using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
250
+ service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
251
+
252
+ service.removeDrawer('left')
253
+
254
+ expect(service.drawerState.getValue().left).toBeUndefined()
255
+ })
256
+ })
257
+
258
+ it('should remove right drawer state', () => {
259
+ using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
260
+ service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })
261
+
262
+ service.removeDrawer('right')
263
+
264
+ expect(service.drawerState.getValue().right).toBeUndefined()
265
+ })
266
+ })
267
+
268
+ it('should not affect the other drawer when removing one', () => {
269
+ using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
270
+ service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
271
+ service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })
272
+
273
+ service.removeDrawer('left')
274
+
275
+ expect(service.drawerState.getValue().left).toBeUndefined()
276
+ expect(service.drawerState.getValue().right).toEqual({ open: true, width: '200px', variant: 'temporary' })
277
+ })
278
+ })
279
+
280
+ it('should be a no-op if the drawer does not exist', () => {
281
+ using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
282
+ const stateBefore = service.drawerState.getValue()
283
+
284
+ service.removeDrawer('left')
285
+
286
+ expect(service.drawerState.getValue()).toEqual(stateBefore)
287
+ })
288
+ })
289
+
290
+ it('should be a no-op if the service is already disposed', () => {
291
+ const service = new LayoutService(mockElement as unknown as HTMLElement)
292
+ service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
293
+ service[Symbol.dispose]()
294
+
295
+ expect(() => service.removeDrawer('left')).not.toThrow()
296
+ })
297
+
298
+ it('should reset CSS variables after removing a drawer', () => {
299
+ using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
300
+ service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
301
+ mockSetProperty.mockClear()
302
+
303
+ service.removeDrawer('left')
304
+
305
+ expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-width', '0px')
306
+ expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-configured-width', '0px')
307
+ expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-left', '0px')
308
+ })
309
+ })
310
+ })
237
311
  })
238
312
 
239
313
  describe('CSS Variables', () => {
@@ -237,6 +237,24 @@ export class LayoutService implements Disposable {
237
237
  })
238
238
  }
239
239
 
240
+ /**
241
+ * Removes a drawer from the state, clearing its configuration.
242
+ * Use this when a Drawer component is unmounted to prevent stale state
243
+ * from affecting content margins.
244
+ *
245
+ * @param position - Which drawer to remove ('left' or 'right')
246
+ */
247
+ public removeDrawer(position: 'left' | 'right'): void {
248
+ if (this.drawerState.isDisposed) return
249
+
250
+ const currentState = this.drawerState.getValue()
251
+
252
+ if (currentState[position]) {
253
+ const { [position]: _, ...rest } = currentState
254
+ this.drawerState.setValue(rest)
255
+ }
256
+ }
257
+
240
258
  /**
241
259
  * Sets the top gap spacing between AppBar and content.
242
260
  *
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
2
  import { ThemeProviderService } from './theme-provider-service.js'
3
3
 
4
4
  describe('ThemeProviderService', () => {
@@ -19,4 +19,52 @@ describe('ThemeProviderService', () => {
19
19
  expect(service.theme.name).toBe('css-variable-theme')
20
20
  })
21
21
  })
22
+
23
+ describe('setAssignedTheme with custom root', () => {
24
+ let customRoot: HTMLElement
25
+
26
+ beforeEach(() => {
27
+ customRoot = document.createElement('div')
28
+ document.body.appendChild(customRoot)
29
+ })
30
+
31
+ afterEach(() => {
32
+ customRoot.remove()
33
+ document.documentElement.style.cssText = ''
34
+ })
35
+
36
+ it('should apply CSS variables to the provided root element', () => {
37
+ service.setAssignedTheme(
38
+ {
39
+ text: { primary: '#abcdef' },
40
+ palette: { primary: { main: '#123456' } },
41
+ },
42
+ customRoot,
43
+ )
44
+
45
+ expect(customRoot.style.getPropertyValue('--shades-theme-text-primary')).toBe('#abcdef')
46
+ expect(customRoot.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('#123456')
47
+ expect(document.documentElement.style.getPropertyValue('--shades-theme-text-primary')).toBe('')
48
+ })
49
+
50
+ it('should still update the stored theme when a custom root is provided', () => {
51
+ const theme = { text: { primary: '#111' } }
52
+ service.setAssignedTheme(theme, customRoot)
53
+
54
+ expect(service.getAssignedTheme()).toBe(theme)
55
+ })
56
+
57
+ it('should emit themeChanged when a custom root is provided', () => {
58
+ const theme = { text: { primary: '#222' } }
59
+ let emittedTheme: unknown
60
+
61
+ service.subscribe('themeChanged', (t) => {
62
+ emittedTheme = t
63
+ })
64
+
65
+ service.setAssignedTheme(theme, customRoot)
66
+
67
+ expect(emittedTheme).toBe(theme)
68
+ })
69
+ })
22
70
  })
@@ -85,8 +85,8 @@ export interface Background {
85
85
  default: Color
86
86
  /** Elevated surface background (cards, dialogs, etc.) */
87
87
  paper: Color
88
- /** Optional CSS background-image for paper surfaces (e.g. a tiled texture) */
89
- paperImage?: string
88
+ /** CSS background-image for paper surfaces (e.g. a tiled texture). Use 'none' for no image. */
89
+ paperImage: string
90
90
  }
91
91
 
92
92
  /**
@@ -131,8 +131,8 @@ export type BorderRadiusScale = {
131
131
  export type Shape = {
132
132
  /** Border radius scale */
133
133
  borderRadius: BorderRadiusScale
134
- /** Border width for surface components (paper, card, etc.). Defaults to 0. */
135
- borderWidth?: string
134
+ /** Border width for surface components (paper, card, etc.). Use '0px' for no border. */
135
+ borderWidth: string
136
136
  }
137
137
 
138
138
  /**
@@ -230,9 +230,9 @@ export type ThemeTypography = {
230
230
  /** Line height scale */
231
231
  lineHeight: LineHeightScale
232
232
  /** Letter spacing scale */
233
- letterSpacing?: LetterSpacingScale
234
- /** CSS text-shadow value applied globally to text */
235
- textShadow?: string
233
+ letterSpacing: LetterSpacingScale
234
+ /** CSS text-shadow value applied globally to text. Use 'none' for no shadow. */
235
+ textShadow: string
236
236
  }
237
237
 
238
238
  /**
@@ -358,9 +358,9 @@ export interface Theme {
358
358
  /** Spacing scale */
359
359
  spacing: Spacing
360
360
  /** Z-index stacking layers */
361
- zIndex?: ZIndex
361
+ zIndex: ZIndex
362
362
  /** Visual effect tokens (blur, backdrop) */
363
- effects?: Effects
363
+ effects: Effects
364
364
  }
365
365
 
366
366
  /**
@@ -382,10 +382,11 @@ export class ThemeProviderService extends EventHub<{ themeChanged: DeepPartial<T
382
382
  /**
383
383
  * Assigns a new theme, updates the CSS variables and emits a themeChanged event
384
384
  * @param theme The Theme instance
385
+ * @param root Optional HTML element to scope CSS variables to. Defaults to `:root`.
385
386
  */
386
- public setAssignedTheme(theme: DeepPartial<Theme>) {
387
+ public setAssignedTheme(theme: DeepPartial<Theme>, root?: HTMLElement) {
387
388
  this._assignedTheme = theme
388
- useThemeCssVariables(theme)
389
+ useThemeCssVariables(theme, root)
389
390
  this.emit('themeChanged', theme)
390
391
  }
391
392
  }
@@ -88,6 +88,7 @@ export const architectTheme = {
88
88
  wider: '1.5px',
89
89
  widest: '2.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -88,6 +88,7 @@ export const auditoreTheme = {
88
88
  wider: '1px',
89
89
  widest: '1.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -88,6 +88,7 @@ export const blackMesaTheme = {
88
88
  wider: '1.25px',
89
89
  widest: '2px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -83,6 +83,7 @@ export const defaultDarkTheme = {
83
83
  wider: '0.5px',
84
84
  widest: '1.5px',
85
85
  },
86
+ textShadow: 'none',
86
87
  },
87
88
  transitions: {
88
89
  duration: {
@@ -83,6 +83,7 @@ export const defaultLightTheme = {
83
83
  wider: '0.5px',
84
84
  widest: '1.5px',
85
85
  },
86
+ textShadow: 'none',
86
87
  },
87
88
  transitions: {
88
89
  duration: {
@@ -88,6 +88,7 @@ export const dragonbornTheme = {
88
88
  wider: '1px',
89
89
  widest: '1.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -88,6 +88,7 @@ export const hawkinsTheme = {
88
88
  wider: '1px',
89
89
  widest: '2px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -88,6 +88,7 @@ export const jediTheme = {
88
88
  wider: '0.75px',
89
89
  widest: '1.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -85,6 +85,7 @@ export const neonRunnerTheme = {
85
85
  wider: '1.5px',
86
86
  widest: '2.5px',
87
87
  },
88
+ textShadow: 'none',
88
89
  },
89
90
  transitions: {
90
91
  duration: {
@@ -86,6 +86,7 @@ export const plumberTheme = {
86
86
  wider: '1px',
87
87
  widest: '1.5px',
88
88
  },
89
+ textShadow: 'none',
89
90
  },
90
91
  transitions: {
91
92
  duration: {
@@ -88,6 +88,7 @@ export const replicantTheme = {
88
88
  wider: '2px',
89
89
  widest: '3px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -88,6 +88,7 @@ export const sandwormTheme = {
88
88
  wider: '1px',
89
89
  widest: '1.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -86,6 +86,7 @@ export const shadowBrokerTheme = {
86
86
  wider: '1.25px',
87
87
  widest: '2px',
88
88
  },
89
+ textShadow: 'none',
89
90
  },
90
91
  transitions: {
91
92
  duration: {
@@ -88,6 +88,7 @@ export const sithTheme = {
88
88
  wider: '1.5px',
89
89
  widest: '2.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -88,6 +88,7 @@ export const vaultDwellerTheme = {
88
88
  wider: '1.5px',
89
89
  widest: '2.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -88,6 +88,7 @@ export const wildHuntTheme = {
88
88
  wider: '1px',
89
89
  widest: '1.5px',
90
90
  },
91
+ textShadow: 'none',
91
92
  },
92
93
  transitions: {
93
94
  duration: {
@@ -86,6 +86,7 @@ export const xenomorphTheme = {
86
86
  wider: '1.25px',
87
87
  widest: '2px',
88
88
  },
89
+ textShadow: 'none',
89
90
  },
90
91
  transitions: {
91
92
  duration: {