@lukso/core 1.1.0-dev.5ea12c5 → 1.1.0-dev.c21633f

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 (72) hide show
  1. package/README.md +55 -0
  2. package/dist/chains/index.cjs +1 -0
  3. package/dist/chains/index.cjs.map +1 -1
  4. package/dist/chains/index.js +1 -0
  5. package/dist/chunk-CUDG6NPH.cjs +111 -0
  6. package/dist/chunk-CUDG6NPH.cjs.map +1 -0
  7. package/dist/chunk-DWXFDFMM.cjs +1 -0
  8. package/dist/chunk-DWXFDFMM.cjs.map +1 -0
  9. package/dist/chunk-EUXUH3YW.js +15 -0
  10. package/dist/chunk-GFLV5EJV.js +159 -0
  11. package/dist/chunk-GFLV5EJV.js.map +1 -0
  12. package/dist/chunk-JEE6C34P.js +1 -0
  13. package/dist/chunk-JEE6C34P.js.map +1 -0
  14. package/dist/chunk-LQIOVPBE.js +111 -0
  15. package/dist/chunk-LQIOVPBE.js.map +1 -0
  16. package/dist/chunk-QU6NUTY6.cjs +159 -0
  17. package/dist/chunk-QU6NUTY6.cjs.map +1 -0
  18. package/dist/chunk-ZBDE64SD.cjs +15 -0
  19. package/dist/chunk-ZBDE64SD.cjs.map +1 -0
  20. package/dist/config.cjs +1 -0
  21. package/dist/config.cjs.map +1 -1
  22. package/dist/config.js +1 -0
  23. package/dist/index.cjs +14 -5
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +2 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +13 -4
  28. package/dist/mixins/device.cjs +1 -0
  29. package/dist/mixins/device.cjs.map +1 -1
  30. package/dist/mixins/device.js +1 -0
  31. package/dist/mixins/index.cjs +7 -2
  32. package/dist/mixins/index.cjs.map +1 -1
  33. package/dist/mixins/index.d.cts +1 -0
  34. package/dist/mixins/index.d.ts +1 -0
  35. package/dist/mixins/index.js +7 -2
  36. package/dist/mixins/intl.cjs +1 -0
  37. package/dist/mixins/intl.cjs.map +1 -1
  38. package/dist/mixins/intl.js +1 -0
  39. package/dist/mixins/theme.cjs +8 -0
  40. package/dist/mixins/theme.cjs.map +1 -0
  41. package/dist/mixins/theme.d.cts +45 -0
  42. package/dist/mixins/theme.d.ts +45 -0
  43. package/dist/mixins/theme.js +8 -0
  44. package/dist/mixins/theme.js.map +1 -0
  45. package/dist/services/device.cjs +1 -0
  46. package/dist/services/device.cjs.map +1 -1
  47. package/dist/services/device.js +1 -0
  48. package/dist/services/index.cjs +1 -0
  49. package/dist/services/index.cjs.map +1 -1
  50. package/dist/services/index.js +1 -0
  51. package/dist/services/intl.cjs +1 -0
  52. package/dist/services/intl.cjs.map +1 -1
  53. package/dist/services/intl.js +1 -0
  54. package/dist/utils/index.cjs +7 -2
  55. package/dist/utils/index.cjs.map +1 -1
  56. package/dist/utils/index.d.cts +34 -1
  57. package/dist/utils/index.d.ts +34 -1
  58. package/dist/utils/index.js +6 -1
  59. package/package.json +7 -1
  60. package/src/mixins/__tests__/theme.spec.ts +478 -0
  61. package/src/mixins/index.ts +1 -0
  62. package/src/mixins/theme.ts +172 -0
  63. package/src/utils/index.ts +1 -0
  64. package/src/utils/url-resolver.ts +93 -0
  65. package/dist/chunk-AMRGSLR5.cjs +0 -1
  66. package/dist/chunk-AMRGSLR5.cjs.map +0 -1
  67. package/dist/chunk-DKEXQFNE.js +0 -1
  68. package/dist/chunk-DKXHVRHM.js +0 -84
  69. package/dist/chunk-DKXHVRHM.js.map +0 -1
  70. package/dist/chunk-MBIRTPNM.cjs +0 -84
  71. package/dist/chunk-MBIRTPNM.cjs.map +0 -1
  72. /package/dist/{chunk-DKEXQFNE.js.map → chunk-EUXUH3YW.js.map} +0 -0
