@furystack/shades 11.0.35 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +86 -0
- package/esm/compile-route.spec.d.ts +2 -0
- package/esm/compile-route.spec.d.ts.map +1 -0
- package/esm/compile-route.spec.js +34 -0
- package/esm/compile-route.spec.js.map +1 -0
- package/esm/components/route-link.d.ts.map +1 -1
- package/esm/components/route-link.js +4 -5
- package/esm/components/route-link.js.map +1 -1
- package/esm/components/route-link.spec.js +1 -1
- package/esm/components/route-link.spec.js.map +1 -1
- package/esm/css-generator.d.ts +50 -0
- package/esm/css-generator.d.ts.map +1 -0
- package/esm/css-generator.js +107 -0
- package/esm/css-generator.js.map +1 -0
- package/esm/css-generator.spec.d.ts +2 -0
- package/esm/css-generator.spec.d.ts.map +1 -0
- package/esm/css-generator.spec.js +162 -0
- package/esm/css-generator.spec.js.map +1 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +2 -0
- package/esm/index.js.map +1 -1
- package/esm/models/css-object.d.ts +33 -0
- package/esm/models/css-object.d.ts.map +1 -0
- package/esm/models/css-object.js +2 -0
- package/esm/models/css-object.js.map +1 -0
- package/esm/models/index.d.ts +1 -0
- package/esm/models/index.d.ts.map +1 -1
- package/esm/models/index.js +1 -0
- package/esm/models/index.js.map +1 -1
- package/esm/shade.d.ts +18 -2
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +8 -0
- package/esm/shade.js.map +1 -1
- package/esm/shade.spec.d.ts +2 -0
- package/esm/shade.spec.d.ts.map +1 -0
- package/esm/shade.spec.js +197 -0
- package/esm/shade.spec.js.map +1 -0
- package/esm/style-manager.d.ts +65 -0
- package/esm/style-manager.d.ts.map +1 -0
- package/esm/style-manager.js +95 -0
- package/esm/style-manager.js.map +1 -0
- package/esm/style-manager.spec.d.ts +2 -0
- package/esm/style-manager.spec.d.ts.map +1 -0
- package/esm/style-manager.spec.js +179 -0
- package/esm/style-manager.spec.js.map +1 -0
- package/esm/styled-element.spec.d.ts +2 -0
- package/esm/styled-element.spec.d.ts.map +1 -0
- package/esm/styled-element.spec.js +86 -0
- package/esm/styled-element.spec.js.map +1 -0
- package/esm/styled-shade.spec.d.ts +2 -0
- package/esm/styled-shade.spec.d.ts.map +1 -0
- package/esm/styled-shade.spec.js +66 -0
- package/esm/styled-shade.spec.js.map +1 -0
- package/package.json +1 -1
- package/src/compile-route.spec.ts +39 -0
- package/src/components/route-link.spec.tsx +1 -1
- package/src/components/route-link.tsx +4 -5
- package/src/css-generator.spec.ts +183 -0
- package/src/css-generator.ts +117 -0
- package/src/index.ts +2 -0
- package/src/models/css-object.ts +34 -0
- package/src/models/index.ts +1 -0
- package/src/shade.spec.tsx +238 -0
- package/src/shade.ts +29 -2
- package/src/style-manager.spec.ts +229 -0
- package/src/style-manager.ts +104 -0
- package/src/styled-element.spec.tsx +117 -0
- package/src/styled-shade.spec.ts +86 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { CSSObject, CSSProperties } from './models/css-object.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a camelCase string to kebab-case
|
|
5
|
+
* @param str - The camelCase string to convert
|
|
6
|
+
* @returns The kebab-case string
|
|
7
|
+
* @example
|
|
8
|
+
* camelToKebab('backgroundColor') // 'background-color'
|
|
9
|
+
* camelToKebab('fontSize') // 'font-size'
|
|
10
|
+
*/
|
|
11
|
+
export const camelToKebab = (str: string): string => {
|
|
12
|
+
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a key is a selector key (starts with '&')
|
|
17
|
+
* @param key - The key to check
|
|
18
|
+
* @returns True if the key is a selector key
|
|
19
|
+
*/
|
|
20
|
+
export const isSelectorKey = (key: string): boolean => {
|
|
21
|
+
return key.startsWith('&')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Converts CSS properties to a CSS declaration string
|
|
26
|
+
* @param properties - The CSS properties object
|
|
27
|
+
* @returns A CSS declaration string (e.g., "color: red; padding: 10px;")
|
|
28
|
+
*/
|
|
29
|
+
export const propertiesToCSSString = (properties: CSSProperties): string => {
|
|
30
|
+
const declarations: string[] = []
|
|
31
|
+
|
|
32
|
+
for (const key in properties) {
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(properties, key) && !isSelectorKey(key)) {
|
|
34
|
+
const value = properties[key as keyof CSSProperties]
|
|
35
|
+
if (value !== undefined && value !== null && value !== '' && typeof value === 'string') {
|
|
36
|
+
declarations.push(`${camelToKebab(key)}: ${value}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return declarations.join('; ')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generates a CSS rule string from a selector and properties
|
|
46
|
+
* @param selector - The CSS selector
|
|
47
|
+
* @param properties - The CSS properties object
|
|
48
|
+
* @returns A complete CSS rule string (e.g., "selector { color: red; }")
|
|
49
|
+
*/
|
|
50
|
+
export const generateCSSRule = (selector: string, properties: CSSProperties): string => {
|
|
51
|
+
const cssString = propertiesToCSSString(properties)
|
|
52
|
+
if (!cssString) {
|
|
53
|
+
return ''
|
|
54
|
+
}
|
|
55
|
+
return `${selector} { ${cssString}; }`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generates complete CSS from a CSSObject for a given component selector
|
|
60
|
+
* @param selector - The base selector (typically the shadowDomName)
|
|
61
|
+
* @param cssObject - The CSSObject containing styles and nested selectors
|
|
62
|
+
* @returns A complete CSS string with all rules
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* generateCSS('my-component', {
|
|
67
|
+
* color: 'red',
|
|
68
|
+
* '&:hover': { color: 'blue' },
|
|
69
|
+
* '& .inner': { fontWeight: 'bold' }
|
|
70
|
+
* })
|
|
71
|
+
* // Returns:
|
|
72
|
+
* // "my-component { color: red; }
|
|
73
|
+
* // my-component:hover { color: blue; }
|
|
74
|
+
* // my-component .inner { font-weight: bold; }"
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export const generateCSS = (selector: string, cssObject: CSSObject): string => {
|
|
78
|
+
const rules: string[] = []
|
|
79
|
+
|
|
80
|
+
// Extract base properties (non-selector keys)
|
|
81
|
+
const baseProperties: CSSProperties = {}
|
|
82
|
+
const selectorRules: Array<{ selectorKey: string; properties: CSSProperties }> = []
|
|
83
|
+
|
|
84
|
+
for (const key in cssObject) {
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(cssObject, key)) {
|
|
86
|
+
if (isSelectorKey(key)) {
|
|
87
|
+
const properties = cssObject[key as keyof CSSObject]
|
|
88
|
+
if (properties && typeof properties === 'object') {
|
|
89
|
+
selectorRules.push({ selectorKey: key, properties: properties as CSSProperties })
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
const value = cssObject[key as keyof CSSObject]
|
|
93
|
+
if (typeof value !== 'object') {
|
|
94
|
+
;(baseProperties as Record<string, unknown>)[key] = value
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Generate base rule
|
|
101
|
+
const baseRule = generateCSSRule(selector, baseProperties)
|
|
102
|
+
if (baseRule) {
|
|
103
|
+
rules.push(baseRule)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Generate selector rules
|
|
107
|
+
for (const { selectorKey, properties } of selectorRules) {
|
|
108
|
+
// Replace '&' with the base selector
|
|
109
|
+
const fullSelector = selectorKey.replace(/&/g, selector)
|
|
110
|
+
const rule = generateCSSRule(fullSelector, properties)
|
|
111
|
+
if (rule) {
|
|
112
|
+
rules.push(rule)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return rules.join('\n')
|
|
117
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from './compile-route.js'
|
|
2
2
|
export * from './components/index.js'
|
|
3
|
+
export * from './css-generator.js'
|
|
3
4
|
export * from './initialize.js'
|
|
4
5
|
export * from './models/index.js'
|
|
5
6
|
export * from './services/index.js'
|
|
6
7
|
export * from './shade-component.js'
|
|
7
8
|
export * from './shade.js'
|
|
9
|
+
export * from './style-manager.js'
|
|
8
10
|
export * from './styled-element.js'
|
|
9
11
|
export * from './styled-shade.js'
|
|
10
12
|
import './jsx.js'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base CSS properties - subset of CSSStyleDeclaration
|
|
3
|
+
*/
|
|
4
|
+
export type CSSProperties = Partial<CSSStyleDeclaration>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Selector key pattern for pseudo-classes and nested selectors
|
|
8
|
+
* Examples: '&:hover', '&:active', '& .className', '& > div'
|
|
9
|
+
*/
|
|
10
|
+
export type SelectorKey = `&${string}`
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CSS object supporting nested selectors for component-level styling.
|
|
14
|
+
*
|
|
15
|
+
* Use this type for the `css` property in Shade components to define
|
|
16
|
+
* styles that are injected as a stylesheet during component registration.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const styles: CSSObject = {
|
|
21
|
+
* padding: '16px',
|
|
22
|
+
* backgroundColor: 'white',
|
|
23
|
+
* '&:hover': {
|
|
24
|
+
* backgroundColor: '#f0f0f0'
|
|
25
|
+
* },
|
|
26
|
+
* '& .title': {
|
|
27
|
+
* fontWeight: 'bold'
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export type CSSObject = CSSProperties & {
|
|
33
|
+
[K in SelectorKey]?: CSSProperties
|
|
34
|
+
}
|
package/src/models/index.ts
CHANGED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
4
|
+
import { initializeShadeRoot } from './initialize.js'
|
|
5
|
+
import { createComponent } from './shade-component.js'
|
|
6
|
+
import { Shade } from './shade.js'
|
|
7
|
+
|
|
8
|
+
describe('Shade edge cases', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
11
|
+
})
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
document.body.innerHTML = ''
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('duplicate shadowDomName error', () => {
|
|
17
|
+
it('should throw an error when registering a duplicate shadowDomName', () => {
|
|
18
|
+
// First registration should succeed
|
|
19
|
+
Shade({
|
|
20
|
+
shadowDomName: 'shade-duplicate-test',
|
|
21
|
+
render: () => <div>First</div>,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Second registration with the same name should throw
|
|
25
|
+
expect(() => {
|
|
26
|
+
Shade({
|
|
27
|
+
shadowDomName: 'shade-duplicate-test',
|
|
28
|
+
render: () => <div>Second</div>,
|
|
29
|
+
})
|
|
30
|
+
}).toThrow("A custom shade with shadow DOM name 'shade-duplicate-test' has already been registered!")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should include the duplicate name in the error message', () => {
|
|
34
|
+
const uniqueName = `shade-duplicate-name-in-error-${Date.now()}`
|
|
35
|
+
|
|
36
|
+
Shade({
|
|
37
|
+
shadowDomName: uniqueName,
|
|
38
|
+
render: () => <div>First</div>,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
Shade({
|
|
43
|
+
shadowDomName: uniqueName,
|
|
44
|
+
render: () => <div>Second</div>,
|
|
45
|
+
})
|
|
46
|
+
// Should not reach here
|
|
47
|
+
expect.fail('Expected an error to be thrown')
|
|
48
|
+
} catch (e) {
|
|
49
|
+
expect((e as Error).message).toContain(uniqueName)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('injector from props', () => {
|
|
55
|
+
it('should use props injector for child component instead of inheriting from parent', async () => {
|
|
56
|
+
await usingAsync(new Injector(), async (rootInjector) => {
|
|
57
|
+
const propsInjector = new Injector()
|
|
58
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
59
|
+
|
|
60
|
+
let parentCapturedInjector: Injector | undefined
|
|
61
|
+
let childCapturedInjector: Injector | undefined
|
|
62
|
+
|
|
63
|
+
const ChildComponent = Shade<{ injector?: Injector }>({
|
|
64
|
+
shadowDomName: 'shade-injector-child-props-test',
|
|
65
|
+
render: ({ injector }) => {
|
|
66
|
+
childCapturedInjector = injector
|
|
67
|
+
return <div>Child</div>
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const ParentComponent = Shade({
|
|
72
|
+
shadowDomName: 'shade-injector-parent-props-test',
|
|
73
|
+
render: ({ injector, children }) => {
|
|
74
|
+
parentCapturedInjector = injector
|
|
75
|
+
return <div>{children}</div>
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
initializeShadeRoot({
|
|
80
|
+
injector: rootInjector,
|
|
81
|
+
rootElement,
|
|
82
|
+
jsxElement: (
|
|
83
|
+
<ParentComponent>
|
|
84
|
+
<ChildComponent injector={propsInjector} />
|
|
85
|
+
</ParentComponent>
|
|
86
|
+
),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await sleepAsync(10)
|
|
90
|
+
|
|
91
|
+
// Parent should use root injector (inherited from parent)
|
|
92
|
+
expect(parentCapturedInjector).toBe(rootInjector)
|
|
93
|
+
// Child should use the props injector, not the parent's
|
|
94
|
+
expect(childCapturedInjector).toBe(propsInjector)
|
|
95
|
+
expect(childCapturedInjector).not.toBe(rootInjector)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('BroadcastChannel cross-tab communication', () => {
|
|
101
|
+
it('should update stored state when receiving BroadcastChannel message with matching key', async () => {
|
|
102
|
+
const mockedStorage = new Map<string, string>()
|
|
103
|
+
|
|
104
|
+
const store: typeof localStorage = {
|
|
105
|
+
getItem: (key) => mockedStorage.get(key) || null,
|
|
106
|
+
setItem: (key, value) => mockedStorage.set(key, value),
|
|
107
|
+
length: 0,
|
|
108
|
+
clear: () => mockedStorage.clear(),
|
|
109
|
+
key: (index) => Array.from(mockedStorage.keys())[index] || null,
|
|
110
|
+
removeItem: (key) => mockedStorage.delete(key),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
114
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
115
|
+
const stateKey = 'broadcast-test-key'
|
|
116
|
+
|
|
117
|
+
const ExampleComponent = Shade({
|
|
118
|
+
shadowDomName: 'shade-broadcast-channel-test',
|
|
119
|
+
render: ({ useStoredState }) => {
|
|
120
|
+
const [value] = useStoredState(stateKey, 'initial', store)
|
|
121
|
+
return <div id="value">{value}</div>
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
initializeShadeRoot({
|
|
126
|
+
injector,
|
|
127
|
+
rootElement,
|
|
128
|
+
jsxElement: <ExampleComponent />,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await sleepAsync(50)
|
|
132
|
+
expect(document.getElementById('value')?.textContent).toBe('initial')
|
|
133
|
+
|
|
134
|
+
// Simulate cross-tab message via BroadcastChannel
|
|
135
|
+
const channel = new BroadcastChannel('useStoredState-broadcast-channel')
|
|
136
|
+
channel.postMessage({ key: stateKey, value: 'updated-from-other-tab' })
|
|
137
|
+
|
|
138
|
+
await sleepAsync(50)
|
|
139
|
+
expect(document.getElementById('value')?.textContent).toBe('updated-from-other-tab')
|
|
140
|
+
|
|
141
|
+
channel.close()
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should ignore BroadcastChannel messages with different key', async () => {
|
|
146
|
+
const mockedStorage = new Map<string, string>()
|
|
147
|
+
|
|
148
|
+
const store: typeof localStorage = {
|
|
149
|
+
getItem: (key) => mockedStorage.get(key) || null,
|
|
150
|
+
setItem: (key, value) => mockedStorage.set(key, value),
|
|
151
|
+
length: 0,
|
|
152
|
+
clear: () => mockedStorage.clear(),
|
|
153
|
+
key: (index) => Array.from(mockedStorage.keys())[index] || null,
|
|
154
|
+
removeItem: (key) => mockedStorage.delete(key),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
158
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
159
|
+
const stateKey = 'broadcast-filter-test-key'
|
|
160
|
+
|
|
161
|
+
const ExampleComponent = Shade({
|
|
162
|
+
shadowDomName: 'shade-broadcast-channel-filter-test',
|
|
163
|
+
render: ({ useStoredState }) => {
|
|
164
|
+
const [value] = useStoredState(stateKey, 'initial', store)
|
|
165
|
+
return <div id="value">{value}</div>
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
initializeShadeRoot({
|
|
170
|
+
injector,
|
|
171
|
+
rootElement,
|
|
172
|
+
jsxElement: <ExampleComponent />,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await sleepAsync(50)
|
|
176
|
+
expect(document.getElementById('value')?.textContent).toBe('initial')
|
|
177
|
+
|
|
178
|
+
// Simulate cross-tab message with different key
|
|
179
|
+
const channel = new BroadcastChannel('useStoredState-broadcast-channel')
|
|
180
|
+
channel.postMessage({ key: 'different-key', value: 'should-be-ignored' })
|
|
181
|
+
|
|
182
|
+
await sleepAsync(50)
|
|
183
|
+
// Value should remain unchanged
|
|
184
|
+
expect(document.getElementById('value')?.textContent).toBe('initial')
|
|
185
|
+
|
|
186
|
+
channel.close()
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should cleanup BroadcastChannel on component disposal', async () => {
|
|
191
|
+
const mockedStorage = new Map<string, string>()
|
|
192
|
+
|
|
193
|
+
const store: typeof localStorage = {
|
|
194
|
+
getItem: (key) => mockedStorage.get(key) || null,
|
|
195
|
+
setItem: (key, value) => mockedStorage.set(key, value),
|
|
196
|
+
length: 0,
|
|
197
|
+
clear: () => mockedStorage.clear(),
|
|
198
|
+
key: (index) => Array.from(mockedStorage.keys())[index] || null,
|
|
199
|
+
removeItem: (key) => mockedStorage.delete(key),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
203
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
204
|
+
const stateKey = 'broadcast-cleanup-test-key'
|
|
205
|
+
|
|
206
|
+
const ExampleComponent = Shade({
|
|
207
|
+
shadowDomName: 'shade-broadcast-channel-cleanup-test',
|
|
208
|
+
render: ({ useStoredState }) => {
|
|
209
|
+
const [value] = useStoredState(stateKey, 'initial', store)
|
|
210
|
+
return <div id="value">{value}</div>
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
initializeShadeRoot({
|
|
215
|
+
injector,
|
|
216
|
+
rootElement,
|
|
217
|
+
jsxElement: <ExampleComponent />,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await sleepAsync(50)
|
|
221
|
+
expect(document.getElementById('value')?.textContent).toBe('initial')
|
|
222
|
+
|
|
223
|
+
// Remove the component from DOM
|
|
224
|
+
document.body.innerHTML = ''
|
|
225
|
+
await sleepAsync(50)
|
|
226
|
+
|
|
227
|
+
// Create a new channel to send a message (simulating another tab)
|
|
228
|
+
const channel = new BroadcastChannel('useStoredState-broadcast-channel')
|
|
229
|
+
// This should not cause any errors since the component's channel should be closed
|
|
230
|
+
channel.postMessage({ key: stateKey, value: 'should-not-crash' })
|
|
231
|
+
await sleepAsync(50)
|
|
232
|
+
|
|
233
|
+
channel.close()
|
|
234
|
+
// Test passes if no errors occur
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
})
|
package/src/shade.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Constructable } from '@furystack/inject'
|
|
2
2
|
import { hasInjectorReference, Injector } from '@furystack/inject'
|
|
3
3
|
import { ObservableValue } from '@furystack/utils'
|
|
4
|
-
import type { ChildrenList, PartialElement, RenderOptions } from './models/index.js'
|
|
4
|
+
import type { ChildrenList, CSSObject, PartialElement, RenderOptions } from './models/index.js'
|
|
5
5
|
import { LocationService } from './services/location-service.js'
|
|
6
6
|
import { ResourceManager } from './services/resource-manager.js'
|
|
7
7
|
import { attachProps, attachStyles } from './shade-component.js'
|
|
8
|
+
import { StyleManager } from './style-manager.js'
|
|
8
9
|
|
|
9
10
|
export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
|
|
10
11
|
/**
|
|
@@ -45,9 +46,26 @@ export type ShadeOptions<TProps, TElementBase extends HTMLElement> = {
|
|
|
45
46
|
elementBase?: Constructable<TElementBase>
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* A default style that will be applied to the element
|
|
49
|
+
* A default style that will be applied to the element as inline styles.
|
|
50
|
+
* Can be overridden by external styles on instances.
|
|
49
51
|
*/
|
|
50
52
|
style?: Partial<CSSStyleDeclaration>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* CSS styles injected as a stylesheet during component registration.
|
|
56
|
+
* Supports pseudo-selectors (&:hover, &:active) and nested selectors (& .class).
|
|
57
|
+
* Use this for component-level styling that doesn't need per-instance overrides.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* css: {
|
|
62
|
+
* padding: '16px',
|
|
63
|
+
* '&:hover': { backgroundColor: '#f0f0f0' },
|
|
64
|
+
* '& .title': { fontWeight: 'bold' }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
css?: CSSObject
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
/**
|
|
@@ -63,6 +81,11 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
|
63
81
|
|
|
64
82
|
const existing = customElements.get(customElementName)
|
|
65
83
|
if (!existing) {
|
|
84
|
+
// Register CSS styles if provided
|
|
85
|
+
if (o.css) {
|
|
86
|
+
StyleManager.registerComponentStyles(customElementName, o.css, o.elementBaseName)
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
const ElementBase = o.elementBase || HTMLElement
|
|
67
90
|
|
|
68
91
|
customElements.define(
|
|
@@ -285,6 +308,10 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
|
285
308
|
el.props = props || ({} as TProps & PartialElement<TElementBase>)
|
|
286
309
|
el.shadeChildren = children
|
|
287
310
|
|
|
311
|
+
if (o.elementBaseName) {
|
|
312
|
+
el.setAttribute('is', customElementName)
|
|
313
|
+
}
|
|
314
|
+
|
|
288
315
|
attachStyles(el, { style: o.style })
|
|
289
316
|
attachProps(el, props)
|
|
290
317
|
|