@nordcraft/runtime 1.0.32 → 1.0.34
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 +44 -0
- package/dist/components/createComponent.js.map +1 -1
- package/dist/components/createElement.js +47 -3
- package/dist/components/createElement.js.map +1 -1
- package/dist/custom-element.main.esm.js +31 -25
- package/dist/custom-element.main.esm.js.map +4 -4
- package/dist/debug/panicScreen.d.ts +6 -0
- package/dist/debug/panicScreen.js +25 -0
- package/dist/debug/panicScreen.js.map +1 -0
- package/dist/debug/sendEditorToast.d.ts +3 -0
- package/dist/debug/sendEditorToast.js +9 -0
- package/dist/debug/sendEditorToast.js.map +1 -0
- package/dist/editor-preview.main.js +84 -29
- 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 +35 -0
- package/dist/styles/CustomPropertyStyleSheet.js +118 -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 +11 -0
- package/dist/utils/subscribeCustomProperty.js +16 -0
- package/dist/utils/subscribeCustomProperty.js.map +1 -0
- package/package.json +3 -3
- package/src/components/createComponent.ts +52 -0
- package/src/components/createElement.ts +61 -3
- package/src/debug/panicScreen.ts +37 -0
- package/src/debug/sendEditorToast.ts +19 -0
- package/src/editor-preview.main.ts +108 -40
- package/src/styles/CustomPropertyStyleSheet.test.ts +104 -0
- package/src/styles/CustomPropertyStyleSheet.ts +180 -0
- package/src/styles/style.ts +33 -0
- package/src/utils/omitStyle.ts +10 -2
- package/src/utils/subscribeCustomProperty.ts +54 -0
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
Component,
|
|
9
9
|
ComponentData,
|
|
10
10
|
MetaEntry,
|
|
11
|
+
StyleVariant,
|
|
11
12
|
} from '@nordcraft/core/dist/component/component.types'
|
|
12
13
|
import { isPageComponent } from '@nordcraft/core/dist/component/isPageComponent'
|
|
13
14
|
import type {
|
|
@@ -37,6 +38,8 @@ import { createLegacyAPI } from './api/createAPI'
|
|
|
37
38
|
import { createAPI } from './api/createAPIv2'
|
|
38
39
|
import { createNode } from './components/createNode'
|
|
39
40
|
import { isContextProvider } from './context/isContextProvider'
|
|
41
|
+
import { createPanicScreen } from './debug/panicScreen'
|
|
42
|
+
import { sendEditorToast } from './debug/sendEditorToast'
|
|
40
43
|
import { dragEnded } from './editor/drag-drop/dragEnded'
|
|
41
44
|
import { dragMove } from './editor/drag-drop/dragMove'
|
|
42
45
|
import { dragReorder } from './editor/drag-drop/dragReorder'
|
|
@@ -565,7 +568,7 @@ export const createRoot = (
|
|
|
565
568
|
}
|
|
566
569
|
const { x, y, type } = message.data
|
|
567
570
|
const elementsAtPoint = document.elementsFromPoint(x, y)
|
|
568
|
-
|
|
571
|
+
const element = elementsAtPoint.find((elem) => {
|
|
569
572
|
const id = elem.getAttribute('data-id')
|
|
570
573
|
if (
|
|
571
574
|
typeof id !== 'string' ||
|
|
@@ -582,31 +585,12 @@ export const createRoot = (
|
|
|
582
585
|
if (elem.getAttribute('data-node-type') === 'text') {
|
|
583
586
|
return (
|
|
584
587
|
// Select text nodes if the meta key is pressed or the text node is double-clicked
|
|
585
|
-
metaKey ||
|
|
586
|
-
type === 'dblclick' ||
|
|
587
|
-
// Select text nodes if the selected node is a text node. This is useful as the user is likely in a text editing mode
|
|
588
|
-
getDOMNodeFromNodeId(selectedNodeId)?.getAttribute(
|
|
589
|
-
'data-node-type',
|
|
590
|
-
) === 'text'
|
|
588
|
+
metaKey || type === 'dblclick'
|
|
591
589
|
)
|
|
592
590
|
}
|
|
593
591
|
return true
|
|
594
592
|
})
|
|
595
593
|
|
|
596
|
-
// Bubble selection to the topmost parent that has the exact same size as the element.
|
|
597
|
-
// This is important for drag and drop as you are often left with childless parents after dragging.
|
|
598
|
-
while (
|
|
599
|
-
element?.parentElement &&
|
|
600
|
-
element.getAttribute('data-node-id') !== 'root' &&
|
|
601
|
-
fastDeepEqual(
|
|
602
|
-
element.getBoundingClientRect().toJSON(),
|
|
603
|
-
element.parentElement.getBoundingClientRect().toJSON(),
|
|
604
|
-
) &&
|
|
605
|
-
element.getAttribute('data-node-type') !== 'text'
|
|
606
|
-
) {
|
|
607
|
-
element = element.parentElement
|
|
608
|
-
}
|
|
609
|
-
|
|
610
594
|
const id = element?.getAttribute('data-id') ?? null
|
|
611
595
|
if (type === 'click' && id !== selectedNodeId) {
|
|
612
596
|
if (message.data.metaKey) {
|
|
@@ -985,10 +969,30 @@ export const createRoot = (
|
|
|
985
969
|
document.head.appendChild(styleTag)
|
|
986
970
|
}
|
|
987
971
|
|
|
972
|
+
// If style variant targets a pseudo-element, apply styles to it instead
|
|
973
|
+
let pseudoElement = ''
|
|
974
|
+
if (component && styleVariantSelection) {
|
|
975
|
+
const nodeLookup = getNodeAndAncestors(
|
|
976
|
+
component,
|
|
977
|
+
component.nodes.root,
|
|
978
|
+
styleVariantSelection.nodeId,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
if (
|
|
982
|
+
nodeLookup?.node.type === 'element' ||
|
|
983
|
+
(nodeLookup?.node.type === 'component' &&
|
|
984
|
+
nodeLookup.node.variants?.[
|
|
985
|
+
styleVariantSelection.styleVariantIndex
|
|
986
|
+
].pseudoElement)
|
|
987
|
+
) {
|
|
988
|
+
pseudoElement = `::${nodeLookup.node.variants?.[styleVariantSelection.styleVariantIndex].pseudoElement}`
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
988
992
|
const previewStyles = Object.entries(previewStyleStyles)
|
|
989
993
|
.map(([key, value]) => `${key}: ${value} !important;`)
|
|
990
994
|
.join('\n')
|
|
991
|
-
styleTag.innerHTML = `[data-id="${selectedNodeId}"], [data-id="${selectedNodeId}"] ~ [data-id^="${selectedNodeId}("] {
|
|
995
|
+
styleTag.innerHTML = `[data-id="${selectedNodeId}"]${pseudoElement}, [data-id="${selectedNodeId}"] ~ [data-id^="${selectedNodeId}("]${pseudoElement} {
|
|
992
996
|
${previewStyles}
|
|
993
997
|
transition: none !important;
|
|
994
998
|
}`
|
|
@@ -1053,19 +1057,49 @@ export const createRoot = (
|
|
|
1053
1057
|
(nodeLookup.node.type === 'element' ||
|
|
1054
1058
|
nodeLookup.node.type === 'component')
|
|
1055
1059
|
) {
|
|
1056
|
-
const selectedStyleVariant =
|
|
1057
|
-
|
|
1058
|
-
|
|
1060
|
+
const selectedStyleVariant =
|
|
1061
|
+
nodeLookup.node.variants?.[
|
|
1062
|
+
styleVariantSelection.styleVariantIndex
|
|
1063
|
+
] ?? ({ style: {} } as StyleVariant)
|
|
1059
1064
|
// Add a style element specific to the selected element which
|
|
1060
1065
|
// is only applied when the preview is in design mode
|
|
1066
|
+
const styleVariantCustomProperties = Object.entries(
|
|
1067
|
+
(selectedStyleVariant as StyleVariant).customProperties ?? {},
|
|
1068
|
+
)
|
|
1069
|
+
.map(([customPropertyName, customProperty]) => ({
|
|
1070
|
+
name: customPropertyName,
|
|
1071
|
+
value: applyFormula(customProperty.formula, {
|
|
1072
|
+
data: {
|
|
1073
|
+
Attributes: dataSignal.get().Attributes,
|
|
1074
|
+
Variables: dataSignal.get().Variables,
|
|
1075
|
+
Contexts: ctxDataSignal?.get().Contexts ?? {},
|
|
1076
|
+
},
|
|
1077
|
+
component: getCurrentComponent(),
|
|
1078
|
+
root: ctx?.root,
|
|
1079
|
+
formulaCache: {},
|
|
1080
|
+
package: ctx?.package,
|
|
1081
|
+
toddle: window.toddle,
|
|
1082
|
+
env,
|
|
1083
|
+
} as FormulaContext),
|
|
1084
|
+
}))
|
|
1085
|
+
.filter(({ value }) => value !== undefined)
|
|
1086
|
+
|
|
1061
1087
|
const styleElem = document.createElement('style')
|
|
1088
|
+
const pseudoElement = selectedStyleVariant.pseudoElement
|
|
1089
|
+
? `::${selectedStyleVariant.pseudoElement}`
|
|
1090
|
+
: ''
|
|
1062
1091
|
styleElem.setAttribute('data-hash', selectedNodeId)
|
|
1063
1092
|
styleElem.appendChild(
|
|
1064
1093
|
document.createTextNode(`
|
|
1065
|
-
body[data-mode="design"] [data-id="${selectedNodeId}"] {
|
|
1094
|
+
body[data-mode="design"] [data-id="${selectedNodeId}"]${pseudoElement} {
|
|
1066
1095
|
${styleToCss({
|
|
1067
|
-
...nodeLookup.node.style,
|
|
1096
|
+
...(!pseudoElement && nodeLookup.node.style),
|
|
1068
1097
|
...selectedStyleVariant.style,
|
|
1098
|
+
...Object.fromEntries(
|
|
1099
|
+
styleVariantCustomProperties.map(
|
|
1100
|
+
({ name, value }) => [name, value],
|
|
1101
|
+
),
|
|
1102
|
+
),
|
|
1069
1103
|
})}
|
|
1070
1104
|
}
|
|
1071
1105
|
`),
|
|
@@ -1379,19 +1413,53 @@ export const createRoot = (
|
|
|
1379
1413
|
// Clear old root signal and create a new one to not keep old signals with previous root around
|
|
1380
1414
|
ctxDataSignal?.destroy()
|
|
1381
1415
|
ctxDataSignal = dataSignal.map((data) => data)
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1416
|
+
try {
|
|
1417
|
+
const rootElem = createNode({
|
|
1418
|
+
id: 'root',
|
|
1419
|
+
path: '0',
|
|
1420
|
+
dataSignal: ctxDataSignal,
|
|
1421
|
+
ctx: newCtx,
|
|
1422
|
+
parentElement: domNode,
|
|
1423
|
+
instance: { [newCtx.component.name]: 'root' },
|
|
1424
|
+
})
|
|
1425
|
+
newCtx.component.onLoad?.actions.forEach((action) => {
|
|
1426
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1427
|
+
handleAction(action, dataSignal.get(), newCtx)
|
|
1428
|
+
})
|
|
1429
|
+
rootElem.forEach((elem) => domNode.appendChild(elem))
|
|
1430
|
+
} catch (error: unknown) {
|
|
1431
|
+
const isPage = isPageComponent(newCtx.component)
|
|
1432
|
+
let name = `Unexpected error while rendering ${isPage ? 'page' : 'component'}`
|
|
1433
|
+
let message = error instanceof Error ? error.message : String(error)
|
|
1434
|
+
let panic = false
|
|
1435
|
+
if (error instanceof RangeError) {
|
|
1436
|
+
// RangeError is unrecoverable
|
|
1437
|
+
panic = true
|
|
1438
|
+
name = 'Infinite loop detected'
|
|
1439
|
+
message =
|
|
1440
|
+
'RangeError (Maximum call stack size exceeded): Remove any circular dependencies or recursive calls. This is most likely caused by components, formulas or actions using themselves without an exit case.'
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Send a toast to the editor with the error
|
|
1444
|
+
sendEditorToast(name, message, {
|
|
1445
|
+
type: 'critical',
|
|
1446
|
+
})
|
|
1447
|
+
|
|
1448
|
+
if (panic) {
|
|
1449
|
+
// Show error overlay in the editor until next update
|
|
1450
|
+
const panicScreen = createPanicScreen({
|
|
1451
|
+
name: name,
|
|
1452
|
+
message,
|
|
1453
|
+
isPage,
|
|
1454
|
+
cause: error,
|
|
1455
|
+
})
|
|
1456
|
+
|
|
1457
|
+
// Replace the inner HTML of the editor preview with the panic screen
|
|
1458
|
+
domNode.innerHTML = ''
|
|
1459
|
+
domNode.appendChild(panicScreen)
|
|
1460
|
+
}
|
|
1461
|
+
console.error(name, message, error)
|
|
1462
|
+
}
|
|
1395
1463
|
window.parent?.postMessage(
|
|
1396
1464
|
{
|
|
1397
1465
|
type: 'style',
|
|
@@ -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(document)
|
|
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(document)
|
|
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(document)
|
|
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(document)
|
|
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(document)
|
|
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(document)
|
|
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(document)
|
|
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,180 @@
|
|
|
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(root: Document | ShadowRoot, styleSheet?: CSSStyleSheet | null) {
|
|
19
|
+
if (styleSheet) {
|
|
20
|
+
this.styleSheet = styleSheet
|
|
21
|
+
} else {
|
|
22
|
+
this.styleSheet = new CSSStyleSheet()
|
|
23
|
+
root.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(
|
|
48
|
+
fullSelector,
|
|
49
|
+
this.styleSheet.cssRules.length,
|
|
50
|
+
)
|
|
51
|
+
let newRule = this.styleSheet.cssRules[ruleIndex]
|
|
52
|
+
|
|
53
|
+
// 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.
|
|
54
|
+
while (
|
|
55
|
+
(newRule as any).cssRules &&
|
|
56
|
+
(newRule as CSSGroupingRule).cssRules.length > 0
|
|
57
|
+
) {
|
|
58
|
+
newRule = (newRule as CSSGroupingRule).cssRules[0]
|
|
59
|
+
}
|
|
60
|
+
rule = newRule as CSSStyleRule | CSSNestedDeclarations
|
|
61
|
+
this.ruleMap.set(fullSelector, rule)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (value: string) => {
|
|
65
|
+
rule.style.setProperty(name, value)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public unregisterProperty(
|
|
70
|
+
selector: string,
|
|
71
|
+
name: string,
|
|
72
|
+
options?: {
|
|
73
|
+
mediaQuery?: MediaQuery
|
|
74
|
+
startingStyle?: boolean
|
|
75
|
+
deepClean?: boolean
|
|
76
|
+
},
|
|
77
|
+
): void {
|
|
78
|
+
if (!this.ruleMap) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fullSelector = CustomPropertyStyleSheet.getFullSelector(
|
|
83
|
+
selector,
|
|
84
|
+
options,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const rule = this.ruleMap.get(fullSelector)
|
|
88
|
+
if (!rule) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rule.style.removeProperty(name)
|
|
93
|
+
|
|
94
|
+
// Cleaning up empty selectors is probably not necessary in production and may have performance implications.
|
|
95
|
+
// However, it is required for the editor-preview as it is a dynamic environment and things may get reordered and canvas reused.
|
|
96
|
+
if (options?.deepClean && rule.style.length === 0) {
|
|
97
|
+
this.styleSheet.deleteRule(
|
|
98
|
+
Array.from(this.ruleMap.keys()).indexOf(fullSelector),
|
|
99
|
+
)
|
|
100
|
+
this.ruleMap.delete(fullSelector)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public getStyleSheet(): CSSStyleSheet {
|
|
105
|
+
return this.styleSheet
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Maps all selectors to their rule index. This is used to map the initial
|
|
110
|
+
* SSR style variable values to their selectors.
|
|
111
|
+
*/
|
|
112
|
+
private hydrateFromBase() {
|
|
113
|
+
const ruleIndex: Map<string, CSSStyleRule> = new Map()
|
|
114
|
+
for (let i = 0; i < this.styleSheet.cssRules.length; i++) {
|
|
115
|
+
let rule = this.styleSheet.cssRules[i]
|
|
116
|
+
const selector = CustomPropertyStyleSheet.selectorFromCSSRule(rule)
|
|
117
|
+
// Get last part of the selector, which is the actual selector we are interested in
|
|
118
|
+
while (
|
|
119
|
+
(rule as CSSGroupingRule).cssRules &&
|
|
120
|
+
(rule as CSSGroupingRule).cssRules.length > 0
|
|
121
|
+
) {
|
|
122
|
+
rule = (rule as CSSGroupingRule).cssRules[0]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ruleIndex.set(selector, rule as CSSStyleRule)
|
|
126
|
+
}
|
|
127
|
+
return ruleIndex
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private static selectorFromCSSRule(rule: CSSRule): string {
|
|
131
|
+
switch (rule.constructor.name) {
|
|
132
|
+
case 'CSSStyleRule':
|
|
133
|
+
// For these rules, we just return (potentially with subrules if any cssRules exist)
|
|
134
|
+
return `${(rule as CSSStyleRule).selectorText} { ${Array.from(
|
|
135
|
+
(rule as CSSStyleRule).cssRules,
|
|
136
|
+
)
|
|
137
|
+
.map(CustomPropertyStyleSheet.selectorFromCSSRule)
|
|
138
|
+
.join(', ')}}`
|
|
139
|
+
case 'CSSStartingStyleRule':
|
|
140
|
+
return `@starting-style { ${Array.from(
|
|
141
|
+
(rule as CSSStartingStyleRule).cssRules,
|
|
142
|
+
)
|
|
143
|
+
.map(CustomPropertyStyleSheet.selectorFromCSSRule)
|
|
144
|
+
.join(', ')}}`
|
|
145
|
+
case 'CSSMediaRule':
|
|
146
|
+
return `@media ${(rule as CSSMediaRule).media.mediaText} { ${Array.from(
|
|
147
|
+
(rule as CSSMediaRule).cssRules,
|
|
148
|
+
)
|
|
149
|
+
.map(CustomPropertyStyleSheet.selectorFromCSSRule)
|
|
150
|
+
.join(', ')}}`
|
|
151
|
+
case 'CSSNestedDeclarations':
|
|
152
|
+
return ''
|
|
153
|
+
default:
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.warn(
|
|
156
|
+
`Unsupported CSS rule type: ${rule.constructor.name}. Returning empty selector.`,
|
|
157
|
+
)
|
|
158
|
+
return ''
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static getFullSelector(
|
|
163
|
+
selector: string,
|
|
164
|
+
options?: {
|
|
165
|
+
mediaQuery?: MediaQuery
|
|
166
|
+
startingStyle?: boolean
|
|
167
|
+
},
|
|
168
|
+
) {
|
|
169
|
+
let result =
|
|
170
|
+
selector + (options?.startingStyle ? ' { @starting-style { }}' : ' { }')
|
|
171
|
+
if (options?.mediaQuery) {
|
|
172
|
+
result = `@media (${Object.entries(options.mediaQuery)
|
|
173
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.join(') and (')}) { ${result}}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
}
|
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,54 @@
|
|
|
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 type { Runtime } from '@nordcraft/core/dist/types'
|
|
6
|
+
import { CustomPropertyStyleSheet } from '../styles/CustomPropertyStyleSheet'
|
|
7
|
+
|
|
8
|
+
let customPropertiesStylesheet: CustomPropertyStyleSheet | undefined
|
|
9
|
+
|
|
10
|
+
export function subscribeCustomProperty({
|
|
11
|
+
selector,
|
|
12
|
+
customPropertyName,
|
|
13
|
+
signal,
|
|
14
|
+
variant,
|
|
15
|
+
root,
|
|
16
|
+
runtime,
|
|
17
|
+
}: {
|
|
18
|
+
selector: string
|
|
19
|
+
customPropertyName: string
|
|
20
|
+
signal: Signal<string>
|
|
21
|
+
variant?: StyleVariant
|
|
22
|
+
root: Document | ShadowRoot
|
|
23
|
+
runtime: Runtime
|
|
24
|
+
}) {
|
|
25
|
+
customPropertiesStylesheet ??= new CustomPropertyStyleSheet(
|
|
26
|
+
root,
|
|
27
|
+
(
|
|
28
|
+
root.getElementById(CUSTOM_PROPERTIES_STYLESHEET_ID) as
|
|
29
|
+
| HTMLStyleElement
|
|
30
|
+
| undefined
|
|
31
|
+
)?.sheet,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
signal.subscribe(
|
|
35
|
+
customPropertiesStylesheet.registerProperty(
|
|
36
|
+
selector,
|
|
37
|
+
customPropertyName,
|
|
38
|
+
variant,
|
|
39
|
+
),
|
|
40
|
+
{
|
|
41
|
+
destroy: () => {
|
|
42
|
+
customPropertiesStylesheet?.unregisterProperty(
|
|
43
|
+
selector,
|
|
44
|
+
customPropertyName,
|
|
45
|
+
{
|
|
46
|
+
deepClean: runtime === 'preview',
|
|
47
|
+
mediaQuery: variant?.mediaQuery,
|
|
48
|
+
startingStyle: variant?.startingStyle,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
}
|