@nordcraft/runtime 1.0.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.
Files changed (168) hide show
  1. package/README.md +5 -0
  2. package/dist/api/createAPI.d.ts +20 -0
  3. package/dist/api/createAPI.js +319 -0
  4. package/dist/api/createAPI.js.map +1 -0
  5. package/dist/api/createAPIv2.d.ts +7 -0
  6. package/dist/api/createAPIv2.js +686 -0
  7. package/dist/api/createAPIv2.js.map +1 -0
  8. package/dist/components/createComponent.d.ts +13 -0
  9. package/dist/components/createComponent.js +216 -0
  10. package/dist/components/createComponent.js.map +1 -0
  11. package/dist/components/createElement.d.ts +3 -0
  12. package/dist/components/createElement.js +208 -0
  13. package/dist/components/createElement.js.map +1 -0
  14. package/dist/components/createNode.d.ts +22 -0
  15. package/dist/components/createNode.js +272 -0
  16. package/dist/components/createNode.js.map +1 -0
  17. package/dist/components/createSlot.d.ts +3 -0
  18. package/dist/components/createSlot.js +49 -0
  19. package/dist/components/createSlot.js.map +1 -0
  20. package/dist/components/createText.d.ts +23 -0
  21. package/dist/components/createText.js +68 -0
  22. package/dist/components/createText.js.map +1 -0
  23. package/dist/components/createText.test.d.ts +1 -0
  24. package/dist/components/createText.test.js +113 -0
  25. package/dist/components/createText.test.js.map +1 -0
  26. package/dist/components/renderComponent.d.ts +34 -0
  27. package/dist/components/renderComponent.js +66 -0
  28. package/dist/components/renderComponent.js.map +1 -0
  29. package/dist/context/isContextProvider.d.ts +2 -0
  30. package/dist/context/isContextProvider.js +5 -0
  31. package/dist/context/isContextProvider.js.map +1 -0
  32. package/dist/context/subscribeToContext.d.ts +4 -0
  33. package/dist/context/subscribeToContext.js +93 -0
  34. package/dist/context/subscribeToContext.js.map +1 -0
  35. package/dist/custom-components/components.d.ts +1 -0
  36. package/dist/custom-components/components.js +2 -0
  37. package/dist/custom-components/components.js.map +1 -0
  38. package/dist/custom-components/toddle-portal.d.ts +6 -0
  39. package/dist/custom-components/toddle-portal.js +20 -0
  40. package/dist/custom-components/toddle-portal.js.map +1 -0
  41. package/dist/custom-element/ToddleComponent.d.ts +37 -0
  42. package/dist/custom-element/ToddleComponent.js +244 -0
  43. package/dist/custom-element/ToddleComponent.js.map +1 -0
  44. package/dist/custom-element/defineComponents.d.ts +26 -0
  45. package/dist/custom-element/defineComponents.js +42 -0
  46. package/dist/custom-element/defineComponents.js.map +1 -0
  47. package/dist/custom-element.main.d.ts +3 -0
  48. package/dist/custom-element.main.esm.js +266 -0
  49. package/dist/custom-element.main.esm.js.map +7 -0
  50. package/dist/custom-element.main.js +14 -0
  51. package/dist/custom-element.main.js.map +1 -0
  52. package/dist/debug/logState.d.ts +4 -0
  53. package/dist/debug/logState.js +19 -0
  54. package/dist/debug/logState.js.map +1 -0
  55. package/dist/editor/drag-drop/dragEnded.d.ts +2 -0
  56. package/dist/editor/drag-drop/dragEnded.js +56 -0
  57. package/dist/editor/drag-drop/dragEnded.js.map +1 -0
  58. package/dist/editor/drag-drop/dragMove.d.ts +3 -0
  59. package/dist/editor/drag-drop/dragMove.js +74 -0
  60. package/dist/editor/drag-drop/dragMove.js.map +1 -0
  61. package/dist/editor/drag-drop/dragReorder.d.ts +3 -0
  62. package/dist/editor/drag-drop/dragReorder.js +92 -0
  63. package/dist/editor/drag-drop/dragReorder.js.map +1 -0
  64. package/dist/editor/drag-drop/dragStarted.d.ts +9 -0
  65. package/dist/editor/drag-drop/dragStarted.js +100 -0
  66. package/dist/editor/drag-drop/dragStarted.js.map +1 -0
  67. package/dist/editor/drag-drop/dropHighlight.d.ts +16 -0
  68. package/dist/editor/drag-drop/dropHighlight.js +50 -0
  69. package/dist/editor/drag-drop/dropHighlight.js.map +1 -0
  70. package/dist/editor/drag-drop/getInsertAreas.d.ts +20 -0
  71. package/dist/editor/drag-drop/getInsertAreas.js +220 -0
  72. package/dist/editor/drag-drop/getInsertAreas.js.map +1 -0
  73. package/dist/editor-preview.main.d.ts +19 -0
  74. package/dist/editor-preview.main.js +1303 -0
  75. package/dist/editor-preview.main.js.map +1 -0
  76. package/dist/events/handleAction.d.ts +3 -0
  77. package/dist/events/handleAction.js +307 -0
  78. package/dist/events/handleAction.js.map +1 -0
  79. package/dist/page.main.d.ts +7 -0
  80. package/dist/page.main.esm.js +8 -0
  81. package/dist/page.main.esm.js.map +7 -0
  82. package/dist/page.main.js +395 -0
  83. package/dist/page.main.js.map +1 -0
  84. package/dist/signal/signal.d.ts +19 -0
  85. package/dist/signal/signal.js +65 -0
  86. package/dist/signal/signal.js.map +1 -0
  87. package/dist/styles/style.d.ts +4 -0
  88. package/dist/styles/style.js +196 -0
  89. package/dist/styles/style.js.map +1 -0
  90. package/dist/utils/BatchQueue.d.ts +10 -0
  91. package/dist/utils/BatchQueue.js +25 -0
  92. package/dist/utils/BatchQueue.js.map +1 -0
  93. package/dist/utils/createFormulaCache.d.ts +3 -0
  94. package/dist/utils/createFormulaCache.js +81 -0
  95. package/dist/utils/createFormulaCache.js.map +1 -0
  96. package/dist/utils/findNearestLine.d.ts +13 -0
  97. package/dist/utils/findNearestLine.js +74 -0
  98. package/dist/utils/findNearestLine.js.map +1 -0
  99. package/dist/utils/findNearestLine.test.d.ts +1 -0
  100. package/dist/utils/findNearestLine.test.js +59 -0
  101. package/dist/utils/findNearestLine.test.js.map +1 -0
  102. package/dist/utils/getDragData.d.ts +1 -0
  103. package/dist/utils/getDragData.js +10 -0
  104. package/dist/utils/getDragData.js.map +1 -0
  105. package/dist/utils/getElementTagName.d.ts +3 -0
  106. package/dist/utils/getElementTagName.js +7 -0
  107. package/dist/utils/getElementTagName.js.map +1 -0
  108. package/dist/utils/nodes.d.ts +21 -0
  109. package/dist/utils/nodes.js +89 -0
  110. package/dist/utils/nodes.js.map +1 -0
  111. package/dist/utils/omitStyle.d.ts +2 -0
  112. package/dist/utils/omitStyle.js +13 -0
  113. package/dist/utils/omitStyle.js.map +1 -0
  114. package/dist/utils/rectHasPoint.d.ts +2 -0
  115. package/dist/utils/rectHasPoint.js +4 -0
  116. package/dist/utils/rectHasPoint.js.map +1 -0
  117. package/dist/utils/setAttribute.d.ts +4 -0
  118. package/dist/utils/setAttribute.js +57 -0
  119. package/dist/utils/setAttribute.js.map +1 -0
  120. package/dist/utils/tryStartViewTransition.d.ts +5 -0
  121. package/dist/utils/tryStartViewTransition.js +14 -0
  122. package/dist/utils/tryStartViewTransition.js.map +1 -0
  123. package/dist/utils/url.d.ts +2 -0
  124. package/dist/utils/url.js +36 -0
  125. package/dist/utils/url.js.map +1 -0
  126. package/package.json +25 -0
  127. package/src/api/createAPI.ts +375 -0
  128. package/src/api/createAPIv2.ts +931 -0
  129. package/src/components/createComponent.ts +280 -0
  130. package/src/components/createElement.ts +240 -0
  131. package/src/components/createNode.ts +381 -0
  132. package/src/components/createSlot.ts +61 -0
  133. package/src/components/createText.test.ts +117 -0
  134. package/src/components/createText.ts +104 -0
  135. package/src/components/renderComponent.ts +145 -0
  136. package/src/context/isContextProvider.ts +12 -0
  137. package/src/context/subscribeToContext.ts +135 -0
  138. package/src/custom-components/components.ts +1 -0
  139. package/src/custom-components/toddle-portal.ts +19 -0
  140. package/src/custom-element/ToddleComponent.ts +315 -0
  141. package/src/custom-element/defineComponents.ts +65 -0
  142. package/src/custom-element.main.ts +24 -0
  143. package/src/debug/logState.ts +30 -0
  144. package/src/editor/drag-drop/dragEnded.ts +75 -0
  145. package/src/editor/drag-drop/dragMove.ts +95 -0
  146. package/src/editor/drag-drop/dragReorder.ts +137 -0
  147. package/src/editor/drag-drop/dragStarted.ts +145 -0
  148. package/src/editor/drag-drop/dropHighlight.ts +82 -0
  149. package/src/editor/drag-drop/getInsertAreas.ts +235 -0
  150. package/src/editor/types.d.ts +36 -0
  151. package/src/editor-preview.main.ts +1782 -0
  152. package/src/events/handleAction.ts +387 -0
  153. package/src/page.main.ts +489 -0
  154. package/src/signal/signal.ts +74 -0
  155. package/src/styles/style.ts +254 -0
  156. package/src/types.d.ts +93 -0
  157. package/src/utils/BatchQueue.ts +24 -0
  158. package/src/utils/createFormulaCache.ts +96 -0
  159. package/src/utils/findNearestLine.test.ts +65 -0
  160. package/src/utils/findNearestLine.ts +92 -0
  161. package/src/utils/getDragData.ts +11 -0
  162. package/src/utils/getElementTagName.ts +14 -0
  163. package/src/utils/nodes.ts +125 -0
  164. package/src/utils/omitStyle.ts +19 -0
  165. package/src/utils/rectHasPoint.ts +5 -0
  166. package/src/utils/setAttribute.ts +56 -0
  167. package/src/utils/tryStartViewTransition.ts +32 -0
  168. package/src/utils/url.ts +45 -0
