@nordcraft/runtime 1.0.31 → 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.
Files changed (32) hide show
  1. package/dist/components/createComponent.js +40 -0
  2. package/dist/components/createComponent.js.map +1 -1
  3. package/dist/components/createElement.js +39 -3
  4. package/dist/components/createElement.js.map +1 -1
  5. package/dist/custom-element.main.esm.js +34 -28
  6. package/dist/custom-element.main.esm.js.map +4 -4
  7. package/dist/editor-preview.main.js +19 -0
  8. package/dist/editor-preview.main.js.map +1 -1
  9. package/dist/page.main.esm.js +3 -3
  10. package/dist/page.main.esm.js.map +4 -4
  11. package/dist/styles/CustomPropertyStyleSheet.d.ts +34 -0
  12. package/dist/styles/CustomPropertyStyleSheet.js +109 -0
  13. package/dist/styles/CustomPropertyStyleSheet.js.map +1 -0
  14. package/dist/styles/CustomPropertyStyleSheet.test.d.ts +1 -0
  15. package/dist/styles/CustomPropertyStyleSheet.test.js +66 -0
  16. package/dist/styles/CustomPropertyStyleSheet.test.js.map +1 -0
  17. package/dist/styles/style.js +23 -0
  18. package/dist/styles/style.js.map +1 -1
  19. package/dist/utils/omitStyle.js +3 -1
  20. package/dist/utils/omitStyle.js.map +1 -1
  21. package/dist/utils/subscribeCustomProperty.d.ts +8 -0
  22. package/dist/utils/subscribeCustomProperty.js +9 -0
  23. package/dist/utils/subscribeCustomProperty.js.map +1 -0
  24. package/package.json +3 -3
  25. package/src/components/createComponent.ts +48 -0
  26. package/src/components/createElement.ts +52 -3
  27. package/src/editor-preview.main.ts +27 -0
  28. package/src/styles/CustomPropertyStyleSheet.test.ts +104 -0
  29. package/src/styles/CustomPropertyStyleSheet.ts +163 -0
  30. package/src/styles/style.ts +33 -0
  31. package/src/utils/omitStyle.ts +10 -2
  32. 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
+ }
@@ -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
@@ -1,4 +1,7 @@
1
- import type { Component } from '@nordcraft/core/dist/component/component.types'
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
+ }