@nordcraft/runtime 1.0.88 → 1.0.90

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 (66) hide show
  1. package/dist/custom-element.main.esm.js +24 -24
  2. package/dist/custom-element.main.esm.js.map +4 -4
  3. package/dist/page.main.esm.js +3 -3
  4. package/dist/page.main.esm.js.map +4 -4
  5. package/dist/src/components/createComponent.js +37 -36
  6. package/dist/src/components/createComponent.js.map +1 -1
  7. package/dist/src/components/createElement.js +0 -2
  8. package/dist/src/components/createElement.js.map +1 -1
  9. package/dist/src/components/createNode.js +12 -4
  10. package/dist/src/components/createNode.js.map +1 -1
  11. package/dist/src/components/createNode.test.d.ts +1 -0
  12. package/dist/src/components/createNode.test.js +608 -0
  13. package/dist/src/components/createNode.test.js.map +1 -0
  14. package/dist/src/components/meta.d.ts +3 -0
  15. package/dist/src/components/meta.js +18 -0
  16. package/dist/src/components/meta.js.map +1 -0
  17. package/dist/src/components/meta.test.d.ts +1 -0
  18. package/dist/src/components/meta.test.js +80 -0
  19. package/dist/src/components/meta.test.js.map +1 -0
  20. package/dist/src/components/renderComponent.js +2 -4
  21. package/dist/src/components/renderComponent.js.map +1 -1
  22. package/dist/src/editor/postMessageToEditor.d.ts +2 -0
  23. package/dist/src/editor/postMessageToEditor.js +4 -0
  24. package/dist/src/editor/postMessageToEditor.js.map +1 -0
  25. package/dist/src/editor-preview.main.js +12 -13
  26. package/dist/src/editor-preview.main.js.map +1 -1
  27. package/dist/src/page.main.js +46 -59
  28. package/dist/src/page.main.js.map +1 -1
  29. package/dist/src/styles/CustomPropertyStyleSheet.d.ts +0 -1
  30. package/dist/src/styles/CustomPropertyStyleSheet.js +1 -1
  31. package/dist/src/styles/CustomPropertyStyleSheet.js.map +1 -1
  32. package/dist/src/styles/CustomPropertyStyleSheet.test.js +2 -5
  33. package/dist/src/styles/CustomPropertyStyleSheet.test.js.map +1 -1
  34. package/dist/src/utils/BatchQueue.d.ts +1 -0
  35. package/dist/src/utils/BatchQueue.js +2 -1
  36. package/dist/src/utils/BatchQueue.js.map +1 -1
  37. package/dist/src/utils/getComponent.d.ts +5 -0
  38. package/dist/src/utils/getComponent.js +8 -0
  39. package/dist/src/utils/getComponent.js.map +1 -0
  40. package/dist/src/utils/getComponent.test.d.ts +1 -0
  41. package/dist/src/utils/getComponent.test.js +24 -0
  42. package/dist/src/utils/getComponent.test.js.map +1 -0
  43. package/dist/src/utils/markSelectedElement.d.ts +1 -0
  44. package/dist/src/utils/markSelectedElement.js +9 -0
  45. package/dist/src/utils/markSelectedElement.js.map +1 -0
  46. package/dist/src/utils/subscribeCustomProperty.d.ts +3 -3
  47. package/dist/src/utils/subscribeCustomProperty.js +2 -3
  48. package/dist/src/utils/subscribeCustomProperty.js.map +1 -1
  49. package/package.json +3 -3
  50. package/src/components/createComponent.ts +57 -51
  51. package/src/components/createElement.ts +0 -2
  52. package/src/components/createNode.test.ts +686 -0
  53. package/src/components/createNode.ts +17 -6
  54. package/src/components/meta.test.ts +90 -0
  55. package/src/components/meta.ts +23 -0
  56. package/src/components/renderComponent.ts +2 -4
  57. package/src/editor/postMessageToEditor.ts +5 -0
  58. package/src/editor-preview.main.ts +12 -15
  59. package/src/page.main.ts +47 -59
  60. package/src/styles/CustomPropertyStyleSheet.test.ts +2 -7
  61. package/src/styles/CustomPropertyStyleSheet.ts +1 -2
  62. package/src/utils/BatchQueue.ts +2 -2
  63. package/src/utils/getComponent.test.ts +29 -0
  64. package/src/utils/getComponent.ts +15 -0
  65. package/src/utils/markSelectedElement.ts +8 -0
  66. package/src/utils/subscribeCustomProperty.ts +1 -5
