@remix-run/ui 0.0.0 → 0.1.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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,404 @@
1
+ import { invariant } from './invariant.ts'
2
+ import type { FrameContext } from './frame.ts'
3
+
4
+ export function diffNodes(curr: Node[], next: Node[], context: FrameContext) {
5
+ let parent = curr[0]?.parentNode ?? context.regionParent ?? null
6
+ invariant(parent, 'Parent node not found')
7
+
8
+ // When diffing a bounded region (e.g. between frame comments), we should insert new
9
+ // nodes before the region tail ref rather than appending to the parent.
10
+ let regionTailRef: ChildNode | null =
11
+ context.regionTailRef ??
12
+ (curr.length > 0 ? (curr[curr.length - 1].nextSibling as ChildNode | null) : null)
13
+
14
+ let max = Math.max(curr.length, next.length)
15
+ for (let i = 0; i < max; i++) {
16
+ let c = curr[i]
17
+ let n = next[i]
18
+
19
+ if (!c && n) {
20
+ if (regionTailRef) {
21
+ parent.insertBefore(n, regionTailRef)
22
+ } else {
23
+ parent.appendChild(n)
24
+ }
25
+ } else if (c && !n) {
26
+ disposeRemovedSubFrames(c, context)
27
+ parent.removeChild(c)
28
+ } else if (c && n) {
29
+ // Skip hydrated client-entry boundary ranges; hydration pass re-renders
30
+ // roots with new props from incoming payload
31
+ if (isVirtualRootStartMarker(c) && isVirtualRootStartMarker(n)) {
32
+ let currentEnd = findHydrationEndMarker(c)
33
+ let nextEnd = findHydrationEndMarker(n)
34
+ let nextData = n.data
35
+ if (c.data !== nextData) c.data = nextData
36
+
37
+ let currentEndIndex = curr.indexOf(currentEnd)
38
+ let nextEndIndex = next.indexOf(nextEnd)
39
+ i = Math.max(currentEndIndex, nextEndIndex)
40
+ continue
41
+ }
42
+
43
+ let cursor = diffNode(c, n, context)
44
+ if (cursor) {
45
+ i = next.indexOf(cursor)
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode | undefined {
52
+ // Text -> Text
53
+ if (isTextNode(current) && isTextNode(next)) {
54
+ let newText = next.textContent || ''
55
+ if (current.textContent !== newText) current.textContent = newText
56
+ return
57
+ }
58
+
59
+ // Hydration boundary -> Hydration boundary
60
+ if (isVirtualRootStartMarker(current) && isVirtualRootStartMarker(next)) {
61
+ let nextData = next.data
62
+ if (current.data !== nextData) {
63
+ current.data = nextData
64
+ }
65
+
66
+ let end = findHydrationEndMarker(next)
67
+ // Fast-forward across this hydrated region.
68
+ return end
69
+ }
70
+
71
+ // Comment -> Comment
72
+ if (isCommentNode(current) && isCommentNode(next)) {
73
+ let newData = next.data
74
+ if (current.data !== newData) current.data = newData
75
+ return
76
+ }
77
+
78
+ // Element -> Element
79
+ if (isElement(current) && isElement(next)) {
80
+ // Different tags: replace
81
+ if (current.tagName !== next.tagName) {
82
+ let parent = current.parentNode
83
+ if (parent) parent.replaceChild(next, current)
84
+ return
85
+ }
86
+
87
+ // Same tag: update attributes then children
88
+ diffElementAttributes(current, next)
89
+ if (shouldPreserveElementChildren(current, next)) return
90
+ diffElementChildren(current, next, context)
91
+ return
92
+ }
93
+
94
+ // Type mismatch: replace
95
+ let parent = current.parentNode
96
+ if (parent) parent.replaceChild(next, current)
97
+ }
98
+
99
+ function diffElementAttributes(current: Element, next: Element): void {
100
+ let prevAttrNames = current.getAttributeNames()
101
+ let nextAttrNames = next.getAttributeNames()
102
+
103
+ let nextNameSet = new Set(nextAttrNames)
104
+
105
+ // Removals
106
+ for (let name of prevAttrNames) {
107
+ if (!nextNameSet.has(name)) {
108
+ if (shouldPreserveLiveAttribute(current, next, name)) continue
109
+ current.removeAttribute(name)
110
+ }
111
+ }
112
+
113
+ // Additions/updates
114
+ for (let name of nextAttrNames) {
115
+ let prevVal = current.getAttribute(name)
116
+ let nextVal = next.getAttribute(name)
117
+ if (prevVal !== nextVal) {
118
+ if (shouldPreserveLiveAttribute(current, next, name)) continue
119
+ current.setAttribute(name, nextVal == null ? '' : String(nextVal))
120
+ }
121
+ }
122
+ }
123
+
124
+ function shouldPreserveLiveAttribute(current: Element, next: Element, name: string): boolean {
125
+ if (name === 'open') {
126
+ if (current instanceof HTMLDetailsElement && next instanceof HTMLDetailsElement) {
127
+ return current.open !== next.open
128
+ }
129
+
130
+ if (current instanceof HTMLDialogElement && next instanceof HTMLDialogElement) {
131
+ return current.open !== next.open
132
+ }
133
+ }
134
+
135
+ if (name === 'checked') {
136
+ if (current instanceof HTMLInputElement && next instanceof HTMLInputElement) {
137
+ return current.checked !== next.checked
138
+ }
139
+ }
140
+
141
+ if (name === 'value') {
142
+ if (
143
+ current instanceof HTMLInputElement &&
144
+ next instanceof HTMLInputElement &&
145
+ shouldPreserveInputValue(current)
146
+ ) {
147
+ return current.value !== next.value
148
+ }
149
+ }
150
+
151
+ if (name === 'selected') {
152
+ if (current instanceof HTMLOptionElement && next instanceof HTMLOptionElement) {
153
+ return current.selected !== next.selected
154
+ }
155
+ }
156
+
157
+ if (name === 'popover') {
158
+ return isPopoverOpen(current) !== isPopoverOpen(next)
159
+ }
160
+
161
+ return false
162
+ }
163
+
164
+ function shouldPreserveElementChildren(current: Element, next: Element): boolean {
165
+ if (current instanceof HTMLTextAreaElement && next instanceof HTMLTextAreaElement) {
166
+ return current.value !== next.value
167
+ }
168
+
169
+ return false
170
+ }
171
+
172
+ function shouldPreserveInputValue(input: HTMLInputElement): boolean {
173
+ return (
174
+ input.type !== 'button' &&
175
+ input.type !== 'checkbox' &&
176
+ input.type !== 'hidden' &&
177
+ input.type !== 'image' &&
178
+ input.type !== 'radio' &&
179
+ input.type !== 'reset' &&
180
+ input.type !== 'submit'
181
+ )
182
+ }
183
+
184
+ function isPopoverOpen(element: Element): boolean {
185
+ try {
186
+ return element.matches(':popover-open')
187
+ } catch {
188
+ return false
189
+ }
190
+ }
191
+
192
+ function diffElementChildren(current: Element, next: Element, context: FrameContext): void {
193
+ let currentChildren = Array.from(current.childNodes)
194
+ let nextChildren = Array.from(next.childNodes)
195
+
196
+ // Keyed map by data-key for current children
197
+ let keyToIndex = new Map<string, number>()
198
+ for (let i = 0; i < currentChildren.length; i++) {
199
+ let node = currentChildren[i]
200
+ if (isElement(node)) {
201
+ let key = node.getAttribute('data-key')
202
+ if (key != null) keyToIndex.set(key, i)
203
+ }
204
+ }
205
+
206
+ let used = new Array<boolean>(currentChildren.length).fill(false)
207
+ let matchIndexForNext = new Array<number>(nextChildren.length).fill(-1)
208
+
209
+ for (let i = 0; i < nextChildren.length; i++) {
210
+ let nextChild = nextChildren[i]
211
+ let matchIndex = -1
212
+
213
+ if (isElement(nextChild)) {
214
+ let key = nextChild.getAttribute('data-key')
215
+ if (key != null && keyToIndex.has(key)) {
216
+ let idx = keyToIndex.get(key)!
217
+ if (!used[idx]) matchIndex = idx
218
+ }
219
+ }
220
+
221
+ if (matchIndex === -1) {
222
+ let candidateIndex = i
223
+ if (
224
+ candidateIndex < currentChildren.length &&
225
+ !used[candidateIndex] &&
226
+ nodeTypesComparable(currentChildren[candidateIndex], nextChild)
227
+ ) {
228
+ matchIndex = candidateIndex
229
+ }
230
+ }
231
+
232
+ if (matchIndex !== -1) used[matchIndex] = true
233
+ matchIndexForNext[i] = matchIndex
234
+ }
235
+
236
+ // Forward pass: update matched, collect committed
237
+ let committed: Array<Node | undefined> = new Array(nextChildren.length)
238
+ for (let i = 0; i < nextChildren.length; i++) {
239
+ let mi = matchIndexForNext[i]
240
+ if (mi !== -1) {
241
+ let curChild = currentChildren[mi]
242
+ let cursor = diffNode(curChild, nextChildren[i], context)
243
+ if (cursor) {
244
+ // Fast-forward across a hydrated virtual root region.
245
+ let nextEndIdx = nextChildren.indexOf(cursor)
246
+ let currEndIdx = findHydrationEndIndex(currentChildren, mi)
247
+
248
+ // Adjacent hydration regions can pre-match into the next region and leave
249
+ // an orphaned `<!-- /rmx:h -->` behind. Clear those stale matches first.
250
+ for (let j = i + 1; j <= nextEndIdx; j++) {
251
+ let matchedIndex = matchIndexForNext[j]
252
+ if (matchedIndex > currEndIdx) {
253
+ used[matchedIndex] = false
254
+ }
255
+ matchIndexForNext[j] = -1
256
+ }
257
+
258
+ // Mark the entire current region as used to avoid removals.
259
+ for (let k = mi; k <= currEndIdx; k++) used[k] = true
260
+
261
+ // Preserve both boundary markers in committed; skip interior in reorder pass.
262
+ committed[i] = curChild // start marker
263
+ committed[nextEndIdx] = currentChildren[currEndIdx] // end marker
264
+ for (let j = i + 1; j < nextEndIdx; j++) committed[j] = undefined
265
+
266
+ // Jump to end of region.
267
+ i = nextEndIdx
268
+ continue
269
+ }
270
+ committed[i] = curChild
271
+ } else {
272
+ committed[i] = nextChildren[i]
273
+ }
274
+ }
275
+
276
+ // Backward pass: reorder via inserts while avoiding redundant moves
277
+ let anchor: Node | undefined = undefined
278
+ for (let i = committed.length - 1; i >= 0; i--) {
279
+ let node = committed[i]
280
+ if (!node) continue
281
+
282
+ // Use only an anchor that is actually a child of the current parent
283
+ let ref = anchor && anchor.parentNode === current ? anchor : null
284
+
285
+ // Do not move hydration boundary markers; keep region stable.
286
+ // If a boundary marker is new, ensure it is inserted before using it as an anchor.
287
+ if (isVirtualRootStartMarker(node) || isVirtualRootEndMarker(node)) {
288
+ if (node.parentNode !== current) {
289
+ current.insertBefore(node, ref)
290
+ }
291
+ anchor = node
292
+ continue
293
+ }
294
+
295
+ if (node.parentNode === current) {
296
+ // Node already in parent: move only if its nextSibling is not the desired ref.
297
+ let targetNext = ref
298
+ let alreadyInPlace =
299
+ (targetNext === null && node.nextSibling === null) || node.nextSibling === targetNext
300
+ if (!alreadyInPlace) {
301
+ current.insertBefore(node, targetNext)
302
+ }
303
+ } else {
304
+ // New node: insert relative to a valid ref or append
305
+ current.insertBefore(node, ref)
306
+ }
307
+
308
+ // Advance anchor only after the node is placed in the correct parent
309
+ if (node.parentNode === current) {
310
+ anchor = node
311
+ }
312
+ }
313
+
314
+ // Remove any current children not used
315
+ for (let i = 0; i < currentChildren.length; i++) {
316
+ if (!used[i]) {
317
+ let nodeToRemove = currentChildren[i]
318
+ disposeRemovedSubFrames(nodeToRemove, context)
319
+ current.removeChild(currentChildren[i])
320
+ }
321
+ }
322
+ }
323
+
324
+ function nodeTypesComparable(a: Node, b: Node): boolean {
325
+ if (isTextNode(a) && isTextNode(b)) return true
326
+ if (isElement(a) && isElement(b)) return a.tagName === b.tagName
327
+ if (isVirtualRootStartMarker(a) && isVirtualRootStartMarker(b)) return true
328
+ if (isVirtualRootEndMarker(a) && isVirtualRootEndMarker(b)) return true
329
+ if (isCommentNode(a) && isCommentNode(b)) return true
330
+ return false
331
+ }
332
+
333
+ function isHydrationEndComment(node: Node): node is Comment {
334
+ return isCommentNode(node) && node.data.trim() === '/rmx:h'
335
+ }
336
+
337
+ function findHydrationEndMarker(start: Comment): Comment {
338
+ let node: Node | null = start.nextSibling
339
+ let depth = 1
340
+
341
+ while (node) {
342
+ if (isCommentNode(node)) {
343
+ if (isVirtualRootStartMarker(node)) depth++
344
+ if (isVirtualRootEndMarker(node)) {
345
+ depth--
346
+ if (depth === 0) return node
347
+ }
348
+ }
349
+ node = node.nextSibling
350
+ }
351
+
352
+ throw new Error('Hydration end marker not found')
353
+ }
354
+
355
+ function findHydrationEndIndex(nodes: Node[], startIdx: number): number {
356
+ for (let j = startIdx + 1; j < nodes.length; j++) {
357
+ if (isHydrationEndComment(nodes[j])) return j
358
+ }
359
+ return startIdx
360
+ }
361
+
362
+ function isTextNode(node: Node): node is Text {
363
+ return node.nodeType === Node.TEXT_NODE
364
+ }
365
+
366
+ function isElement(node: Node): node is Element {
367
+ return node.nodeType === Node.ELEMENT_NODE
368
+ }
369
+
370
+ function isCommentNode(node: Node): node is Comment {
371
+ return node.nodeType === Node.COMMENT_NODE
372
+ }
373
+
374
+ function isFrameStartMarker(node: Node): node is Comment {
375
+ return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
376
+ }
377
+
378
+ function disposeRemovedSubFrames(node: Node, context: FrameContext): void {
379
+ let stack: Node[] = [node]
380
+ while (stack.length > 0) {
381
+ let next = stack.pop()
382
+ if (!next) continue
383
+
384
+ if (isFrameStartMarker(next)) {
385
+ let subFrame = context.frameInstances.get(next)
386
+ if (subFrame) {
387
+ subFrame.dispose()
388
+ context.frameInstances.delete(next)
389
+ }
390
+ }
391
+
392
+ for (let child of Array.from(next.childNodes)) {
393
+ stack.push(child)
394
+ }
395
+ }
396
+ }
397
+
398
+ function isVirtualRootStartMarker(node: Node): node is Comment {
399
+ return isCommentNode(node) && node.data.trim().startsWith('rmx:h:')
400
+ }
401
+
402
+ function isVirtualRootEndMarker(node: Node): node is Comment {
403
+ return isCommentNode(node) && node.data.trim() === '/rmx:h'
404
+ }
@@ -0,0 +1,209 @@
1
+ import { invariant } from './invariant.ts'
2
+ import { createStyleManager, normalizeCssValue } from '../style/index.ts'
3
+ import type { StyleManager } from '../style/index.ts'
4
+ import type { ElementProps } from './jsx.ts'
5
+ import { normalizeSvgAttribute } from './svg-attributes.ts'
6
+
7
+ const SVG_NS = 'http://www.w3.org/2000/svg'
8
+
9
+ let globalStyleManager =
10
+ typeof window !== 'undefined' ? createStyleManager() : (null as unknown as StyleManager)
11
+
12
+ export { type StyleManager }
13
+
14
+ export let defaultStyleManager: StyleManager = globalStyleManager
15
+
16
+ // Preact excludes certain attributes from the property path due to browser quirks
17
+ const ATTRIBUTE_FALLBACK_NAMES = new Set([
18
+ 'width',
19
+ 'height',
20
+ 'href',
21
+ 'list',
22
+ 'form',
23
+ 'tabIndex',
24
+ 'download',
25
+ 'rowSpan',
26
+ 'colSpan',
27
+ 'role',
28
+ 'popover',
29
+ ])
30
+
31
+ // Determine if we should use the property path for a given name.
32
+ // Also acts as a type guard to allow bracket assignment without casts.
33
+ function canUseProperty(
34
+ dom: Element,
35
+ name: string,
36
+ isSvg: boolean,
37
+ ): dom is Element & Record<string, unknown> {
38
+ if (isSvg) return false
39
+ if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false
40
+ return name in dom
41
+ }
42
+
43
+ function isFrameworkProp(name: string): boolean {
44
+ return (
45
+ name === 'children' ||
46
+ name === 'mix' ||
47
+ name === 'key' ||
48
+ name === 'animate' ||
49
+ name === 'innerHTML'
50
+ )
51
+ }
52
+
53
+ // TODO: would rather actually diff el.style object directly instead of writing
54
+ // to the style attribute
55
+ function serializeStyleObject(style: Record<string, unknown>): string {
56
+ let parts: string[] = []
57
+ for (let [key, value] of Object.entries(style)) {
58
+ if (value == null) continue
59
+ if (typeof value === 'boolean') continue
60
+ if (typeof value === 'number' && !Number.isFinite(value)) continue
61
+
62
+ let cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
63
+
64
+ let cssValue = Array.isArray(value)
65
+ ? (value as unknown[]).join(', ')
66
+ : normalizeCssValue(key, value)
67
+
68
+ parts.push(`${cssKey}: ${cssValue};`)
69
+ }
70
+ return parts.join(' ')
71
+ }
72
+
73
+ function normalizePropName(name: string, isSvg: boolean): { ns?: string; attr: string } {
74
+ // aria-/data- pass through
75
+ if (name.startsWith('aria-') || name.startsWith('data-')) return { attr: name }
76
+
77
+ // DOM property -> HTML mappings
78
+ if (name === 'className') return { attr: 'class' }
79
+ if (!isSvg) {
80
+ if (name === 'htmlFor') return { attr: 'for' }
81
+ if (name === 'tabIndex') return { attr: 'tabindex' }
82
+ if (name === 'acceptCharset') return { attr: 'accept-charset' }
83
+ if (name === 'httpEquiv') return { attr: 'http-equiv' }
84
+ return { attr: name.toLowerCase() }
85
+ }
86
+
87
+ return normalizeSvgAttribute(name)
88
+ }
89
+
90
+ function toLocalName(attrName: string): string {
91
+ let separatorIndex = attrName.indexOf(':')
92
+ if (separatorIndex === -1) return attrName
93
+ return attrName.slice(separatorIndex + 1)
94
+ }
95
+
96
+ function clearRuntimePropertyOnRemoval(dom: Element & Record<string, unknown>, name: string): void {
97
+ try {
98
+ if (name === 'value' || name === 'defaultValue') {
99
+ dom[name] = ''
100
+ return
101
+ }
102
+ if (name === 'checked' || name === 'defaultChecked' || name === 'selected') {
103
+ dom[name] = false
104
+ return
105
+ }
106
+ if (name === 'selectedIndex') {
107
+ dom[name] = -1
108
+ }
109
+ } catch {}
110
+ }
111
+
112
+ function getMergedClassName(props: ElementProps): string | undefined {
113
+ let classAttr = typeof props.class === 'string' ? props.class : ''
114
+ let className = typeof props.className === 'string' ? props.className : ''
115
+ let mergedClassName =
116
+ classAttr && className ? `${classAttr} ${className}` : classAttr || className
117
+ return mergedClassName || undefined
118
+ }
119
+
120
+ export function diffHostProps(curr: ElementProps, next: ElementProps, dom: Element) {
121
+ let isSvg = dom.namespaceURI === SVG_NS
122
+ let currClassName = getMergedClassName(curr)
123
+ let nextClassName = getMergedClassName(next)
124
+
125
+ if (currClassName !== nextClassName) {
126
+ if (nextClassName) {
127
+ dom.setAttribute('class', nextClassName)
128
+ } else {
129
+ dom.removeAttribute('class')
130
+ }
131
+ }
132
+
133
+ // Removals
134
+ for (let name in curr) {
135
+ if (isFrameworkProp(name)) continue
136
+ if (name === 'class' || name === 'className') continue
137
+ if (!(name in next) || next[name] == null) {
138
+ // Clear runtime state for form-like props where removing the attribute is not enough.
139
+ if (canUseProperty(dom, name, isSvg)) {
140
+ clearRuntimePropertyOnRemoval(dom, name)
141
+ }
142
+
143
+ let { ns, attr } = normalizePropName(name, isSvg)
144
+ if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
145
+ else dom.removeAttribute(attr)
146
+ }
147
+ }
148
+
149
+ // Additions/updates
150
+ for (let name in next) {
151
+ if (isFrameworkProp(name)) continue
152
+ if (name === 'class' || name === 'className') continue
153
+ let nextValue = next[name]
154
+ if (nextValue == null) continue
155
+ let prevValue = curr[name]
156
+ if (prevValue !== nextValue) {
157
+ let { ns, attr } = normalizePropName(name, isSvg)
158
+
159
+ // Object style: serialize to attribute for now
160
+ if (
161
+ attr === 'style' &&
162
+ typeof nextValue === 'object' &&
163
+ nextValue &&
164
+ !Array.isArray(nextValue)
165
+ ) {
166
+ dom.setAttribute('style', serializeStyleObject(nextValue))
167
+ continue
168
+ }
169
+
170
+ // Prefer property assignment when possible (HTML only, not SVG)
171
+ if (canUseProperty(dom, name, isSvg)) {
172
+ try {
173
+ dom[name] = nextValue == null ? '' : nextValue
174
+ continue
175
+ } catch {}
176
+ }
177
+
178
+ // Attribute path
179
+ if (typeof nextValue === 'function') {
180
+ // Never serialize functions as attribute values
181
+ continue
182
+ }
183
+
184
+ let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')
185
+ if (nextValue != null && (nextValue !== false || isAriaOrData)) {
186
+ // Special-case popover: true => presence only
187
+ let attrValue = name === 'popover' && nextValue === true ? '' : String(nextValue)
188
+ if (ns) dom.setAttributeNS(ns, attr, attrValue)
189
+ else dom.setAttribute(attr, attrValue)
190
+ } else {
191
+ if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
192
+ else dom.removeAttribute(attr)
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Reset the global style state. For testing only - not exported from index.ts.
200
+ */
201
+ export function resetStyleState() {
202
+ invariant(
203
+ typeof window !== 'undefined',
204
+ 'resetStyleState() is only available in a browser environment',
205
+ )
206
+ globalStyleManager.dispose()
207
+ globalStyleManager = createStyleManager()
208
+ defaultStyleManager = globalStyleManager
209
+ }