@nordcraft/runtime 1.0.32 → 1.0.33
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/dist/components/createComponent.js +40 -0
- package/dist/components/createComponent.js.map +1 -1
- package/dist/components/createElement.js +39 -3
- package/dist/components/createElement.js.map +1 -1
- package/dist/custom-element.main.esm.js +34 -28
- package/dist/custom-element.main.esm.js.map +4 -4
- package/dist/editor-preview.main.js +19 -0
- package/dist/editor-preview.main.js.map +1 -1
- package/dist/page.main.esm.js +3 -3
- package/dist/page.main.esm.js.map +4 -4
- package/dist/styles/CustomPropertyStyleSheet.d.ts +34 -0
- package/dist/styles/CustomPropertyStyleSheet.js +109 -0
- package/dist/styles/CustomPropertyStyleSheet.js.map +1 -0
- package/dist/styles/CustomPropertyStyleSheet.test.d.ts +1 -0
- package/dist/styles/CustomPropertyStyleSheet.test.js +66 -0
- package/dist/styles/CustomPropertyStyleSheet.test.js.map +1 -0
- package/dist/styles/style.js +23 -0
- package/dist/styles/style.js.map +1 -1
- package/dist/utils/omitStyle.js +3 -1
- package/dist/utils/omitStyle.js.map +1 -1
- package/dist/utils/subscribeCustomProperty.d.ts +8 -0
- package/dist/utils/subscribeCustomProperty.js +9 -0
- package/dist/utils/subscribeCustomProperty.js.map +1 -0
- package/package.json +3 -3
- package/src/components/createComponent.ts +48 -0
- package/src/components/createElement.ts +52 -3
- package/src/editor-preview.main.ts +27 -0
- package/src/styles/CustomPropertyStyleSheet.test.ts +104 -0
- package/src/styles/CustomPropertyStyleSheet.ts +163 -0
- package/src/styles/style.ts +33 -0
- package/src/utils/omitStyle.ts +10 -2
- package/src/utils/subscribeCustomProperty.ts +41 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { CustomPropertyStyleSheet } from './CustomPropertyStyleSheet'
|
|
3
|
+
|
|
4
|
+
describe('CustomPropertyStyleSheet', () => {
|
|
5
|
+
test('it creates a new stylesheet', () => {
|
|
6
|
+
const instance = new CustomPropertyStyleSheet()
|
|
7
|
+
expect(instance).toBeInstanceOf(CustomPropertyStyleSheet)
|
|
8
|
+
expect(instance.getStyleSheet().cssRules).toHaveLength(0)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('it adds a property definition', () => {
|
|
12
|
+
const instance = new CustomPropertyStyleSheet()
|
|
13
|
+
instance.registerProperty('.my-class', '--my-property')
|
|
14
|
+
expect(instance.getStyleSheet().cssRules.length).toBe(1)
|
|
15
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe('.my-class { }')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('it puts different selectors in different rules', () => {
|
|
19
|
+
const instance = new CustomPropertyStyleSheet()
|
|
20
|
+
instance.registerProperty('.my-class', '--my-property')('256px')
|
|
21
|
+
instance.registerProperty('.my-other-class', '--my-property')('256px')
|
|
22
|
+
expect(instance.getStyleSheet().cssRules.length).toBe(2)
|
|
23
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
24
|
+
'.my-class { --my-property: 256px; }',
|
|
25
|
+
)
|
|
26
|
+
expect(instance.getStyleSheet().cssRules[1].cssText).toBe(
|
|
27
|
+
'.my-other-class { --my-property: 256px; }',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('it can update properties', () => {
|
|
32
|
+
const instance = new CustomPropertyStyleSheet()
|
|
33
|
+
const setter = instance.registerProperty('.my-class', '--my-property')
|
|
34
|
+
setter('256px')
|
|
35
|
+
expect(instance.getStyleSheet().cssRules.length).toBe(1)
|
|
36
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
37
|
+
'.my-class { --my-property: 256px; }',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
setter('inherit')
|
|
41
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
42
|
+
'.my-class { --my-property: inherit; }',
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('it works with media queries', () => {
|
|
47
|
+
const instance = new CustomPropertyStyleSheet()
|
|
48
|
+
instance.registerProperty('.my-class', '--my-property', {
|
|
49
|
+
mediaQuery: { 'max-width': '600px' },
|
|
50
|
+
})('256px')
|
|
51
|
+
expect(instance.getStyleSheet().cssRules.length).toBe(1)
|
|
52
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
53
|
+
'@media (max-width: 600px) { .my-class { --my-property: 256px; } }',
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('it unregisters a property', () => {
|
|
58
|
+
const instance = new CustomPropertyStyleSheet()
|
|
59
|
+
const setter = instance.registerProperty('.my-class', '--my-property')
|
|
60
|
+
setter('256px')
|
|
61
|
+
const setter2 = instance.registerProperty(
|
|
62
|
+
'.my-class',
|
|
63
|
+
'--my-other-property',
|
|
64
|
+
)
|
|
65
|
+
setter2('512px')
|
|
66
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
67
|
+
'.my-class { --my-property: 256px; --my-other-property: 512px; }',
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
instance.unregisterProperty('.my-class', '--my-property')
|
|
71
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
72
|
+
'.my-class { --my-other-property: 512px; }',
|
|
73
|
+
)
|
|
74
|
+
instance.unregisterProperty('.my-class', '--my-other-property')
|
|
75
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe('.my-class { }')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('it unregisters a property with media queries', () => {
|
|
79
|
+
const instance = new CustomPropertyStyleSheet()
|
|
80
|
+
const setter = instance.registerProperty(
|
|
81
|
+
'.my-class-with-media',
|
|
82
|
+
'--my-property-with-media',
|
|
83
|
+
{
|
|
84
|
+
mediaQuery: { 'max-width': '600px' },
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
setter('256px')
|
|
88
|
+
expect(instance.getStyleSheet().cssRules.length).toBe(1)
|
|
89
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
90
|
+
'@media (max-width: 600px) { .my-class-with-media { --my-property-with-media: 256px; } }',
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
instance.unregisterProperty(
|
|
94
|
+
'.my-class-with-media',
|
|
95
|
+
'--my-property-with-media',
|
|
96
|
+
{
|
|
97
|
+
mediaQuery: { 'max-width': '600px' },
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
|
|
101
|
+
'@media (max-width: 600px) { .my-class-with-media { } }',
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { MediaQuery } from '@nordcraft/core/dist/component/component.types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CustomPropertyStyleSheet is a utility class that manages CSS custom properties
|
|
5
|
+
* (variables) in a dedicated CSSStyleSheet. It allows for efficient registration,
|
|
6
|
+
* updating, and removal of style properties as fast as setting style properties.
|
|
7
|
+
*
|
|
8
|
+
* It abstracts the complexity of managing CSS rules via. indexing and
|
|
9
|
+
* provides a simple API to register and unregister style properties for specific
|
|
10
|
+
* selectors.
|
|
11
|
+
*/
|
|
12
|
+
export class CustomPropertyStyleSheet {
|
|
13
|
+
private styleSheet: CSSStyleSheet
|
|
14
|
+
|
|
15
|
+
// Selector to rule index mapping
|
|
16
|
+
private ruleMap: Map<string, CSSStyleRule | CSSNestedDeclarations> | undefined
|
|
17
|
+
|
|
18
|
+
constructor(styleSheet?: CSSStyleSheet | null) {
|
|
19
|
+
if (styleSheet) {
|
|
20
|
+
this.styleSheet = styleSheet
|
|
21
|
+
} else {
|
|
22
|
+
this.styleSheet = new CSSStyleSheet()
|
|
23
|
+
document.adoptedStyleSheets.push(this.getStyleSheet())
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @returns A function to update the property value efficiently.
|
|
29
|
+
*/
|
|
30
|
+
public registerProperty(
|
|
31
|
+
selector: string,
|
|
32
|
+
name: string,
|
|
33
|
+
options?: {
|
|
34
|
+
mediaQuery?: MediaQuery
|
|
35
|
+
startingStyle?: boolean
|
|
36
|
+
},
|
|
37
|
+
): (newValue: string) => void {
|
|
38
|
+
this.ruleMap ??= this.hydrateFromBase()
|
|
39
|
+
const fullSelector = CustomPropertyStyleSheet.getFullSelector(
|
|
40
|
+
selector,
|
|
41
|
+
options,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// Check if the selector already exists
|
|
45
|
+
let rule = this.ruleMap.get(fullSelector)
|
|
46
|
+
if (!rule) {
|
|
47
|
+
const ruleIndex = this.styleSheet.insertRule(fullSelector)
|
|
48
|
+
let newRule = this.styleSheet.cssRules[ruleIndex]
|
|
49
|
+
|
|
50
|
+
// We are only interested in the dynamic style, so get the actual style rule, not media or other nested rules. Loop until we are at the bottom most rule.
|
|
51
|
+
while (
|
|
52
|
+
(newRule as any).cssRules &&
|
|
53
|
+
(newRule as CSSGroupingRule).cssRules.length > 0
|
|
54
|
+
) {
|
|
55
|
+
newRule = (newRule as CSSGroupingRule).cssRules[0]
|
|
56
|
+
}
|
|
57
|
+
rule = newRule as CSSStyleRule | CSSNestedDeclarations
|
|
58
|
+
this.ruleMap.set(fullSelector, rule)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (value: string) => {
|
|
62
|
+
rule.style.setProperty(name, value)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public unregisterProperty(
|
|
67
|
+
selector: string,
|
|
68
|
+
name: string,
|
|
69
|
+
options?: {
|
|
70
|
+
mediaQuery?: MediaQuery
|
|
71
|
+
startingStyle?: boolean
|
|
72
|
+
},
|
|
73
|
+
): void {
|
|
74
|
+
if (!this.ruleMap) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fullSelector = CustomPropertyStyleSheet.getFullSelector(
|
|
79
|
+
selector,
|
|
80
|
+
options,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// We only clean up the property, as we assume that the rule will be reused.
|
|
84
|
+
this.ruleMap.get(fullSelector)?.style.removeProperty(name)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public getStyleSheet(): CSSStyleSheet {
|
|
88
|
+
return this.styleSheet
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Maps all selectors to their rule index. This is used to map the initial
|
|
93
|
+
* SSR style variable values to their selectors.
|
|
94
|
+
*/
|
|
95
|
+
private hydrateFromBase() {
|
|
96
|
+
const ruleIndex: Map<string, CSSStyleRule> = new Map()
|
|
97
|
+
for (let i = 0; i < this.styleSheet.cssRules.length; i++) {
|
|
98
|
+
let rule = this.styleSheet.cssRules[i]
|
|
99
|
+
const selector = CustomPropertyStyleSheet.selectorFromCSSRule(rule)
|
|
100
|
+
// Get last part of the selector, which is the actual selector we are interested in
|
|
101
|
+
while (
|
|
102
|
+
(rule as CSSGroupingRule).cssRules &&
|
|
103
|
+
(rule as CSSGroupingRule).cssRules.length > 0
|
|
104
|
+
) {
|
|
105
|
+
rule = (rule as CSSGroupingRule).cssRules[0]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ruleIndex.set(selector, rule as CSSStyleRule)
|
|
109
|
+
}
|
|
110
|
+
return ruleIndex
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static selectorFromCSSRule(rule: CSSRule): string {
|
|
114
|
+
switch (rule.constructor.name) {
|
|
115
|
+
case 'CSSStyleRule':
|
|
116
|
+
// For these rules, we just return (potentially with subrules if any cssRules exist)
|
|
117
|
+
return `${(rule as CSSStyleRule).selectorText} { ${Array.from(
|
|
118
|
+
(rule as CSSStyleRule).cssRules,
|
|
119
|
+
)
|
|
120
|
+
.map(CustomPropertyStyleSheet.selectorFromCSSRule)
|
|
121
|
+
.join(', ')}}`
|
|
122
|
+
case 'CSSStartingStyleRule':
|
|
123
|
+
return `@starting-style { ${Array.from(
|
|
124
|
+
(rule as CSSStartingStyleRule).cssRules,
|
|
125
|
+
)
|
|
126
|
+
.map(CustomPropertyStyleSheet.selectorFromCSSRule)
|
|
127
|
+
.join(', ')}}`
|
|
128
|
+
case 'CSSMediaRule':
|
|
129
|
+
return `@media ${(rule as CSSMediaRule).media.mediaText} { ${Array.from(
|
|
130
|
+
(rule as CSSMediaRule).cssRules,
|
|
131
|
+
)
|
|
132
|
+
.map(CustomPropertyStyleSheet.selectorFromCSSRule)
|
|
133
|
+
.join(', ')}}`
|
|
134
|
+
case 'CSSNestedDeclarations':
|
|
135
|
+
return ''
|
|
136
|
+
default:
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.warn(
|
|
139
|
+
`Unsupported CSS rule type: ${rule.constructor.name}. Returning empty selector.`,
|
|
140
|
+
)
|
|
141
|
+
return ''
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private static getFullSelector(
|
|
146
|
+
selector: string,
|
|
147
|
+
options?: {
|
|
148
|
+
mediaQuery?: MediaQuery
|
|
149
|
+
startingStyle?: boolean
|
|
150
|
+
},
|
|
151
|
+
) {
|
|
152
|
+
let result =
|
|
153
|
+
selector + (options?.startingStyle ? ' { @starting-style { }}' : ' { }')
|
|
154
|
+
if (options?.mediaQuery) {
|
|
155
|
+
result = `@media (${Object.entries(options.mediaQuery)
|
|
156
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join(') and (')}) { ${result}}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/styles/style.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
getClassName,
|
|
9
9
|
toValidClassName,
|
|
10
10
|
} from '@nordcraft/core/dist/styling/className'
|
|
11
|
+
import {
|
|
12
|
+
syntaxNodeToPropertyAtDefinition,
|
|
13
|
+
type CssSyntaxNode,
|
|
14
|
+
} from '@nordcraft/core/dist/styling/customProperty'
|
|
11
15
|
import { kebabCase } from '@nordcraft/core/dist/styling/style.css'
|
|
12
16
|
import { variantSelector } from '@nordcraft/core/dist/styling/variantSelector'
|
|
13
17
|
import { omitKeys } from '@nordcraft/core/dist/utils/collections'
|
|
@@ -61,6 +65,7 @@ export const insertStyles = (
|
|
|
61
65
|
root: Component,
|
|
62
66
|
components: Component[],
|
|
63
67
|
) => {
|
|
68
|
+
const registeredCustomProperties = new Map<string, CssSyntaxNode>()
|
|
64
69
|
const getNodeStyles = (
|
|
65
70
|
node: ElementNodeModel | ComponentNodeModel,
|
|
66
71
|
classHash: string,
|
|
@@ -131,6 +136,34 @@ ${
|
|
|
131
136
|
.join('\n')
|
|
132
137
|
: ''
|
|
133
138
|
}
|
|
139
|
+
${[
|
|
140
|
+
...Object.entries(node.customProperties ?? {}),
|
|
141
|
+
...(node.variants?.flatMap((variant) =>
|
|
142
|
+
Object.entries(variant.customProperties ?? {}),
|
|
143
|
+
) ?? []),
|
|
144
|
+
]
|
|
145
|
+
.map(([customPropertyName, customProperty]) => {
|
|
146
|
+
const existingCustomProperty =
|
|
147
|
+
registeredCustomProperties.get(customPropertyName)
|
|
148
|
+
if (existingCustomProperty) {
|
|
149
|
+
// Warn if the style variable is already registered with a different syntax, as registration is global.
|
|
150
|
+
// The editor should also report an Error-level issue.
|
|
151
|
+
if (
|
|
152
|
+
existingCustomProperty.type === 'primitive' &&
|
|
153
|
+
customProperty.syntax.type === 'primitive' &&
|
|
154
|
+
existingCustomProperty.name !== customProperty.syntax.name
|
|
155
|
+
) {
|
|
156
|
+
// eslint-disable-next-line no-console
|
|
157
|
+
console.warn(
|
|
158
|
+
`Custom property "${customPropertyName}" is already registered with a different syntax: "${existingCustomProperty.name}".`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return ''
|
|
162
|
+
}
|
|
163
|
+
registeredCustomProperties.set(customPropertyName, customProperty.syntax)
|
|
164
|
+
return syntaxNodeToPropertyAtDefinition(customPropertyName, customProperty)
|
|
165
|
+
})
|
|
166
|
+
.join('\n')}
|
|
134
167
|
`),
|
|
135
168
|
)
|
|
136
169
|
return styleElem
|
package/src/utils/omitStyle.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Component,
|
|
3
|
+
StyleVariant,
|
|
4
|
+
} from '@nordcraft/core/dist/component/component.types'
|
|
2
5
|
|
|
3
6
|
export function omitSubnodeStyleForComponent<T extends Component | undefined>(
|
|
4
7
|
component: T,
|
|
@@ -10,8 +13,13 @@ export function omitSubnodeStyleForComponent<T extends Component | undefined>(
|
|
|
10
13
|
nodeId !== 'root'
|
|
11
14
|
) {
|
|
12
15
|
delete node.style
|
|
13
|
-
delete node.variants
|
|
14
16
|
delete node.animations
|
|
17
|
+
node.variants = node.variants?.map(
|
|
18
|
+
({ customProperties }) =>
|
|
19
|
+
({
|
|
20
|
+
customProperties,
|
|
21
|
+
}) as StyleVariant,
|
|
22
|
+
)
|
|
15
23
|
}
|
|
16
24
|
})
|
|
17
25
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { StyleVariant } from '@nordcraft/core/dist/component/component.types'
|
|
2
|
+
import type { Signal } from '../signal/signal'
|
|
3
|
+
|
|
4
|
+
import { CUSTOM_PROPERTIES_STYLESHEET_ID } from '@nordcraft/core/dist/styling/theme.const'
|
|
5
|
+
import { CustomPropertyStyleSheet } from '../styles/CustomPropertyStyleSheet'
|
|
6
|
+
|
|
7
|
+
const CUSTOM_PROPERTIES_STYLESHEET = new CustomPropertyStyleSheet(
|
|
8
|
+
(
|
|
9
|
+
document.getElementById(CUSTOM_PROPERTIES_STYLESHEET_ID) as
|
|
10
|
+
| HTMLStyleElement
|
|
11
|
+
| undefined
|
|
12
|
+
)?.sheet,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export function subscribeCustomProperty({
|
|
16
|
+
selector,
|
|
17
|
+
customPropertyName,
|
|
18
|
+
signal,
|
|
19
|
+
variant,
|
|
20
|
+
}: {
|
|
21
|
+
selector: string
|
|
22
|
+
customPropertyName: string
|
|
23
|
+
signal: Signal<string>
|
|
24
|
+
variant?: StyleVariant
|
|
25
|
+
}) {
|
|
26
|
+
signal.subscribe(
|
|
27
|
+
CUSTOM_PROPERTIES_STYLESHEET.registerProperty(
|
|
28
|
+
selector,
|
|
29
|
+
customPropertyName,
|
|
30
|
+
variant,
|
|
31
|
+
),
|
|
32
|
+
{
|
|
33
|
+
destroy: () =>
|
|
34
|
+
CUSTOM_PROPERTIES_STYLESHEET.unregisterProperty(
|
|
35
|
+
selector,
|
|
36
|
+
customPropertyName,
|
|
37
|
+
variant,
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
}
|