@@ -5,6 +5,7 @@
5
5
 
6
6
 
7
7
  var _chunkRM42NG7Ecjs = require('../chunk-RM42NG7E.cjs');
8
+ require('../chunk-ZBDE64SD.cjs');
8
9
 
9
10
 
10
11
 
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/services/intl.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACF,8SAAC","file":"/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/services/intl.cjs"}
1
+ {"version":3,"sources":["/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/services/intl.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACF,8SAAC","file":"/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/services/intl.cjs"}
@@ -5,6 +5,7 @@ import {
5
5
  getIntlService,
6
6
  setIntlService
7
7
  } from "../chunk-4TNWG4ME.js";
8
+ import "../chunk-EUXUH3YW.js";
8
9
  export {
9
10
  clearIntlService,
10
11
  createIntlService,
@@ -2,10 +2,15 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkMBIRTPNMcjs = require('../chunk-MBIRTPNM.cjs');
6
5
 
7
6
 
7
+ var _chunkQU6NUTY6cjs = require('../chunk-QU6NUTY6.cjs');
8
+ require('../chunk-ZBDE64SD.cjs');
8
9
 
9
10
 
10
- exports.EXTENSION_STORE_LINKS = _chunkMBIRTPNMcjs.EXTENSION_STORE_LINKS; exports.browserInfo = _chunkMBIRTPNMcjs.browserInfo; exports.slug = _chunkMBIRTPNMcjs.slug;
11
+
12
+
13
+
14
+
15
+ exports.EXTENSION_STORE_LINKS = _chunkQU6NUTY6cjs.EXTENSION_STORE_LINKS; exports.UrlConverter = _chunkQU6NUTY6cjs.UrlConverter; exports.UrlResolver = _chunkQU6NUTY6cjs.UrlResolver; exports.browserInfo = _chunkQU6NUTY6cjs.browserInfo; exports.slug = _chunkQU6NUTY6cjs.slug;
11
16
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/utils/index.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,oKAAC","file":"/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/utils/index.cjs"}
1
+ {"version":3,"sources":["/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/utils/index.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACF,gRAAC","file":"/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/utils/index.cjs"}
@@ -29,4 +29,37 @@ declare const browserInfo: (deviceService: DeviceService) => BrowserInfo;
29
29
  */
30
30
  declare const slug: (value?: string) => string;
31
31
 
32
- export { type BrowserInfo, type BrowserName, EXTENSION_STORE_LINKS, browserInfo, slug };
32
+ declare class UrlConverter {
33
+ private destination;
34
+ /**
35
+ * It will relatively append pathname or hostname to the destination URL
36
+ *
37
+ * For example:
38
+ * destination=https://some.api.gateway/something/ipfs
39
+ * url=ipfs://QmSomeHash
40
+ * output=https://some.api.gateway/something/ipfs/QmSomeHash
41
+ *
42
+ * destination=https://some.api.gateway/something/ipfs
43
+ * url=https://something.com/somewhere
44
+ * output=https://some.api.gateway/something/ipfs/somewhere
45
+ *
46
+ * @param destination destination string | URL
47
+ */
48
+ constructor(destination: string | URL);
49
+ resolveUrl(url: string): string;
50
+ }
51
+ declare class UrlResolver {
52
+ private converters;
53
+ constructor(converters: Array<[string | RegExp, UrlConverter | string]>);
54
+ /**
55
+ * Resolves a URL to a gateway URL.
56
+ * Supports possible multiple converters transforming the URL
57
+ * in sequence until no converter matches.
58
+ *
59
+ * @param {string} url to resolve
60
+ * @returns {string} resolved url (if resolver is found, otherwise the parameter url is returned)
61
+ */
62
+ resolveUrl(url_: string): string;
63
+ }
64
+
65
+ export { type BrowserInfo, type BrowserName, EXTENSION_STORE_LINKS, UrlConverter, UrlResolver, browserInfo, slug };
@@ -29,4 +29,37 @@ declare const browserInfo: (deviceService: DeviceService) => BrowserInfo;
29
29
  */
30
30
  declare const slug: (value?: string) => string;
31
31
 
32
- export { type BrowserInfo, type BrowserName, EXTENSION_STORE_LINKS, browserInfo, slug };
32
+ declare class UrlConverter {
33
+ private destination;
34
+ /**
35
+ * It will relatively append pathname or hostname to the destination URL
36
+ *
37
+ * For example:
38
+ * destination=https://some.api.gateway/something/ipfs
39
+ * url=ipfs://QmSomeHash
40
+ * output=https://some.api.gateway/something/ipfs/QmSomeHash
41
+ *
42
+ * destination=https://some.api.gateway/something/ipfs
43
+ * url=https://something.com/somewhere
44
+ * output=https://some.api.gateway/something/ipfs/somewhere
45
+ *
46
+ * @param destination destination string | URL
47
+ */
48
+ constructor(destination: string | URL);
49
+ resolveUrl(url: string): string;
50
+ }
51
+ declare class UrlResolver {
52
+ private converters;
53
+ constructor(converters: Array<[string | RegExp, UrlConverter | string]>);
54
+ /**
55
+ * Resolves a URL to a gateway URL.
56
+ * Supports possible multiple converters transforming the URL
57
+ * in sequence until no converter matches.
58
+ *
59
+ * @param {string} url to resolve
60
+ * @returns {string} resolved url (if resolver is found, otherwise the parameter url is returned)
61
+ */
62
+ resolveUrl(url_: string): string;
63
+ }
64
+
65
+ export { type BrowserInfo, type BrowserName, EXTENSION_STORE_LINKS, UrlConverter, UrlResolver, browserInfo, slug };
@@ -1,10 +1,15 @@
1
1
  import {
2
2
  EXTENSION_STORE_LINKS,
3
+ UrlConverter,
4
+ UrlResolver,
3
5
  browserInfo,
4
6
  slug
5
- } from "../chunk-DKXHVRHM.js";
7
+ } from "../chunk-GFLV5EJV.js";
8
+ import "../chunk-EUXUH3YW.js";
6
9
  export {
7
10
  EXTENSION_STORE_LINKS,
11
+ UrlConverter,
12
+ UrlResolver,
8
13
  browserInfo,
9
14
  slug
10
15
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukso/core",
3
- "version": "1.1.0-dev.5ea12c5",
3
+ "version": "1.1.0-dev.c21633f",
4
4
  "description": "Core utilities, services, and mixins for LUKSO web components and applications",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -52,6 +52,11 @@
52
52
  "import": "./dist/mixins/intl.js",
53
53
  "require": "./dist/mixins/intl.cjs"
54
54
  },
55
+ "./mixins/theme": {
56
+ "types": "./dist/mixins/theme.d.ts",
57
+ "import": "./dist/mixins/theme.js",
58
+ "require": "./dist/mixins/theme.cjs"
59
+ },
55
60
  "./utils": {
56
61
  "types": "./dist/utils/index.d.ts",
57
62
  "import": "./dist/utils/index.js",
@@ -111,6 +116,7 @@
111
116
  "src/mixins/index.ts",
112
117
  "src/mixins/device.ts",
113
118
  "src/mixins/intl.ts",
119
+ "src/mixins/theme.ts",
114
120
  "src/utils/index.ts"
115
121
  ],
116
122
  "format": [
@@ -0,0 +1,478 @@
1
+ import { html, LitElement } from 'lit'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { withTheme } from '../theme'
4
+
5
+ describe('withTheme Mixin', () => {
6
+ let element: any
7
+ let TestComponent: any
8
+ let tagName: string
9
+
10
+ beforeEach(() => {
11
+ // Create a unique tag name for each test to avoid registration conflicts
12
+ tagName = `test-theme-${Math.random().toString(36).slice(2)}`
13
+
14
+ // Create a test component using the mixin
15
+ TestComponent = class extends withTheme(LitElement) {
16
+ render() {
17
+ return html`
18
+ <div class="test-content bg-neutral-100 dark:bg-neutral-10">
19
+ Theme: ${this.theme}
20
+ </div>
21
+ `
22
+ }
23
+ }
24
+
25
+ // Register the component before instantiation
26
+ if (!customElements.get(tagName)) {
27
+ customElements.define(tagName, TestComponent)
28
+ }
29
+
30
+ element = new TestComponent()
31
+ })
32
+
33
+ afterEach(() => {
34
+ if (element?.parentElement) {
35
+ element.remove()
36
+ }
37
+ })
38
+
39
+ describe('Initialization', () => {
40
+ it('should have default theme property set to "light"', () => {
41
+ expect(element.theme).toBe('light')
42
+ })
43
+
44
+ it('should have isDark property set to false by default', () => {
45
+ expect(element.isDark).toBe(false)
46
+ })
47
+
48
+ it('should have theme property reflected to attribute', async () => {
49
+ document.body.appendChild(element)
50
+ await element.updateComplete
51
+
52
+ expect(element.hasAttribute('theme')).toBe(true)
53
+ expect(element.getAttribute('theme')).toBe('light')
54
+ })
55
+ })
56
+
57
+ describe('Theme Root Creation', () => {
58
+ it('should create a theme root div', async () => {
59
+ document.body.appendChild(element)
60
+ await element.updateComplete
61
+
62
+ const shadowRoot = element.shadowRoot
63
+ expect(shadowRoot).toBeDefined()
64
+
65
+ const themeRoot = shadowRoot?.querySelector('[data-theme-root]')
66
+ expect(themeRoot).toBeDefined()
67
+ expect(themeRoot?.tagName).toBe('DIV')
68
+ })
69
+
70
+ it('should have data-theme-root attribute', async () => {
71
+ document.body.appendChild(element)
72
+ await element.updateComplete
73
+
74
+ const themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
75
+ expect(themeRoot?.hasAttribute('data-theme-root')).toBe(true)
76
+ })
77
+
78
+ it('should render content inside theme root', async () => {
79
+ document.body.appendChild(element)
80
+ await element.updateComplete
81
+
82
+ const themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
83
+ const content = themeRoot?.querySelector('.test-content')
84
+ expect(content).toBeDefined()
85
+ })
86
+ })
87
+
88
+ describe('Light Theme', () => {
89
+ it('should not have dark class when theme is light', async () => {
90
+ element.theme = 'light'
91
+ document.body.appendChild(element)
92
+ await element.updateComplete
93
+
94
+ const themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
95
+ expect(themeRoot?.classList.contains('dark')).toBe(false)
96
+ })
97
+
98
+ it('should set isDark to false when theme is light', async () => {
99
+ element.theme = 'light'
100
+ document.body.appendChild(element)
101
+ await element.updateComplete
102
+
103
+ expect(element.isDark).toBe(false)
104
+ })
105
+
106
+ it('should update attribute when theme is set to light', async () => {
107
+ element.theme = 'light'
108
+ document.body.appendChild(element)
109
+ await element.updateComplete
110
+
111
+ expect(element.getAttribute('theme')).toBe('light')
112
+ })
113
+ })
114
+
115
+ describe('Dark Theme', () => {
116
+ it('should have dark class when theme is dark', async () => {
117
+ element.theme = 'dark'
118
+ document.body.appendChild(element)
119
+ await element.updateComplete
120
+
121
+ const themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
122
+ expect(themeRoot?.classList.contains('dark')).toBe(true)
123
+ })
124
+
125
+ it('should set isDark to true when theme is dark', async () => {
126
+ element.theme = 'dark'
127
+ document.body.appendChild(element)
128
+ await element.updateComplete
129
+
130
+ expect(element.isDark).toBe(true)
131
+ })
132
+
133
+ it('should update attribute when theme is set to dark', async () => {
134
+ element.theme = 'dark'
135
+ document.body.appendChild(element)
136
+ await element.updateComplete
137
+
138
+ expect(element.getAttribute('theme')).toBe('dark')
139
+ })
140
+ })
141
+
142
+ describe('Auto Theme', () => {
143
+ it('should set theme to auto', () => {
144
+ element.theme = 'auto'
145
+ expect(element.theme).toBe('auto')
146
+ })
147
+
148
+ it('should detect system preference when theme is auto', async () => {
149
+ // Mock matchMedia to return dark mode
150
+ const matchMediaMock = vi.fn((query) => ({
151
+ matches: query === '(prefers-color-scheme: dark)',
152
+ media: query,
153
+ addEventListener: vi.fn(),
154
+ removeEventListener: vi.fn(),
155
+ }))
156
+ vi.stubGlobal('matchMedia', matchMediaMock)
157
+
158
+ element.theme = 'auto'
159
+ document.body.appendChild(element)
160
+ await element.updateComplete
161
+
162
+ expect(element.isDark).toBe(true)
163
+
164
+ vi.unstubAllGlobals()
165
+ })
166
+
167
+ it('should listen for system theme changes when theme is auto', async () => {
168
+ const addEventListenerSpy = vi.fn()
169
+ const matchMediaMock = vi.fn(() => ({
170
+ matches: false,
171
+ media: '(prefers-color-scheme: dark)',
172
+ addEventListener: addEventListenerSpy,
173
+ removeEventListener: vi.fn(),
174
+ }))
175
+ vi.stubGlobal('matchMedia', matchMediaMock)
176
+
177
+ element.theme = 'auto'
178
+ document.body.appendChild(element)
179
+ await element.updateComplete
180
+
181
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
182
+ 'change',
183
+ expect.any(Function)
184
+ )
185
+
186
+ vi.unstubAllGlobals()
187
+ })
188
+
189
+ it('should remove event listener when theme changes from auto', async () => {
190
+ const removeEventListenerSpy = vi.fn()
191
+ const addEventListenerSpy = vi.fn()
192
+ const matchMediaMock = vi.fn(() => ({
193
+ matches: false,
194
+ media: '(prefers-color-scheme: dark)',
195
+ addEventListener: addEventListenerSpy,
196
+ removeEventListener: removeEventListenerSpy,
197
+ }))
198
+ vi.stubGlobal('matchMedia', matchMediaMock)
199
+
200
+ element.theme = 'auto'
201
+ document.body.appendChild(element)
202
+ await element.updateComplete
203
+
204
+ // Change theme from auto to light
205
+ element.theme = 'light'
206
+ await element.updateComplete
207
+
208
+ expect(removeEventListenerSpy).toHaveBeenCalled()
209
+
210
+ vi.unstubAllGlobals()
211
+ })
212
+ })
213
+
214
+ describe('Theme Switching', () => {
215
+ it('should switch from light to dark', async () => {
216
+ document.body.appendChild(element)
217
+ element.theme = 'light'
218
+ await element.updateComplete
219
+ await element.updateComplete // Wait for property change update
220
+
221
+ let themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
222
+ expect(themeRoot?.classList.contains('dark')).toBe(false)
223
+
224
+ element.theme = 'dark'
225
+ await element.updateComplete
226
+ await element.updateComplete // Wait for property change update
227
+
228
+ themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
229
+ expect(themeRoot?.classList.contains('dark')).toBe(true)
230
+ })
231
+
232
+ it('should switch from dark to light', async () => {
233
+ document.body.appendChild(element)
234
+ element.theme = 'dark'
235
+ await element.updateComplete
236
+ await element.updateComplete // Wait for property change update
237
+
238
+ let themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
239
+ expect(themeRoot?.classList.contains('dark')).toBe(true)
240
+
241
+ element.theme = 'light'
242
+ await element.updateComplete
243
+ await element.updateComplete // Wait for property change update
244
+
245
+ themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
246
+ expect(themeRoot?.classList.contains('dark')).toBe(false)
247
+ })
248
+
249
+ it('should update isDark when switching themes', async () => {
250
+ document.body.appendChild(element)
251
+ await element.updateComplete
252
+
253
+ element.theme = 'dark'
254
+ await element.updateComplete
255
+ expect(element.isDark).toBe(true)
256
+
257
+ element.theme = 'light'
258
+ await element.updateComplete
259
+ expect(element.isDark).toBe(false)
260
+ })
261
+ })
262
+
263
+ describe('Multiple Instances', () => {
264
+ it('should work with multiple component instances', async () => {
265
+ const element2 = new TestComponent()
266
+
267
+ element.theme = 'dark'
268
+ element2.theme = 'light'
269
+
270
+ document.body.appendChild(element)
271
+ document.body.appendChild(element2)
272
+ await element.updateComplete
273
+ await element2.updateComplete
274
+
275
+ expect(element.isDark).toBe(true)
276
+ expect(element2.isDark).toBe(false)
277
+
278
+ const themeRoot1 = element.shadowRoot?.querySelector('[data-theme-root]')
279
+ const themeRoot2 = element2.shadowRoot?.querySelector('[data-theme-root]')
280
+
281
+ expect(themeRoot1?.classList.contains('dark')).toBe(true)
282
+ expect(themeRoot2?.classList.contains('dark')).toBe(false)
283
+
284
+ element2.remove()
285
+ })
286
+
287
+ it('should not affect other instances when changing theme', async () => {
288
+ const element2 = new TestComponent()
289
+
290
+ document.body.appendChild(element)
291
+ document.body.appendChild(element2)
292
+ await element.updateComplete
293
+ await element2.updateComplete
294
+
295
+ element.theme = 'dark'
296
+ await element.updateComplete
297
+
298
+ expect(element.isDark).toBe(true)
299
+ expect(element2.isDark).toBe(false)
300
+
301
+ element2.remove()
302
+ })
303
+ })
304
+
305
+ describe('Lifecycle', () => {
306
+ it('should cleanup event listener on disconnect when theme is auto', async () => {
307
+ const removeEventListenerSpy = vi.fn()
308
+ const matchMediaMock = vi.fn(() => ({
309
+ matches: false,
310
+ media: '(prefers-color-scheme: dark)',
311
+ addEventListener: vi.fn(),
312
+ removeEventListener: removeEventListenerSpy,
313
+ }))
314
+ vi.stubGlobal('matchMedia', matchMediaMock)
315
+
316
+ element.theme = 'auto'
317
+ document.body.appendChild(element)
318
+ await element.updateComplete
319
+
320
+ element.remove()
321
+
322
+ expect(removeEventListenerSpy).toHaveBeenCalled()
323
+
324
+ vi.unstubAllGlobals()
325
+ })
326
+
327
+ it('should maintain theme property after disconnection', async () => {
328
+ element.theme = 'dark'
329
+ document.body.appendChild(element)
330
+ await element.updateComplete
331
+
332
+ const themeBefore = element.theme
333
+ const isDarkBefore = element.isDark
334
+
335
+ element.remove()
336
+
337
+ expect(element.theme).toBe(themeBefore)
338
+ expect(element.isDark).toBe(isDarkBefore)
339
+ })
340
+ })
341
+
342
+ describe('Mixin Composition', () => {
343
+ it('should work when composed with other mixins', async () => {
344
+ // Simple test mixin
345
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346
+ const withTestFeature = <T extends typeof LitElement>(Base: T): any => {
347
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
348
+ return class extends (Base as any) {
349
+ testProperty = 'test'
350
+ }
351
+ }
352
+
353
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
354
+ const ComposedComponent: any = class extends withTheme(
355
+ withTestFeature(LitElement)
356
+ ) {
357
+ render() {
358
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
359
+ return html`<div>${(this as any).testProperty}</div>`
360
+ }
361
+ }
362
+
363
+ const composedTagName = `test-composed-${Math.random().toString(36).slice(2)}`
364
+ customElements.define(composedTagName, ComposedComponent)
365
+
366
+ const composedElement = new ComposedComponent()
367
+ document.body.appendChild(composedElement)
368
+ await composedElement.updateComplete
369
+
370
+ // Should have both theme and test properties
371
+ expect(composedElement.theme).toBeDefined()
372
+ expect(composedElement.isDark).toBeDefined()
373
+ expect(composedElement.testProperty).toBe('test')
374
+
375
+ // Should have theme root
376
+ const themeRoot =
377
+ composedElement.shadowRoot?.querySelector('[data-theme-root]')
378
+ expect(themeRoot).toBeDefined()
379
+
380
+ composedElement.remove()
381
+ })
382
+ })
383
+
384
+ describe('System Preference Changes', () => {
385
+ it('should update isDark when system preference changes in auto mode', async () => {
386
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
387
+ const handlers: any[] = []
388
+ const matchMediaMock = vi.fn(() => ({
389
+ matches: false,
390
+ media: '(prefers-color-scheme: dark)',
391
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
392
+ addEventListener: (_: string, handler: any) => {
393
+ handlers.push(handler)
394
+ },
395
+ removeEventListener: vi.fn(),
396
+ }))
397
+ vi.stubGlobal('matchMedia', matchMediaMock)
398
+
399
+ element.theme = 'auto'
400
+ document.body.appendChild(element)
401
+ await element.updateComplete
402
+
403
+ expect(element.isDark).toBe(false)
404
+
405
+ // Simulate system preference change to dark
406
+ expect(handlers.length).toBeGreaterThan(0)
407
+ handlers[0]({ matches: true })
408
+ await element.updateComplete
409
+
410
+ expect(element.isDark).toBe(true)
411
+
412
+ const themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
413
+ expect(themeRoot?.classList.contains('dark')).toBe(true)
414
+
415
+ vi.unstubAllGlobals()
416
+ })
417
+
418
+ it('should update isDark when system preference changes to light in auto mode', async () => {
419
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
420
+ const handlers: any[] = []
421
+ const matchMediaMock = vi.fn(() => ({
422
+ matches: true, // Start with dark
423
+ media: '(prefers-color-scheme: dark)',
424
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
425
+ addEventListener: (_: string, handler: any) => {
426
+ handlers.push(handler)
427
+ },
428
+ removeEventListener: vi.fn(),
429
+ }))
430
+ vi.stubGlobal('matchMedia', matchMediaMock)
431
+
432
+ element.theme = 'auto'
433
+ document.body.appendChild(element)
434
+ await element.updateComplete
435
+
436
+ expect(element.isDark).toBe(true)
437
+
438
+ // Simulate system preference change to light
439
+ expect(handlers.length).toBeGreaterThan(0)
440
+ handlers[0]({ matches: false })
441
+ await element.updateComplete
442
+
443
+ expect(element.isDark).toBe(false)
444
+
445
+ const themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
446
+ expect(themeRoot?.classList.contains('dark')).toBe(false)
447
+
448
+ vi.unstubAllGlobals()
449
+ })
450
+ })
451
+
452
+ describe('Edge Cases', () => {
453
+ it('should handle rapid theme changes', () => {
454
+ element.theme = 'dark'
455
+ element.theme = 'light'
456
+ element.theme = 'dark'
457
+ element.theme = 'auto'
458
+
459
+ expect(element.theme).toBe('auto')
460
+ expect(element.isDark).toBeDefined()
461
+ })
462
+
463
+ it('should handle theme changes before connection', () => {
464
+ element.theme = 'dark'
465
+ expect(element.theme).toBe('dark')
466
+ })
467
+
468
+ it('should properly initialize when connected with pre-set theme', async () => {
469
+ element.theme = 'dark'
470
+ document.body.appendChild(element)
471
+ await element.updateComplete
472
+
473
+ expect(element.isDark).toBe(true)
474
+ const themeRoot = element.shadowRoot?.querySelector('[data-theme-root]')
475
+ expect(themeRoot?.classList.contains('dark')).toBe(true)
476
+ })
477
+ })
478
+ })
@@ -6,3 +6,4 @@
6
6
 
7
7
  export { withDeviceService } from './device.js'
8
8
  export { withIntlService } from './intl.js'
9
+ export { type Theme, withTheme } from './theme.js'