@lukso/core 0.1.0-dev.0f1bea5

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 (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +159 -0
  3. package/dist/chunk-3WGYJTN4.js +19 -0
  4. package/dist/chunk-3WGYJTN4.js.map +1 -0
  5. package/dist/chunk-4TNWG4ME.js +106 -0
  6. package/dist/chunk-4TNWG4ME.js.map +1 -0
  7. package/dist/chunk-AMRGSLR5.cjs +1 -0
  8. package/dist/chunk-AMRGSLR5.cjs.map +1 -0
  9. package/dist/chunk-CC3LFUYY.cjs +19 -0
  10. package/dist/chunk-CC3LFUYY.cjs.map +1 -0
  11. package/dist/chunk-DFMMMF62.cjs +1 -0
  12. package/dist/chunk-DFMMMF62.cjs.map +1 -0
  13. package/dist/chunk-DKEXQFNE.js +1 -0
  14. package/dist/chunk-DKEXQFNE.js.map +1 -0
  15. package/dist/chunk-DKXHVRHM.js +84 -0
  16. package/dist/chunk-DKXHVRHM.js.map +1 -0
  17. package/dist/chunk-FR74YPGJ.cjs +87 -0
  18. package/dist/chunk-FR74YPGJ.cjs.map +1 -0
  19. package/dist/chunk-LEL6VWU4.js +1 -0
  20. package/dist/chunk-LEL6VWU4.js.map +1 -0
  21. package/dist/chunk-MBIRTPNM.cjs +84 -0
  22. package/dist/chunk-MBIRTPNM.cjs.map +1 -0
  23. package/dist/chunk-NJQVWIZL.cjs +49 -0
  24. package/dist/chunk-NJQVWIZL.cjs.map +1 -0
  25. package/dist/chunk-RM42NG7E.cjs +106 -0
  26. package/dist/chunk-RM42NG7E.cjs.map +1 -0
  27. package/dist/chunk-SV4TVR2K.js +87 -0
  28. package/dist/chunk-SV4TVR2K.js.map +1 -0
  29. package/dist/chunk-X2QNFZU7.js +49 -0
  30. package/dist/chunk-X2QNFZU7.js.map +1 -0
  31. package/dist/index.cjs +37 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +8 -0
  34. package/dist/index.d.ts +8 -0
  35. package/dist/index.js +37 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mixins/device.cjs +8 -0
  38. package/dist/mixins/device.cjs.map +1 -0
  39. package/dist/mixins/device.d.cts +34 -0
  40. package/dist/mixins/device.d.ts +34 -0
  41. package/dist/mixins/device.js +8 -0
  42. package/dist/mixins/device.js.map +1 -0
  43. package/dist/mixins/index.cjs +14 -0
  44. package/dist/mixins/index.cjs.map +1 -0
  45. package/dist/mixins/index.d.cts +3 -0
  46. package/dist/mixins/index.d.ts +3 -0
  47. package/dist/mixins/index.js +14 -0
  48. package/dist/mixins/index.js.map +1 -0
  49. package/dist/mixins/intl.cjs +8 -0
  50. package/dist/mixins/intl.cjs.map +1 -0
  51. package/dist/mixins/intl.d.cts +37 -0
  52. package/dist/mixins/intl.d.ts +37 -0
  53. package/dist/mixins/intl.js +8 -0
  54. package/dist/mixins/intl.js.map +1 -0
  55. package/dist/services/device.cjs +7 -0
  56. package/dist/services/device.cjs.map +1 -0
  57. package/dist/services/device.d.cts +71 -0
  58. package/dist/services/device.d.ts +71 -0
  59. package/dist/services/device.js +7 -0
  60. package/dist/services/device.js.map +1 -0
  61. package/dist/services/index.cjs +20 -0
  62. package/dist/services/index.cjs.map +1 -0
  63. package/dist/services/index.d.cts +4 -0
  64. package/dist/services/index.d.ts +4 -0
  65. package/dist/services/index.js +20 -0
  66. package/dist/services/index.js.map +1 -0
  67. package/dist/services/intl.cjs +15 -0
  68. package/dist/services/intl.cjs.map +1 -0
  69. package/dist/services/intl.d.cts +170 -0
  70. package/dist/services/intl.d.ts +170 -0
  71. package/dist/services/intl.js +15 -0
  72. package/dist/services/intl.js.map +1 -0
  73. package/dist/utils/index.cjs +11 -0
  74. package/dist/utils/index.cjs.map +1 -0
  75. package/dist/utils/index.d.cts +32 -0
  76. package/dist/utils/index.d.ts +32 -0
  77. package/dist/utils/index.js +11 -0
  78. package/dist/utils/index.js.map +1 -0
  79. package/package.json +114 -0
  80. package/src/index.ts +10 -0
  81. package/src/mixins/__tests__/device.spec.ts +119 -0
  82. package/src/mixins/__tests__/intl.spec.ts +198 -0
  83. package/src/mixins/device.ts +48 -0
  84. package/src/mixins/index.ts +8 -0
  85. package/src/mixins/intl.ts +112 -0
  86. package/src/services/__tests__/device.spec.ts +36 -0
  87. package/src/services/__tests__/intl.spec.ts +536 -0
  88. package/src/services/device.ts +121 -0
  89. package/src/services/index.ts +17 -0
  90. package/src/services/intl.ts +326 -0
  91. package/src/utils/__tests__/browserInfo.spec.ts +310 -0
  92. package/src/utils/__tests__/slug.spec.ts +26 -0
  93. package/src/utils/browserInfo.ts +102 -0
  94. package/src/utils/index.ts +4 -0
  95. package/src/utils/slug.ts +13 -0
@@ -0,0 +1,119 @@
1
+ import { html, LitElement } from 'lit'
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
3
+ import { withDeviceService } from '../device'
4
+
5
+ describe('withDeviceService 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-device-${Math.random().toString(36).slice(2)}`
13
+
14
+ // Create a test component using the mixin
15
+ TestComponent = class extends withDeviceService(LitElement) {
16
+ render() {
17
+ return html`<div>${this.device?.isMobile ? 'mobile' : 'desktop'}</div>`
18
+ }
19
+ }
20
+
21
+ // Register the component before instantiation
22
+ if (!customElements.get(tagName)) {
23
+ customElements.define(tagName, TestComponent)
24
+ }
25
+
26
+ element = new TestComponent()
27
+ })
28
+
29
+ afterEach(() => {
30
+ if (element?.parentElement) {
31
+ element.remove()
32
+ }
33
+ })
34
+
35
+ it('should initialize device service when connected', async () => {
36
+ document.body.appendChild(element)
37
+ await element.updateComplete
38
+
39
+ expect(element.device).toBeDefined()
40
+ expect(typeof element.device).toBe('object')
41
+ })
42
+
43
+ it('should have device detection methods', async () => {
44
+ document.body.appendChild(element)
45
+ await element.updateComplete
46
+
47
+ expect(element.device).toHaveProperty('isMobile')
48
+ expect(element.device).toHaveProperty('isTablet')
49
+ expect(element.device).toHaveProperty('isDesktop')
50
+ })
51
+
52
+ it('should have OS detection properties', async () => {
53
+ document.body.appendChild(element)
54
+ await element.updateComplete
55
+
56
+ expect(element.device).toHaveProperty('isIOS')
57
+ expect(element.device).toHaveProperty('isAndroid')
58
+ expect(element.device).toHaveProperty('isWindows')
59
+ expect(element.device).toHaveProperty('isMacOS')
60
+ expect(element.device).toHaveProperty('isLinux')
61
+ })
62
+
63
+ it('should have browser detection properties', async () => {
64
+ document.body.appendChild(element)
65
+ await element.updateComplete
66
+
67
+ expect(element.device).toHaveProperty('isChrome')
68
+ expect(element.device).toHaveProperty('isSafari')
69
+ expect(element.device).toHaveProperty('isFirefox')
70
+ expect(element.device).toHaveProperty('isEdge')
71
+ expect(element.device).toHaveProperty('isOpera')
72
+ })
73
+
74
+ it('should have getRaw method', async () => {
75
+ document.body.appendChild(element)
76
+ await element.updateComplete
77
+
78
+ expect(element.device).toHaveProperty('getRaw')
79
+ expect(typeof element.device.getRaw).toBe('function')
80
+ })
81
+
82
+ it('should provide device detection values as booleans', async () => {
83
+ document.body.appendChild(element)
84
+ await element.updateComplete
85
+
86
+ expect(typeof element.device.isMobile).toBe('boolean')
87
+ expect(typeof element.device.isDesktop).toBe('boolean')
88
+ expect(typeof element.device.isChrome).toBe('boolean')
89
+ })
90
+
91
+ it('should work with multiple component instances', async () => {
92
+ const element2 = new TestComponent()
93
+
94
+ document.body.appendChild(element)
95
+ document.body.appendChild(element2)
96
+ await element.updateComplete
97
+ await element2.updateComplete
98
+
99
+ expect(element.device).toBeDefined()
100
+ expect(element2.device).toBeDefined()
101
+ // Both instances should have the same device data
102
+ expect(element.device.isMobile).toEqual(element2.device.isMobile)
103
+
104
+ element2.remove()
105
+ })
106
+
107
+ it('should maintain device property after disconnection', async () => {
108
+ document.body.appendChild(element)
109
+ await element.updateComplete
110
+
111
+ const deviceBefore = element.device
112
+ element.remove()
113
+ const deviceAfter = element.device
114
+
115
+ // Device reference should still exist after disconnection
116
+ expect(deviceAfter).toBeDefined()
117
+ expect(deviceAfter).toEqual(deviceBefore)
118
+ })
119
+ })
@@ -0,0 +1,198 @@
1
+ import { html, LitElement } from 'lit'
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
3
+ import {
4
+ clearIntlService,
5
+ createIntlService,
6
+ setIntlService,
7
+ } from '../../services/intl'
8
+ import { withIntlService } from '../intl'
9
+
10
+ describe('withIntlService Mixin', () => {
11
+ let element: any
12
+ let TestComponent: any
13
+ let tagName: string
14
+
15
+ beforeEach(() => {
16
+ clearIntlService()
17
+
18
+ // Create a unique tag name for each test to avoid registration conflicts
19
+ tagName = `test-intl-${Math.random().toString(36).slice(2)}`
20
+
21
+ // Create a test component using the mixin
22
+ TestComponent = class extends withIntlService(LitElement) {
23
+ render() {
24
+ return html`<div>${this.formatMessage('greeting')}</div>`
25
+ }
26
+ }
27
+
28
+ // Register the component before instantiation
29
+ if (!customElements.get(tagName)) {
30
+ customElements.define(tagName, TestComponent)
31
+ }
32
+
33
+ element = new TestComponent()
34
+ })
35
+
36
+ afterEach(() => {
37
+ if (element?.parentElement) {
38
+ element.remove()
39
+ }
40
+ clearIntlService()
41
+ })
42
+
43
+ it('should add formatMessage method to component', async () => {
44
+ document.body.appendChild(element)
45
+ await element.updateComplete
46
+
47
+ expect(element).toHaveProperty('formatMessage')
48
+ expect(typeof element.formatMessage).toBe('function')
49
+ })
50
+
51
+ it('should use global intl service when available', async () => {
52
+ const intl = createIntlService({
53
+ locale: 'en-US',
54
+ messages: { greeting: 'Hello' },
55
+ })
56
+ setIntlService(intl)
57
+
58
+ document.body.appendChild(element)
59
+ await element.updateComplete
60
+
61
+ const result = element.formatMessage('greeting')
62
+ expect(result).toBe('Hello')
63
+ })
64
+
65
+ it('should create fallback behavior if global service not available', async () => {
66
+ clearIntlService()
67
+
68
+ document.body.appendChild(element)
69
+ await element.updateComplete
70
+
71
+ // Should return key as fallback
72
+ const result = element.formatMessage('any_key')
73
+ expect(result).toBe('any_key')
74
+ })
75
+
76
+ it('should format messages with variables', async () => {
77
+ const intl = createIntlService({
78
+ locale: 'en-US',
79
+ messages: {
80
+ welcome: 'Welcome, {name}!',
81
+ },
82
+ })
83
+ setIntlService(intl)
84
+
85
+ document.body.appendChild(element)
86
+ await element.updateComplete
87
+
88
+ const result = element.formatMessage('welcome', { name: 'John' })
89
+ expect(result).toContain('Welcome')
90
+ expect(result).toContain('John')
91
+ })
92
+
93
+ it('should return key as fallback for missing messages', async () => {
94
+ const intl = createIntlService({
95
+ locale: 'en-US',
96
+ messages: {},
97
+ })
98
+ setIntlService(intl)
99
+
100
+ document.body.appendChild(element)
101
+ await element.updateComplete
102
+
103
+ const result = element.formatMessage('missing_key')
104
+ expect(result).toBe('missing_key')
105
+ })
106
+
107
+ it('should reflect locale changes in formatting', async () => {
108
+ const intl = createIntlService({
109
+ locale: 'en-US',
110
+ messages: { greeting: 'Hello' },
111
+ })
112
+ setIntlService(intl)
113
+
114
+ document.body.appendChild(element)
115
+ await element.updateComplete
116
+
117
+ expect(element.formatMessage('greeting')).toBe('Hello')
118
+
119
+ intl.setLocale('de-DE', { greeting: 'Hallo' })
120
+ await element.updateComplete
121
+
122
+ expect(element.formatMessage('greeting')).toBe('Hallo')
123
+ })
124
+
125
+ it('should work with multiple component instances using same global service', async () => {
126
+ const intl = createIntlService({
127
+ locale: 'en-US',
128
+ messages: { greeting: 'Hello' },
129
+ })
130
+ setIntlService(intl)
131
+
132
+ const element2 = new TestComponent()
133
+
134
+ document.body.appendChild(element)
135
+ document.body.appendChild(element2)
136
+ await element.updateComplete
137
+ await element2.updateComplete
138
+
139
+ expect(element.formatMessage('greeting')).toBe('Hello')
140
+ expect(element2.formatMessage('greeting')).toBe('Hello')
141
+
142
+ element2.remove()
143
+ })
144
+
145
+ it('should handle locale changes without errors', async () => {
146
+ const intl = createIntlService({
147
+ locale: 'en-US',
148
+ messages: { greeting: 'Hello' },
149
+ })
150
+ setIntlService(intl)
151
+
152
+ document.body.appendChild(element)
153
+ await element.updateComplete
154
+
155
+ // Rapidly change locales - should not throw
156
+ expect(() => {
157
+ intl.setLocale('de-DE', { greeting: 'Hallo' })
158
+ intl.setLocale('fr-FR', { greeting: 'Bonjour' })
159
+ intl.setLocale('es-ES', { greeting: 'Hola' })
160
+ }).not.toThrow()
161
+
162
+ expect(element.formatMessage('greeting')).toBe('Hola')
163
+ })
164
+
165
+ it('should handle undefined key gracefully', async () => {
166
+ const intl = createIntlService({
167
+ locale: 'en-US',
168
+ messages: {},
169
+ })
170
+ setIntlService(intl)
171
+
172
+ document.body.appendChild(element)
173
+ await element.updateComplete
174
+
175
+ // Should not throw with undefined key
176
+ expect(() => {
177
+ element.formatMessage(undefined)
178
+ }).not.toThrow()
179
+ })
180
+
181
+ it('should work after component disconnection', async () => {
182
+ const intl = createIntlService({
183
+ locale: 'en-US',
184
+ messages: { greeting: 'Hello' },
185
+ })
186
+ setIntlService(intl)
187
+
188
+ document.body.appendChild(element)
189
+ await element.updateComplete
190
+
191
+ element.remove()
192
+
193
+ // Locale changes should still work without errors
194
+ expect(() => {
195
+ intl.setLocale('de-DE', { greeting: 'Hallo' })
196
+ }).not.toThrow()
197
+ })
198
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Device Detection Mixin
3
+ *
4
+ * Mixin to add device detection service to a Lit component
5
+ */
6
+
7
+ import type { LitElement } from 'lit'
8
+ import type { DeviceService } from '../services/device.js'
9
+ import { deviceService, type NavigatorExtended } from '../services/device.js'
10
+
11
+ /**
12
+ * Mixin to add device detection service to a Lit component
13
+ *
14
+ * Provides a `device` property with device/OS/browser detection capabilities.
15
+ * The device service is initialized in connectedCallback and follows component lifecycle.
16
+ *
17
+ * @typeParam T - The Lit component class being extended
18
+ * @returns Extended class with device detection capabilities
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { LitElement } from 'lit';
23
+ * import { customElement } from 'lit/decorators.js';
24
+ * import { withDeviceService } from '@lukso/core/mixins';
25
+ *
26
+ * @customElement('my-component')
27
+ * export class MyComponent extends withDeviceService(LitElement) {
28
+ * render() {
29
+ * return html\`Device is mobile: \${this.device?.isMobile}\`;
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ export function withDeviceService<T extends typeof LitElement>(Base: T): any {
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ class Mixin extends (Base as any) {
38
+ device: DeviceService | undefined
39
+
40
+ connectedCallback(): void {
41
+ super.connectedCallback()
42
+ this.device = deviceService(navigator as NavigatorExtended)
43
+ }
44
+ }
45
+
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ return Mixin as any
48
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @lukso/core - Mixins
3
+ *
4
+ * Reusable Lit component mixins for device detection, internationalization, and more
5
+ */
6
+
7
+ export { withDeviceService } from './device.js'
8
+ export { withIntlService } from './intl.js'
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Internationalization Mixin
3
+ *
4
+ * Mixin to add internationalization service to a Lit component
5
+ */
6
+
7
+ import { effect } from '@preact/signals-core'
8
+ import type { LitElement } from 'lit'
9
+ import englishTranslations from '../../../translations/en_US.json'
10
+ import {
11
+ createIntlService,
12
+ defaultConfig,
13
+ getIntlService,
14
+ type IntlService,
15
+ setIntlService,
16
+ } from '../services/intl.js'
17
+
18
+ /**
19
+ * Mixin to add internationalization service to a Lit component
20
+ *
21
+ * Provides access to the global intl service with reactive locale changes.
22
+ * Automatically subscribes to locale changes and triggers re-renders.
23
+ *
24
+ * The component will use the global intl service if available, or create a local one.
25
+ * This follows the singleton pattern for the global service while allowing flexibility.
26
+ *
27
+ * @typeParam T - The Lit component class being extended
28
+ * @returns Extended class with intl service capabilities
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { LitElement, html } from 'lit';
33
+ * import { customElement } from 'lit/decorators.js';
34
+ * import { withIntlService } from '@lukso/core/mixins';
35
+ *
36
+ * @customElement('my-component')
37
+ * export class MyComponent extends withIntlService(LitElement) {
38
+ * render() {
39
+ * return html\`<p>\${this.formatMessage('app.welcome')}</p>\`;
40
+ * }
41
+ * }
42
+ * ```
43
+ */
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ export function withIntlService<T extends typeof LitElement>(Base: T): any {
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ class Mixin extends (Base as any) {
48
+ protected unsubscribeIntl: (() => void) | undefined = undefined
49
+
50
+ connectedCallback(): void {
51
+ super.connectedCallback()
52
+
53
+ // Subscribe to intl changes via signal
54
+ let intl: IntlService | null = getIntlService()
55
+
56
+ // When no intl is provided by host app we initialize our own
57
+ if (!intl) {
58
+ intl = this.setupLocalIntl() ?? null
59
+ }
60
+
61
+ if (intl) {
62
+ this.unsubscribeIntl = effect(() => {
63
+ // Access the signal to track changes
64
+ intl?.localeChanged.value
65
+ this.requestUpdate()
66
+ })
67
+ }
68
+
69
+ // Set fallback for missing translations
70
+ intl?.setFallbackTranslations(englishTranslations as any)
71
+ }
72
+
73
+ disconnectedCallback(): void {
74
+ super.disconnectedCallback()
75
+
76
+ // Unsubscribe from intl changes
77
+ if (typeof this.unsubscribeIntl === 'function') {
78
+ this.unsubscribeIntl()
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Setup a local intl service with default configuration
84
+ * Subclasses can override this to customize initialization
85
+ */
86
+ protected setupLocalIntl(): IntlService | undefined {
87
+ const intlService = createIntlService(
88
+ Object.assign(defaultConfig, {
89
+ messages: englishTranslations,
90
+ })
91
+ )
92
+ setIntlService(intlService)
93
+ return intlService
94
+ }
95
+
96
+ /**
97
+ * Format message using the intl service
98
+ */
99
+ formatMessage(key?: string, options?: Record<string, string>): string {
100
+ if (!key) {
101
+ console.warn('No translation key provided to formatMessage')
102
+ return ''
103
+ }
104
+
105
+ const intl = getIntlService()
106
+ return intl?.formatMessage(key, options) ?? key
107
+ }
108
+ }
109
+
110
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
+ return Mixin as any
112
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { deviceService } from '../device'
3
+
4
+ describe('deviceService', () => {
5
+ it('should create a device service instance', () => {
6
+ const device = deviceService()
7
+ expect(device).toBeDefined()
8
+ expect(device.getRaw).toBeDefined()
9
+ })
10
+
11
+ it('should have device detection properties', () => {
12
+ const device = deviceService()
13
+ expect(device).toHaveProperty('isMobile')
14
+ expect(device).toHaveProperty('isTablet')
15
+ expect(device).toHaveProperty('isDesktop')
16
+ })
17
+
18
+ it('should have OS detection properties', () => {
19
+ const device = deviceService()
20
+ expect(device).toHaveProperty('isIOS')
21
+ expect(device).toHaveProperty('isAndroid')
22
+ expect(device).toHaveProperty('isWindows')
23
+ expect(device).toHaveProperty('isMacOS')
24
+ expect(device).toHaveProperty('isLinux')
25
+ })
26
+
27
+ it('should have browser detection properties', () => {
28
+ const device = deviceService()
29
+ expect(device).toHaveProperty('isChrome')
30
+ expect(device).toHaveProperty('isSafari')
31
+ expect(device).toHaveProperty('isFirefox')
32
+ expect(device).toHaveProperty('isEdge')
33
+ expect(device).toHaveProperty('isOpera')
34
+ expect(device).toHaveProperty('isBrave')
35
+ })
36
+ })