@@ -0,0 +1,254 @@
1
+ import type {
2
+ Component,
3
+ ComponentNodeModel,
4
+ ElementNodeModel,
5
+ NodeStyleModel,
6
+ } from '@nordcraft/core/dist/component/component.types'
7
+ import {
8
+ getClassName,
9
+ toValidClassName,
10
+ } from '@nordcraft/core/dist/styling/className'
11
+ import { kebabCase } from '@nordcraft/core/dist/styling/style.css'
12
+ import { variantSelector } from '@nordcraft/core/dist/styling/variantSelector'
13
+ import { omitKeys } from '@nordcraft/core/dist/utils/collections'
14
+
15
+ const LEGACY_BREAKPOINTS = {
16
+ large: 1440,
17
+ small: 576,
18
+ medium: 960,
19
+ }
20
+
21
+ export const SIZE_PROPERTIES = new Set([
22
+ 'width',
23
+ 'min-width',
24
+ 'max-width',
25
+ 'height',
26
+ 'min-height',
27
+ 'max-height',
28
+ 'margin',
29
+ 'margin-top',
30
+ 'margin-left',
31
+ 'margin-bottom',
32
+ 'margin-right',
33
+ 'padding',
34
+ 'padding-top',
35
+ 'padding-left',
36
+ 'padding-bottom',
37
+ 'padding-right',
38
+ 'gap',
39
+ 'gap-x',
40
+ 'gap-y',
41
+ 'border-radius',
42
+ 'border-bottom-left-radius',
43
+ 'border-bottom-right-radius',
44
+ 'border-top-left-radius',
45
+ 'border-top-right-radius',
46
+ 'border-width',
47
+ 'border-top-width',
48
+ 'border-left-width',
49
+ 'border-bottom-width',
50
+ 'border-right-width',
51
+ 'font-size',
52
+ 'left',
53
+ 'right',
54
+ 'top',
55
+ 'bottom',
56
+ 'outline-width',
57
+ ])
58
+
59
+ export const insertStyles = (
60
+ parent: HTMLElement,
61
+ root: Component,
62
+ components: Component[],
63
+ ) => {
64
+ const getNodeStyles = (
65
+ node: ElementNodeModel | ComponentNodeModel,
66
+ classHash: string,
67
+ ) => {
68
+ const style = omitKeys(node.style ?? {}, [
69
+ 'variants',
70
+ 'breakpoints',
71
+ 'mediaQuery',
72
+ 'shadows',
73
+ ])
74
+
75
+ const styleElem = document.createElement('style')
76
+ styleElem.setAttribute('data-hash', classHash)
77
+ styleElem.appendChild(
78
+ document.createTextNode(`
79
+ ${renderVariant('.' + classHash, style)}
80
+
81
+ ${
82
+ node.variants
83
+ ? node.variants
84
+ .map((variant) => {
85
+ const selector = `.${classHash}${variantSelector(variant)}`
86
+ const renderedVariant = renderVariant(selector, variant.style, {
87
+ startingStyle: variant.startingStyle,
88
+ })
89
+ return variant.mediaQuery
90
+ ? `
91
+ @media (${Object.entries(variant.mediaQuery)
92
+ .map(([key, value]) => `${key}: ${value}`)
93
+ .filter(Boolean)
94
+ .join(') and (')}) {
95
+ ${renderedVariant}
96
+ }
97
+ `
98
+ : variant.breakpoint
99
+ ? `
100
+ @media (min-width: ${
101
+ LEGACY_BREAKPOINTS[variant.breakpoint]
102
+ }px) {
103
+ ${renderedVariant}
104
+ }
105
+ `
106
+ : renderedVariant
107
+ })
108
+ .join('\n')
109
+ : ''
110
+ }
111
+
112
+ ${
113
+ node.animations
114
+ ? Object.entries(node.animations)
115
+ .map(([animationName, keyframes]) => {
116
+ return `
117
+ @keyframes ${animationName} {
118
+ ${Object.values(keyframes)
119
+ .sort((a, b) => a.position - b.position)
120
+ .map(({ key, position, value }) => {
121
+ return `
122
+ ${position * 100}% {
123
+ ${key}: ${value};
124
+ }
125
+ `
126
+ })
127
+ .join('\n')}
128
+ }
129
+ `
130
+ })
131
+ .join('\n')
132
+ : ''
133
+ }
134
+ `),
135
+ )
136
+ return styleElem
137
+ }
138
+
139
+ // Make sure that CSS for dependant components are rendered first so that instance styles will override.
140
+ const visitedComponents = new Set<string>()
141
+ const newStyles = new Map<string, Element>()
142
+ function insertComponentStyles(
143
+ component: Component,
144
+ package_name: string | undefined,
145
+ ): string | undefined {
146
+ if (visitedComponents.has(component.name)) {
147
+ return
148
+ }
149
+ visitedComponents.add(component.name)
150
+ if (!component.nodes) {
151
+ // eslint-disable-next-line no-console
152
+ console.warn('Unable to find nodes for component', component.name)
153
+ return
154
+ }
155
+ Object.entries(component.nodes).forEach(([id, node]) => {
156
+ if (node.type === 'component') {
157
+ const childComponent = components.find(
158
+ (c) =>
159
+ c.name ===
160
+ [node.package ?? package_name, node.name]
161
+ ?.filter((n) => n)
162
+ .join('/'),
163
+ )
164
+ if (childComponent) {
165
+ insertComponentStyles(childComponent, node.package ?? package_name)
166
+
167
+ const instanceClassHash = toValidClassName(
168
+ `${component.name}:${id}`,
169
+ true,
170
+ )
171
+ newStyles.set(
172
+ instanceClassHash,
173
+ getNodeStyles(node, instanceClassHash),
174
+ )
175
+ }
176
+ } else if (node.type === 'element') {
177
+ const classHash = getClassName([node.style, node.variants])
178
+ newStyles.set(classHash, getNodeStyles(node, classHash))
179
+ }
180
+ })
181
+ }
182
+
183
+ insertComponentStyles(root, undefined)
184
+
185
+ // Remove old styles.
186
+ // We do not keep track of changes, so must remove all and re-add as order matters.
187
+ parent.querySelectorAll('[data-hash]').forEach((node) => node.remove())
188
+
189
+ // Add new styles
190
+ const fragment = document.createDocumentFragment()
191
+ newStyles.forEach((style) => {
192
+ fragment.appendChild(style)
193
+ })
194
+ parent.appendChild(fragment)
195
+ }
196
+
197
+ const renderVariant = (
198
+ selector: string,
199
+ style: NodeStyleModel,
200
+ options?: {
201
+ startingStyle?: boolean
202
+ },
203
+ ) => {
204
+ const scrollbarStyles = Object.entries(style).filter(
205
+ ([key]) => key === 'scrollbar-width',
206
+ )
207
+
208
+ let styles = styleToCss(style)
209
+ if (options?.startingStyle) {
210
+ styles = `@starting-style {
211
+ ${styles}
212
+ }`
213
+ }
214
+
215
+ return `
216
+ ${selector} {
217
+ ${styles}
218
+ }
219
+ ${
220
+ scrollbarStyles.length > 0
221
+ ? `
222
+ ${selector}::-webkit-scrollbar {
223
+ ${scrollbarStyles
224
+ .map(([_, value]) => {
225
+ switch (value) {
226
+ case 'none':
227
+ return 'width: 0;'
228
+ case 'thin':
229
+ return 'width:4px;'
230
+ default:
231
+ return ''
232
+ }
233
+ })
234
+ .join('\n')}
235
+ }
236
+ `
237
+ : ''
238
+ }
239
+ `
240
+ }
241
+
242
+ export const styleToCss = (style: NodeStyleModel) => {
243
+ return Object.entries(style)
244
+ .map(([property, value]) => {
245
+ const propertyName = kebabCase(property)
246
+ const propertyValue =
247
+ String(Number(value)) === String(value) &&
248
+ SIZE_PROPERTIES.has(propertyName)
249
+ ? `${Number(value) * 4}px`
250
+ : value
251
+ return `${propertyName}:${propertyValue};`
252
+ })
253
+ .join('\n')
254
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type {
2
+ Component,
3
+ ComponentData,
4
+ } from '@nordcraft/core/dist/component/component.types'
5
+ import type { ToddleEnv } from '@nordcraft/core/dist/formula/formula'
6
+ import type {
7
+ Toddle as NewToddle,
8
+ Toddle,
9
+ ToddleInternals,
10
+ } from '@nordcraft/core/dist/types'
11
+ import type { Signal } from './signal/signal'
12
+
13
+ declare global {
14
+ interface Window {
15
+ __components: Record<string, Signal<ComponentData>> // used for debugging
16
+ __toddle: ToddleInternals
17
+ toddle: NewToddle<LocationSignal, PreviewShowSignal>
18
+ }
19
+ }
20
+
21
+ export type LocationSignal = Signal<Location>
22
+
23
+ export interface Location {
24
+ route: Component['route']
25
+ page?: string
26
+ path: string
27
+ params: Record<string, string | null>
28
+ query: Record<string, string | string[] | null>
29
+ hash?: string
30
+ }
31
+
32
+ export type PreviewShowSignal = Signal<{
33
+ displayedNodes: string[]
34
+ testMode: boolean
35
+ }>
36
+
37
+ interface ListItem {
38
+ Item: unknown
39
+ Index: number
40
+ Parent?: ListItem
41
+ }
42
+
43
+ export interface ComponentChild {
44
+ dataSignal: Signal<ComponentData>
45
+ id: string
46
+ path: string
47
+ ctx: ComponentContext
48
+ }
49
+
50
+ export interface ComponentContext {
51
+ component: Component
52
+ components: Component[]
53
+ package: string | undefined
54
+ abortSignal: AbortSignal
55
+ root: Document | ShadowRoot
56
+ isRootComponent: boolean
57
+ dataSignal: Signal<ComponentData>
58
+ triggerEvent: (event: string, data: unknown) => void
59
+ apis: Record<string, ContextApi>
60
+ children: Record<string, Array<ComponentChild>>
61
+ formulaCache: Record<
62
+ string,
63
+ {
64
+ get: (data: ComponentData) => { hit: true; data: any } | { hit: false }
65
+ set: (data: ComponentData, result: any) => void
66
+ }
67
+ >
68
+ providers: Record<
69
+ string,
70
+ {
71
+ component: Component
72
+ formulaDataSignals: Record<string, Signal<ComponentData>>
73
+ ctx: ComponentContext
74
+ }
75
+ >
76
+ toddle: Toddle<LocationSignal, PreviewShowSignal>
77
+ env: ToddleEnv
78
+ }
79
+
80
+ export type ContextApi = {
81
+ fetch: Function
82
+ destroy: Function
83
+ update?: Function // for updating the dataSignal (v2 only)
84
+ triggerActions?: Function // for triggering actions explicitly. Useful when initializing apis (v2 only)
85
+ }
86
+
87
+ export type FormulaCache = Record<
88
+ string,
89
+ {
90
+ get: (data: ComponentData) => { hit: true; data: any } | { hit: false }
91
+ set: (data: ComponentData, result: any) => void
92
+ }
93
+ >
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A helper class to batch multiple callbacks and process them in a single update step just before the next frame render, but after the current stack.
3
+ * This is more efficient than processing each callback in a separate requestAnimationFrame due to the overhead.
4
+ */
5
+ export class BatchQueue {
6
+ private batchQueue: Array<() => void> = []
7
+ private isProcessing = false
8
+ private processBatch() {
9
+ if (this.isProcessing) return
10
+ this.isProcessing = true
11
+
12
+ requestAnimationFrame(() => {
13
+ while (this.batchQueue.length > 0) {
14
+ const callback = this.batchQueue.shift()
15
+ callback?.()
16
+ }
17
+ this.isProcessing = false
18
+ })
19
+ }
20
+ public add(callback: () => void) {
21
+ this.batchQueue.push(callback)
22
+ this.processBatch()
23
+ }
24
+ }
@@ -0,0 +1,96 @@
1
+ import type {
2
+ Component,
3
+ ComponentData,
4
+ } from '@nordcraft/core/dist/component/component.types'
5
+ import type {
6
+ Formula,
7
+ FunctionOperation,
8
+ } from '@nordcraft/core/dist/formula/formula'
9
+ import { get, mapObject } from '@nordcraft/core/dist/utils/collections'
10
+ import { isDefined } from '@nordcraft/core/dist/utils/util'
11
+ import type { FormulaCache } from '../types'
12
+
13
+ export function createFormulaCache(component: Component): FormulaCache {
14
+ if (!isDefined(component.formulas)) {
15
+ return {}
16
+ }
17
+ return mapObject(component.formulas, ([name, f]) => {
18
+ const { canCache, keys } = f.memoize
19
+ ? getFormulaCacheConfig(f.formula, component)
20
+ : { canCache: false, keys: [] }
21
+ let cacheInput: any
22
+ let cacheData: any
23
+
24
+ return [
25
+ name,
26
+ {
27
+ get: (data: ComponentData) => {
28
+ if (
29
+ canCache &&
30
+ cacheInput &&
31
+ keys.every((key) => {
32
+ return get(data, key) === get(cacheInput, key)
33
+ })
34
+ ) {
35
+ return { hit: true, data: cacheData }
36
+ }
37
+ return { hit: false }
38
+ },
39
+ set: (data: ComponentData, result: any) => {
40
+ if (canCache) {
41
+ cacheInput = data
42
+ cacheData = result
43
+ }
44
+ },
45
+ },
46
+ ]
47
+ })
48
+ }
49
+
50
+ function getFormulaCacheConfig(formula: Formula, component: Component) {
51
+ const paths: string[][] = []
52
+ function visitOperation(op: Formula) {
53
+ if (!op) {
54
+ return
55
+ }
56
+ if (op.type == 'path' && op.path[0] !== 'Args') {
57
+ paths.push(op.path)
58
+ }
59
+ if (Array.isArray((op as any)?.arguments)) {
60
+ ;(op as FunctionOperation)?.arguments.forEach((arg) =>
61
+ visitOperation(arg.formula),
62
+ )
63
+ }
64
+ if (op.type === 'record' && Array.isArray(op.entries)) {
65
+ op.entries.forEach((arg) => visitOperation(arg.formula))
66
+ }
67
+
68
+ if (op.type === 'apply') {
69
+ if (!component.formulas?.[op.name]?.memoize) {
70
+ throw new Error('Cannot memoize')
71
+ }
72
+ visitOperation(component.formulas?.[op.name]?.formula)
73
+ }
74
+ }
75
+ try {
76
+ visitOperation(formula)
77
+ } catch {
78
+ return {
79
+ canCache: false,
80
+ keys: [],
81
+ }
82
+ }
83
+
84
+ const keys: string[][] = []
85
+ paths
86
+ .sort((a, b) => a.length - b.length)
87
+ .forEach((path) => {
88
+ if (!keys.some((key) => key.every((k, i) => k === path[i]))) {
89
+ keys.push(path)
90
+ }
91
+ })
92
+ return {
93
+ canCache: true,
94
+ keys,
95
+ }
96
+ }
@@ -0,0 +1,65 @@
1
+ import type { Line, Point } from '../editor/types'
2
+ import { findNearestLine } from './findNearestLine'
3
+
4
+ describe('findNearestLine', () => {
5
+ test('should find the nearest line to a point', () => {
6
+ const lines: Line[] = [
7
+ { x1: 0, y1: 0, x2: 0, y2: 1 },
8
+ { x1: 1, y1: 0, x2: 1, y2: 1 },
9
+ { x1: 2, y1: 0, x2: 2, y2: 1 },
10
+ ]
11
+ const point: Point = { x: 1.5, y: 0.5 }
12
+ const nearestLine = findNearestLine(lines, point)?.nearestLine
13
+ expect(nearestLine).toEqual({ x1: 1, y1: 0, x2: 1, y2: 1 })
14
+ })
15
+
16
+ test('should handle point exactly on a line', () => {
17
+ const lines: Line[] = [
18
+ { x1: 0, y1: 0, x2: 2, y2: 0 },
19
+ { x1: 1, y1: -1, x2: 1, y2: 1 },
20
+ ]
21
+ const point: Point = { x: 1, y: 0.5 }
22
+ const nearestLine = findNearestLine(lines, point)?.nearestLine
23
+ expect(nearestLine).toEqual({ x1: 1, y1: -1, x2: 1, y2: 1 })
24
+ })
25
+
26
+ test('should handle empty lines array', () => {
27
+ const lines: Line[] = []
28
+ const point: Point = { x: 0, y: 0 }
29
+ expect(findNearestLine(lines, point).nearestLine).toBeNull()
30
+ })
31
+
32
+ test('should handle lines with zero length (points)', () => {
33
+ const lines: Line[] = [
34
+ { x1: 1, y1: 1, x2: 1, y2: 1 },
35
+ { x1: 2, y1: 2, x2: 3, y2: 3 },
36
+ ]
37
+ const point: Point = { x: 1, y: 1.5 }
38
+ const nearestLine = findNearestLine(lines, point)?.nearestLine
39
+ expect(nearestLine).toEqual({ x1: 1, y1: 1, x2: 1, y2: 1 })
40
+ })
41
+
42
+ test('should handle negative coordinates', () => {
43
+ const lines: Line[] = [
44
+ { x1: -2, y1: -2, x2: -2, y2: 2 },
45
+ { x1: -2, y1: 2, x2: 2, y2: 2 },
46
+ { x1: 2, y1: 2, x2: 2, y2: -2 },
47
+ { x1: 2, y1: -2, x2: -2, y2: -2 },
48
+ ]
49
+ const point: Point = { x: 0, y: 0 }
50
+ const nearestLine = findNearestLine(lines, point)?.nearestLine
51
+ // The point is inside the square; function should return one of the sides
52
+ expect(nearestLine).toEqual({ x1: -2, y1: -2, x2: -2, y2: 2 })
53
+ })
54
+
55
+ test('should handle when multiple lines are equally close', () => {
56
+ const lines: Line[] = [
57
+ { x1: 0, y1: 1, x2: 1, y2: 1 },
58
+ { x1: 0, y1: -1, x2: 1, y2: -1 },
59
+ ]
60
+ const point: Point = { x: 0.5, y: 0 }
61
+ const nearestLine = findNearestLine(lines, point)?.nearestLine
62
+ // Both lines are at distance 1; the function should return the first one
63
+ expect(nearestLine).toEqual({ x1: 0, y1: 1, x2: 1, y2: 1 })
64
+ })
65
+ })
@@ -0,0 +1,92 @@
1
+ import type { Line, Point } from '../editor/types'
2
+
3
+ /**
4
+ * Finds the nearest line to a given point from an array of lines.
5
+ *
6
+ * @param lines - An array of line segments defined by their endpoints.
7
+ * @param point - The point to which the nearest line is to be found.
8
+ * @returns The line segment nearest to the given point.
9
+ */
10
+ export function findNearestLine(
11
+ lines: Line[],
12
+ point: Point,
13
+ ): { nearestLine: Line | null; dist: number; projectionPoint: number } {
14
+ let minDistSquared = Infinity
15
+ let nearestLine: Line | null = null
16
+ let nearestProjectionPoint = 0
17
+
18
+ for (const line of lines) {
19
+ const { distSquared, projectionPoint } = distancePointToSegmentSquared(
20
+ point,
21
+ line,
22
+ )
23
+ if (distSquared < minDistSquared) {
24
+ minDistSquared = distSquared
25
+ nearestLine = line
26
+ nearestProjectionPoint = projectionPoint
27
+ }
28
+ }
29
+
30
+ return {
31
+ nearestLine,
32
+ dist: Math.sqrt(minDistSquared),
33
+ projectionPoint: nearestProjectionPoint,
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Represents where the perpendicular projection of the point onto the infinite line lies relative to the line segment:
39
+ * - If t is 0, the projection is at (x1, y1).
40
+ * - If t is 1, the projection is at (x2, y2).
41
+ * - If t is between 0 and 1, the projection lies somewhere between the two endpoints.
42
+ *
43
+ * (x1, y1) *---o-----------* (x2, y2)
44
+ * | /
45
+ * | /
46
+ * | /
47
+ * | /
48
+ * | /
49
+ * MIN_DIST -> | /
50
+ * | /
51
+ * | /
52
+ * | /
53
+ * | /
54
+ * |/
55
+ * (x, y)
56
+ *
57
+ * Where "o" The length along the line segment where the projection point lies.
58
+ *
59
+ * Notice that the calculation works for diagonal lines as well as horizontal and vertical lines.
60
+ */
61
+ function distancePointToSegmentSquared(point: Point, line: Line) {
62
+ const dx = line.x2 - line.x1
63
+ const dy = line.y2 - line.y1
64
+ const l2 = dx * dx + dy * dy
65
+
66
+ // If l2 is zero, the line segment is actually a point. We return the squared distance between the point and this single point.
67
+ if (l2 === 0) {
68
+ return {
69
+ distSquared:
70
+ (point.x - line.x1) * (point.x - line.x1) +
71
+ (point.y - line.y1) * (point.y - line.y1),
72
+ projectionPoint: 0.5,
73
+ }
74
+ }
75
+
76
+ let projectionPoint =
77
+ ((point.x - line.x1) * dx + (point.y - line.y1) * dy) / l2
78
+
79
+ // Clamp t to [0,1] for the projection point to lie within the finite line segment.
80
+ projectionPoint = Math.max(0, Math.min(1, projectionPoint))
81
+
82
+ const projX = line.x1 + projectionPoint * dx
83
+ const projY = line.y1 + projectionPoint * dy
84
+
85
+ // No need to take the square root, since we are only comparing distances and square root is monotonic.
86
+ return {
87
+ distSquared:
88
+ (point.x - projX) * (point.x - projX) +
89
+ (point.y - projY) * (point.y - projY),
90
+ projectionPoint,
91
+ }
92
+ }
@@ -0,0 +1,11 @@
1
+ export function getDragData(event: Event) {
2
+ if (event instanceof DragEvent) {
3
+ return Array.from(event.dataTransfer?.items ?? []).reduce<
4
+ Record<string, any>
5
+ >((dragData, item) => {
6
+ dragData[item.type] = event.dataTransfer?.getData(item.type)
7
+ return dragData
8
+ }, {})
9
+ }
10
+ return
11
+ }
@@ -0,0 +1,14 @@
1
+ import type { ElementNodeModel } from '@nordcraft/core/dist/component/component.types'
2
+ import type { ComponentContext } from '../types'
3
+
4
+ export function getElementTagName(
5
+ node: ElementNodeModel,
6
+ ctx: ComponentContext,
7
+ id: string,
8
+ ) {
9
+ if (ctx.component.version === 2 && id === 'root') {
10
+ return `${ctx.package ?? ctx.toddle.project}-${node.tag}`
11
+ }
12
+
13
+ return node.tag
14
+ }