@@ -9,6 +9,7 @@ import { toBoolean } from '@nordcraft/core/dist/utils/util'
9
9
  import type { Signal } from '../signal/signal'
10
10
  import { signal } from '../signal/signal'
11
11
  import type { ComponentContext } from '../types'
12
+ import { getComponent } from '../utils/getComponent'
12
13
  import { ensureEfficientOrdering, getNextSiblingElement } from '../utils/nodes'
13
14
  import { createComponent } from './createComponent'
14
15
  import { createElement } from './createElement'
@@ -48,11 +49,13 @@ export function createNode({
48
49
  ...props,
49
50
  }),
50
51
  ]
51
- case 'component':
52
- // eslint-disable-next-line no-case-declarations
53
- const isLocalComponent = ctx.components.some(
54
- (c) => c.name === node.name,
55
- )
52
+ case 'component': {
53
+ const isLocalComponent =
54
+ getComponent(
55
+ node.name,
56
+ ctx.components,
57
+ ctx.env.runtime !== 'preview',
58
+ ) !== undefined
56
59
  return createComponent({
57
60
  node: { ...node, id }, // we need the node id for instance classes
58
61
  ...props,
@@ -63,6 +66,7 @@ export function createNode({
63
66
  },
64
67
  parentElement,
65
68
  })
69
+ }
66
70
  case 'text':
67
71
  return [createText({ ...props, node })]
68
72
  case 'slot':
@@ -168,6 +172,7 @@ export function createNode({
168
172
 
169
173
  function repeat(): ReadonlyArray<Element | Text> {
170
174
  let firstRun = true
175
+ let lifetimeSize = 0
171
176
  let repeatItems = new Map<
172
177
  string | number,
173
178
  {
@@ -291,7 +296,13 @@ export function createNode({
291
296
  node: node!,
292
297
  id,
293
298
  dataSignal: childDataSignal,
294
- path: Key === '0' ? path : `${path}(${Key})`,
299
+ // Note that we use the lifetimeSize to ensure that no two items can ever get the same path.
300
+ // Consider a list [A, B, C]:
301
+ // - Update list to [B]
302
+ // - Update list to [A, C, B]
303
+ // Now C and B would have the same path `(1)` if we only used the index or Key, as B would have kept its reference, but the others would be recreated.
304
+ // With lifetimeSize, the keys would be A(3), B(1), C(4) - all unique.
305
+ path: Key === '0' ? path : `${path}(${++lifetimeSize})`,
295
306
  ctx,
296
307
  namespace,
297
308
  parentElement,
@@ -0,0 +1,90 @@
1
+ import type { MetaEntry } from '@nordcraft/core/dist/component/component.types'
2
+ import {
3
+ functionFormula,
4
+ valueFormula,
5
+ } from '@nordcraft/core/dist/formula/formulaUtils'
6
+ import { describe, expect, test } from 'bun:test'
7
+ import { getDynamicMetaEntries } from './meta'
8
+
9
+ describe('getDynamicMetaEntries', () => {
10
+ test('returns an empty object if meta is undefined or null', () => {
11
+ expect(getDynamicMetaEntries(undefined)).toEqual({})
12
+ expect(getDynamicMetaEntries(null)).toEqual({})
13
+ })
14
+
15
+ test('returns an empty object if meta is an empty object', () => {
16
+ expect(getDynamicMetaEntries({})).toEqual({})
17
+ })
18
+
19
+ test('returns only entries with dynamic content', () => {
20
+ const meta: Record<string, MetaEntry> = {
21
+ 'static-entry': {
22
+ tag: 'meta' as any,
23
+ content: valueFormula('static'),
24
+ },
25
+ 'dynamic-entry': {
26
+ tag: 'meta' as any,
27
+ content: functionFormula('dynamic'),
28
+ },
29
+ }
30
+ const result = getDynamicMetaEntries(meta)
31
+ expect(result).toEqual({ 'dynamic-entry': meta['dynamic-entry'] })
32
+ })
33
+
34
+ test('returns only entries with dynamic attributes', () => {
35
+ const meta: Record<string, MetaEntry> = {
36
+ 'static-entry': {
37
+ tag: 'meta' as any,
38
+ attrs: {
39
+ name: valueFormula('static'),
40
+ },
41
+ },
42
+ 'dynamic-entry': {
43
+ tag: 'meta' as any,
44
+ attrs: {
45
+ name: functionFormula('dynamic'),
46
+ },
47
+ },
48
+ }
49
+ const result = getDynamicMetaEntries(meta)
50
+ expect(result).toEqual({ 'dynamic-entry': meta['dynamic-entry'] })
51
+ })
52
+
53
+ test('returns entry if content is dynamic and attributes are static', () => {
54
+ const meta: Record<string, MetaEntry> = {
55
+ 'dynamic-entry': {
56
+ tag: 'meta' as any,
57
+ content: functionFormula('dynamic'),
58
+ attrs: {
59
+ name: valueFormula('static'),
60
+ },
61
+ },
62
+ }
63
+ const result = getDynamicMetaEntries(meta)
64
+ expect(result).toEqual({ 'dynamic-entry': meta['dynamic-entry'] })
65
+ })
66
+
67
+ test('returns entry if content is missing and one attribute is dynamic', () => {
68
+ const meta: Record<string, MetaEntry> = {
69
+ 'dynamic-entry': {
70
+ tag: 'meta' as any,
71
+ attrs: {
72
+ name: valueFormula('static'),
73
+ property: functionFormula('dynamic'),
74
+ },
75
+ },
76
+ }
77
+ const result = getDynamicMetaEntries(meta)
78
+ expect(result).toEqual({ 'dynamic-entry': meta['dynamic-entry'] })
79
+ })
80
+
81
+ test('handles entries with no content or attributes', () => {
82
+ const meta: Record<string, MetaEntry> = {
83
+ 'empty-entry': {
84
+ tag: 'meta' as any,
85
+ },
86
+ }
87
+ const result = getDynamicMetaEntries(meta)
88
+ expect(result).toEqual({})
89
+ })
90
+ })
@@ -0,0 +1,23 @@
1
+ import type { MetaEntry } from '@nordcraft/core/dist/component/component.types'
2
+ import type { Nullable } from '@nordcraft/core/dist/types'
3
+ import { isDefined } from '@nordcraft/core/dist/utils/util'
4
+
5
+ export const getDynamicMetaEntries = (
6
+ meta?: Nullable<Record<string, MetaEntry>>,
7
+ ): Record<string, MetaEntry> => {
8
+ if (!meta) {
9
+ return {}
10
+ }
11
+ const dynamicMetaEntries: Record<string, MetaEntry> = {}
12
+ for (const key in meta) {
13
+ const entry = meta[key]
14
+ if (isDefined(entry.content) && entry.content.type !== 'value') {
15
+ dynamicMetaEntries[key] = entry
16
+ } else if (
17
+ Object.values(entry.attrs ?? {}).some((a) => a.type !== 'value')
18
+ ) {
19
+ dynamicMetaEntries[key] = entry
20
+ }
21
+ }
22
+ return dynamicMetaEntries
23
+ }
@@ -111,8 +111,7 @@ export function renderComponent({
111
111
  .subscribe((props) => {
112
112
  if (prev) {
113
113
  component.onAttributeChange?.actions?.forEach((action) => {
114
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
115
- handleAction(
114
+ void handleAction(
116
115
  action,
117
116
  dataSignal.get(),
118
117
  ctx,
@@ -143,8 +142,7 @@ export function renderComponent({
143
142
  })
144
143
  }
145
144
  component.onLoad?.actions?.forEach((action) => {
146
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
147
- handleAction(action, dataSignal.get(), ctx)
145
+ void handleAction(action, dataSignal.get(), ctx)
148
146
  })
149
147
  })
150
148
  return rootElem
@@ -0,0 +1,5 @@
1
+ import type { EditorPostMessageType } from './types'
2
+
3
+ export const postMessageToEditor = (message: EditorPostMessageType) => {
4
+ window.parent?.postMessage(message, '*')
5
+ }
@@ -63,10 +63,10 @@ import { introspectApiRequest } from './editor/graphql'
63
63
  import { isInputTarget } from './editor/input'
64
64
  import { updateComponentLinks } from './editor/links'
65
65
  import { getRectData } from './editor/overlay'
66
+ import { postMessageToEditor } from './editor/postMessageToEditor'
66
67
  import {
67
68
  TextNodeComputedStyles,
68
69
  type DragState,
69
- type EditorPostMessageType,
70
70
  type NordcraftPreviewEvent,
71
71
  } from './editor/types'
72
72
  import { handleAction } from './events/handleAction'
@@ -81,6 +81,7 @@ import type {
81
81
  } from './types'
82
82
  import { createFormulaCache } from './utils/createFormulaCache'
83
83
  import { getThemeSignal } from './utils/getThemeSignal'
84
+ import { markSelectedElement } from './utils/markSelectedElement'
84
85
  import { getNodeAndAncestors, isNodeOrAncestorConditional } from './utils/nodes'
85
86
  import { rectHasPoint } from './utils/rectHasPoint'
86
87
  import {
@@ -457,6 +458,7 @@ export const createRoot = (
457
458
  updateConditionalElements()
458
459
 
459
460
  const node = getDOMNodeFromNodeId(selectedNodeId)
461
+ markSelectedElement(node)
460
462
  const element =
461
463
  component?.nodes?.[node?.getAttribute('data-node-id') ?? '']
462
464
  if (
@@ -875,10 +877,12 @@ export const createRoot = (
875
877
  '--editor-timeline-timing-function',
876
878
  )
877
879
  document.body.style.removeProperty('--editor-timeline-fill-mode')
880
+ document.body.removeAttribute('data-animating')
878
881
  update()
879
882
  return
880
883
  }
881
884
 
885
+ document.body.setAttribute('data-animating', 'true')
882
886
  document.body.style.setProperty(
883
887
  '--editor-timeline-position',
884
888
  `${time}s`,
@@ -902,17 +906,13 @@ export const createRoot = (
902
906
  document.head.appendChild(styleTag)
903
907
  }
904
908
 
905
- // Set the animation styles for self and repeated nodes, but pause for all others
906
- // TODO: Consider if we should set all other animations to follow the current timeline time, by setting animation-delay with paused
907
909
  styleTag.innerHTML = `
908
- [data-id] {
909
- animation-play-state: paused !important;
910
- }
911
- [data-id="${animationState.animatedElementId}"], [data-id="${animationState.animatedElementId}"] ~ [data-id^="${animationState.animatedElementId}("] {
910
+ body[data-mode="design"] [data-id="${animationState.animatedElementId}"], body[data-mode="design"] [data-id="${animationState.animatedElementId}"] ~ [data-id^="${animationState.animatedElementId}("] {
912
911
  animation: preview_timeline 1s paused normal !important;
913
912
  animation-fill-mode: var(--editor-timeline-fill-mode) !important;
914
913
  animation-timing-function: var(--editor-timeline-timing-function) !important;
915
914
  animation-delay: calc(0s - var(--editor-timeline-position)) !important;
915
+ animation-play-state: paused !important;
916
916
  }`
917
917
  }
918
918
  })
@@ -1547,6 +1547,7 @@ export const createRoot = (
1547
1547
  scrollStateRestorer((nodeId) =>
1548
1548
  document.querySelector(`[data-id="${nodeId}"]`),
1549
1549
  )
1550
+ markSelectedElement(getDOMNodeFromNodeId(selectedNodeId))
1550
1551
  }
1551
1552
 
1552
1553
  const createContext = (
@@ -1710,7 +1711,7 @@ const insertHeadTags = (
1710
1711
  document.createRange().createContextualFragment(`
1711
1712
  <link
1712
1713
  data-meta-id="${id}"
1713
- ${Object.entries(entry.attrs)
1714
+ ${Object.entries(entry.attrs ?? {})
1714
1715
  .map(([key, value]) => `${key}="${applyFormula(value, context)}"`)
1715
1716
  .join(' ')}
1716
1717
  />
@@ -1722,10 +1723,10 @@ const insertHeadTags = (
1722
1723
  document.createRange().createContextualFragment(`
1723
1724
  <script
1724
1725
  data-meta-id="${id}"
1725
- ${Object.entries(entry.attrs)
1726
+ ${Object.entries(entry.attrs ?? {})
1726
1727
  .map(([key, value]) => `${key}="${applyFormula(value, context)}"`)
1727
1728
  .join(' ')}
1728
- ></script>
1729
+ >${applyFormula(entry.content ?? '', context)}</script>
1729
1730
  `),
1730
1731
  )
1731
1732
  case HeadTagTypes.Style:
@@ -1734,7 +1735,7 @@ const insertHeadTags = (
1734
1735
  document.createRange().createContextualFragment(`
1735
1736
  <style
1736
1737
  data-meta-id="${id}"
1737
- ${Object.entries(entry.attrs)
1738
+ ${Object.entries(entry.attrs ?? {})
1738
1739
  .map(([key, value]) => `${key}="${applyFormula(value, context)}"`)
1739
1740
  .join(' ')}
1740
1741
  >
@@ -1913,10 +1914,6 @@ const registerFormulas = (
1913
1914
  window.toddle.formulas[packageName ?? window.__toddle.project] = formulas
1914
1915
  }
1915
1916
 
1916
- const postMessageToEditor = (message: EditorPostMessageType) => {
1917
- window.parent?.postMessage(message, '*')
1918
- }
1919
-
1920
1917
  let _themeRootSignal = null as Signal<string | null> | null
1921
1918
  function setupThemeSubscription(
1922
1919
  component: Component,
package/src/page.main.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  Toddle,
17
17
  } from '@nordcraft/core/dist/types'
18
18
  import { mapObject } from '@nordcraft/core/dist/utils/collections'
19
+ import { VOID_HTML_ELEMENTS } from '@nordcraft/core/dist/utils/html'
19
20
  import { isDefined } from '@nordcraft/core/dist/utils/util'
20
21
  import * as libActions from '@nordcraft/std-lib/dist/actions'
21
22
  import * as libFormulas from '@nordcraft/std-lib/dist/formulas'
@@ -24,6 +25,7 @@ import { match } from 'path-to-regexp'
24
25
  import { isContextApiV2 } from './api/apiUtils'
25
26
  import { createLegacyAPI } from './api/createAPI'
26
27
  import { createAPI } from './api/createAPIv2'
28
+ import { getDynamicMetaEntries } from './components/meta'
27
29
  import { renderComponent } from './components/renderComponent'
28
30
  import { isContextProvider } from './context/isContextProvider'
29
31
  import { initLogState, registerComponentToLogState } from './debug/logState'
@@ -359,22 +361,21 @@ const setupMetaUpdates = (
359
361
  component: Component,
360
362
  dataSignal: Signal<ComponentData>,
361
363
  ) => {
364
+ const getFormulaContext = (data: ComponentData) => ({
365
+ data,
366
+ component,
367
+ root: document,
368
+ package: undefined,
369
+ toddle: window.toddle,
370
+ env,
371
+ })
362
372
  // Handle dynamic updates of the document language
363
373
  const langFormula = component.route?.info?.language?.formula
364
374
  const dynamicLang = langFormula && langFormula.type !== 'value'
365
375
  if (dynamicLang) {
366
376
  dataSignal
367
- .map<string | null>(() =>
368
- component
369
- ? applyFormula(langFormula, {
370
- data: dataSignal.get(),
371
- component,
372
- root: document,
373
- package: undefined,
374
- toddle: window.toddle,
375
- env,
376
- })
377
- : null,
377
+ .map((data) =>
378
+ component ? applyFormula(langFormula, getFormulaContext(data)) : null,
378
379
  )
379
380
  .subscribe((newLang) => {
380
381
  if (isDefined(newLang) && document.documentElement.lang !== newLang) {
@@ -388,17 +389,8 @@ const setupMetaUpdates = (
388
389
  const dynamicTitle = titleFormula && titleFormula.type !== 'value'
389
390
  if (dynamicTitle) {
390
391
  dataSignal
391
- .map<string | null>(() =>
392
- component
393
- ? applyFormula(titleFormula, {
394
- data: dataSignal.get(),
395
- component,
396
- root: document,
397
- package: undefined,
398
- toddle: window.toddle,
399
- env,
400
- })
401
- : null,
392
+ .map((data) =>
393
+ component ? applyFormula(titleFormula, getFormulaContext(data)) : null,
402
394
  )
403
395
  .subscribe((newTitle) => {
404
396
  if (isDefined(newTitle) && document.title !== newTitle) {
@@ -411,19 +403,19 @@ const setupMetaUpdates = (
411
403
  const meta = component.route?.info?.meta
412
404
  const dynamicDescription =
413
405
  descriptionFormula && descriptionFormula.type !== 'value'
414
- const dynamicMetaFormulas = Object.values(meta ?? {}).some((r) =>
415
- Object.values(
416
- r.attrs ?? {}, // fallback to make sure we don't crash on legacy values
417
- ).some((a) => a.type !== 'value'),
418
- )
419
- if (dynamicDescription || dynamicMetaFormulas) {
406
+ const dynamicMetaFormulas = getDynamicMetaEntries(meta)
407
+ if (dynamicDescription || Object.keys(dynamicMetaFormulas).length > 0) {
420
408
  const findMetaElement = (name: string) =>
421
409
  [...document.getElementsByTagName('meta')].find(
422
410
  (el) => el.name === name || el.getAttribute('property') === name,
423
411
  ) ?? null
424
412
 
425
413
  const updateMetaElement = (
426
- entry: { tag: string; attrs: Record<string, string> },
414
+ entry: {
415
+ tag: string
416
+ attrs: Record<string, string>
417
+ content: string | undefined
418
+ },
427
419
  id?: string,
428
420
  ) => {
429
421
  let existingElement: HTMLElement | null = null
@@ -452,19 +444,18 @@ const setupMetaUpdates = (
452
444
  }
453
445
  existingElement!.setAttribute(key, value)
454
446
  })
447
+ if (
448
+ typeof entry.content === 'string' &&
449
+ !VOID_HTML_ELEMENTS.includes(entry.tag.toLowerCase())
450
+ ) {
451
+ existingElement.textContent = entry.content
452
+ }
455
453
  }
456
454
  if (dynamicDescription) {
457
455
  dataSignal
458
- .map<string | null>((data) =>
456
+ .map((data) =>
459
457
  component
460
- ? applyFormula(descriptionFormula, {
461
- data,
462
- component,
463
- root: document,
464
- package: undefined,
465
- toddle: window.toddle,
466
- env,
467
- })
458
+ ? applyFormula(descriptionFormula, getFormulaContext(data))
468
459
  : null,
469
460
  )
470
461
  .subscribe((newDescription) => {
@@ -501,46 +492,43 @@ const setupMetaUpdates = (
501
492
  property: 'og:description',
502
493
  content: newDescription,
503
494
  },
495
+ content: undefined,
504
496
  })
505
497
  }
506
498
  }
507
499
  })
508
500
  }
509
- if (dynamicMetaFormulas) {
510
- Object.entries(meta ?? {})
511
- // Filter out meta tags that have no dynamic formulas
512
- .filter(([_, entry]) =>
513
- // fallback to make sure we don't crash on legacy values.
514
- Object.values(entry.attrs ?? {}).some((a) => a.type !== 'value'),
515
- )
516
- .forEach(([id, entry]) => {
501
+ if (Object.keys(dynamicMetaFormulas).length > 0) {
502
+ for (const id in dynamicMetaFormulas) {
503
+ const entry = dynamicMetaFormulas[id]
504
+ if (entry) {
517
505
  dataSignal
518
- .map<Record<string, string>>((data) => {
506
+ .map((data) => {
507
+ const context = getFormulaContext(data)
519
508
  // Return the new values for all attributes (we assume they're strings)
520
509
  const values = Object.entries(entry.attrs ?? {}).reduce(
521
510
  (agg, [key, formula]) =>
522
511
  component
523
512
  ? {
524
513
  ...agg,
525
- [key]: applyFormula(formula, {
526
- data,
527
- component,
528
- root: document,
529
- package: undefined,
530
- toddle: window.toddle,
531
- env,
532
- }),
514
+ [key]: applyFormula(formula, context),
533
515
  }
534
516
  : agg,
535
517
  {},
536
518
  )
537
- return values
519
+ return {
520
+ attrs: values,
521
+ content: entry.content
522
+ ? applyFormula(entry.content, context)
523
+ : undefined,
524
+ }
538
525
  })
539
- .subscribe((attrs) =>
526
+ .subscribe(({ attrs, content }) =>
540
527
  // Update the meta tags with the new values
541
- updateMetaElement({ tag: entry.tag, attrs }, id),
528
+ updateMetaElement({ tag: entry.tag, attrs, content }, id),
542
529
  )
543
- })
530
+ }
531
+ }
544
532
  }
545
533
  }
546
534
  }
@@ -121,7 +121,7 @@ describe('CustomPropertyStyleSheet', () => {
121
121
  '.my-class { --my-other-property: 512px; }',
122
122
  )
123
123
  instance.unregisterProperty('.my-class', '--my-other-property')
124
- expect(instance.getStyleSheet().cssRules[0].cssText).toBe('.my-class { }')
124
+ expect(instance.getStyleSheet().cssRules).toBeEmpty()
125
125
  })
126
126
 
127
127
  test('it unregisters a property with media queries', () => {
@@ -149,11 +149,6 @@ describe('CustomPropertyStyleSheet', () => {
149
149
  mediaQuery: { 'max-width': '600px' },
150
150
  },
151
151
  )
152
- expect(instance.getStyleSheet().cssRules[0].cssText).toBe(
153
- `\
154
- @media (max-width: 600px) {
155
- .my-class-with-media { }
156
- }`,
157
- )
152
+ expect(instance.getStyleSheet().cssRules).toBeEmpty()
158
153
  })
159
154
  })
@@ -76,7 +76,6 @@ export class CustomPropertyStyleSheet {
76
76
  options?: Nullable<{
77
77
  mediaQuery?: Nullable<MediaQuery>
78
78
  startingStyle?: Nullable<boolean>
79
- deepClean?: Nullable<boolean>
80
79
  }>,
81
80
  ): void {
82
81
  if (!this.ruleMap) {
@@ -97,7 +96,7 @@ export class CustomPropertyStyleSheet {
97
96
 
98
97
  // Cleaning up empty selectors is probably not necessary in production and may have performance implications.
99
98
  // However, it is required for the editor-preview as it is a dynamic environment and things may get reordered and canvas reused.
100
- if (options?.deepClean && rule.style.length === 0) {
99
+ if (rule.style.length === 0) {
101
100
  this.styleSheet.deleteRule(
102
101
  Array.from(this.ruleMap.keys()).indexOf(fullSelector),
103
102
  )
@@ -3,13 +3,13 @@
3
3
  * This is more efficient than processing each callback in a separate requestAnimationFrame due to the overhead.
4
4
  */
5
5
  export class BatchQueue {
6
+ constructor() {}
6
7
  private batchQueue: Array<() => void> = []
7
8
  private isProcessing = false
8
9
  private processBatch() {
9
10
  if (this.isProcessing) return
10
11
  this.isProcessing = true
11
-
12
- requestAnimationFrame(() => {
12
+ queueMicrotask(() => {
13
13
  while (this.batchQueue.length > 0) {
14
14
  const callback = this.batchQueue.shift()
15
15
  callback?.()
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { getComponent } from './getComponent'
3
+
4
+ describe('getComponent', () => {
5
+ const compA = { name: 'A', value: 1 }
6
+ const compB = { name: 'B', value: 2 }
7
+ const compC = { name: 'C', value: 3 }
8
+
9
+ it('caches components after first call and ignores new list on subsequent calls with useCache=true', () => {
10
+ // First call: cache is built from [compA, compB]
11
+ expect(getComponent('A', [compA, compB])).toBe(compA)
12
+ expect(getComponent('B', [compA, compB])).toBe(compB)
13
+
14
+ // Second call: provide a different list, but cache should still return from the original cache
15
+ expect(getComponent('C', [compC])).toBeUndefined()
16
+ expect(getComponent('A', [compC])).toBe(compA)
17
+ })
18
+
19
+ it('does not use cache when useCache is false', () => {
20
+ // First call: should find compA in the list
21
+ expect(getComponent('A', [compA, compB], false)).toBe(compA)
22
+
23
+ // Second call: provide a different list, should now find compC
24
+ expect(getComponent('C', [compC], false)).toBe(compC)
25
+
26
+ // Should not find compA in the new list
27
+ expect(getComponent('A', [compC], false)).toBeUndefined()
28
+ })
29
+ })
@@ -0,0 +1,15 @@
1
+ import type { Component } from '@nordcraft/core/dist/component/component.types'
2
+
3
+ let componentMap: Map<string, Component> | null
4
+
5
+ /**
6
+ * Project components is not expected to change during runtime, so we can memoize the components in a map for faster lookup.
7
+ */
8
+ export const getComponent = (
9
+ key: string,
10
+ components: Component[],
11
+ useCache = true,
12
+ ) =>
13
+ useCache
14
+ ? (componentMap ??= new Map(components.map((c) => [c.name, c]))).get(key)
15
+ : components.find((c) => c.name === key)
@@ -0,0 +1,8 @@
1
+ export function markSelectedElement(node: Element | null) {
2
+ if (node && !node.hasAttribute('data-selected')) {
3
+ document.querySelectorAll('[data-selected="true"]').forEach((el) => {
4
+ el.removeAttribute('data-selected')
5
+ })
6
+ node.setAttribute('data-selected', 'true')
7
+ }
8
+ }
@@ -2,10 +2,9 @@ import type { Signal } from '../signal/signal'
2
2
 
3
3
  import { CUSTOM_PROPERTIES_STYLESHEET_ID } from '@nordcraft/core/dist/styling/theme.const'
4
4
  import type { StyleVariant } from '@nordcraft/core/dist/styling/variantSelector'
5
- import type { Runtime } from '@nordcraft/core/dist/types'
6
5
  import { CustomPropertyStyleSheet } from '../styles/CustomPropertyStyleSheet'
7
6
 
8
- let customPropertiesStylesheet: CustomPropertyStyleSheet | undefined
7
+ export let customPropertiesStylesheet: CustomPropertyStyleSheet | undefined
9
8
 
10
9
  export function subscribeCustomProperty({
11
10
  selector,
@@ -13,14 +12,12 @@ export function subscribeCustomProperty({
13
12
  signal,
14
13
  variant,
15
14
  root,
16
- runtime,
17
15
  }: {
18
16
  selector: string
19
17
  customPropertyName: string
20
18
  signal: Signal<string>
21
19
  variant?: StyleVariant
22
20
  root: Document | ShadowRoot
23
- runtime: Runtime
24
21
  }) {
25
22
  customPropertiesStylesheet ??= new CustomPropertyStyleSheet(
26
23
  root,
@@ -43,7 +40,6 @@ export function subscribeCustomProperty({
43
40
  selector,
44
41
  customPropertyName,
45
42
  {
46
- deepClean: runtime === 'preview',
47
43
  mediaQuery: variant?.mediaQuery,
48
44
  startingStyle: variant?.startingStyle,
49
45
  },