@remix-run/ui 0.1.1 → 0.2.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 (243) hide show
  1. package/README.md +33 -10
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/animation/spring.d.ts +6 -35
  5. package/dist/animation/spring.js +9 -8
  6. package/dist/animation/spring.js.map +1 -1
  7. package/dist/components/accordion/accordion.d.ts +7 -7
  8. package/dist/components/accordion/accordion.js +0 -2
  9. package/dist/components/accordion/accordion.js.map +1 -1
  10. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/card.demo.js +58 -0
  12. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  15. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  16. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  17. package/dist/components/accordion/demos/overview.demo.js +19 -0
  18. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  19. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  20. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  21. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  22. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  23. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  24. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  25. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  26. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  27. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  28. package/dist/components/button/button.d.ts +86 -4
  29. package/dist/components/button/button.js +63 -3
  30. package/dist/components/button/button.js.map +1 -1
  31. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  32. package/dist/components/button/demos/aliases.demo.js +18 -0
  33. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  34. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  35. package/dist/components/button/demos/basic.demo.js +19 -0
  36. package/dist/components/button/demos/basic.demo.js.map +1 -0
  37. package/dist/components/button/demos/states.demo.d.ts +6 -0
  38. package/dist/components/button/demos/states.demo.js +29 -0
  39. package/dist/components/button/demos/states.demo.js.map +1 -0
  40. package/dist/components/combobox/combobox.d.ts +11 -11
  41. package/dist/components/combobox/combobox.js +0 -2
  42. package/dist/components/combobox/combobox.js.map +1 -1
  43. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  44. package/dist/components/combobox/demos/overview.demo.js +153 -0
  45. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  46. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  47. package/dist/components/listbox/demos/overview.demo.js +64 -0
  48. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  49. package/dist/components/listbox/listbox.d.ts +4 -9
  50. package/dist/components/listbox/listbox.js +21 -33
  51. package/dist/components/listbox/listbox.js.map +1 -1
  52. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  53. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  54. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  55. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  56. package/dist/components/menu/demos/overview.demo.js +32 -0
  57. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  58. package/dist/components/menu/menu.d.ts +15 -24
  59. package/dist/components/menu/menu.js +23 -38
  60. package/dist/components/menu/menu.js.map +1 -1
  61. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  62. package/dist/components/popover/demos/overview.demo.js +64 -0
  63. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  64. package/dist/components/popover/popover.d.ts +5 -5
  65. package/dist/components/popover/popover.js.map +1 -1
  66. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  67. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  68. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  69. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  70. package/dist/components/select/demos/overview.demo.js +27 -0
  71. package/dist/components/select/demos/overview.demo.js.map +1 -0
  72. package/dist/components/select/select.d.ts +11 -11
  73. package/dist/components/select/select.js +17 -26
  74. package/dist/components/select/select.js.map +1 -1
  75. package/dist/components/tabs/tabs.d.ts +6 -6
  76. package/dist/components/tabs/tabs.js +0 -2
  77. package/dist/components/tabs/tabs.js.map +1 -1
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/interactions/keydown/keydown.d.ts +1 -1
  81. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  82. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  83. package/dist/runtime/component.d.ts +18 -12
  84. package/dist/runtime/component.js +93 -80
  85. package/dist/runtime/component.js.map +1 -1
  86. package/dist/runtime/core/attributes.d.ts +14 -0
  87. package/dist/runtime/core/attributes.js +103 -0
  88. package/dist/runtime/core/attributes.js.map +1 -0
  89. package/dist/runtime/core/children.d.ts +7 -0
  90. package/dist/runtime/core/children.js +30 -0
  91. package/dist/runtime/core/children.js.map +1 -0
  92. package/dist/runtime/core/props.d.ts +9 -0
  93. package/dist/runtime/core/props.js +170 -0
  94. package/dist/runtime/core/props.js.map +1 -0
  95. package/dist/runtime/core/vnode.d.ts +3 -0
  96. package/dist/runtime/core/vnode.js +39 -0
  97. package/dist/runtime/core/vnode.js.map +1 -0
  98. package/dist/runtime/create-element.js +2 -1
  99. package/dist/runtime/create-element.js.map +1 -1
  100. package/dist/runtime/diff-dom.js +47 -14
  101. package/dist/runtime/diff-dom.js.map +1 -1
  102. package/dist/runtime/diff-props.d.ts +1 -2
  103. package/dist/runtime/diff-props.js +2 -181
  104. package/dist/runtime/diff-props.js.map +1 -1
  105. package/dist/runtime/dom.d.ts +20 -9
  106. package/dist/runtime/frame.d.ts +23 -2
  107. package/dist/runtime/frame.js +41 -47
  108. package/dist/runtime/frame.js.map +1 -1
  109. package/dist/runtime/jsx.d.ts +1 -1
  110. package/dist/runtime/jsx.js +2 -28
  111. package/dist/runtime/jsx.js.map +1 -1
  112. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  114. package/dist/runtime/mixins/mixin.d.ts +2 -1
  115. package/dist/runtime/mixins/mixin.js +9 -4
  116. package/dist/runtime/mixins/mixin.js.map +1 -1
  117. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  118. package/dist/runtime/mixins/on-mixin.js +9 -2
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  120. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  121. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  122. package/dist/runtime/navigation.js +9 -1
  123. package/dist/runtime/navigation.js.map +1 -1
  124. package/dist/runtime/reconcile.d.ts +1 -1
  125. package/dist/runtime/reconcile.js +452 -197
  126. package/dist/runtime/reconcile.js.map +1 -1
  127. package/dist/runtime/run.d.ts +17 -2
  128. package/dist/runtime/run.js.map +1 -1
  129. package/dist/runtime/scheduler.d.ts +10 -10
  130. package/dist/runtime/scheduler.js +1 -1
  131. package/dist/runtime/scheduler.js.map +1 -1
  132. package/dist/runtime/stream-protocol.d.ts +8 -0
  133. package/dist/runtime/stream-protocol.js +20 -0
  134. package/dist/runtime/stream-protocol.js.map +1 -0
  135. package/dist/runtime/svg-attributes.js +1 -0
  136. package/dist/runtime/svg-attributes.js.map +1 -1
  137. package/dist/runtime/to-vnode.js +7 -11
  138. package/dist/runtime/to-vnode.js.map +1 -1
  139. package/dist/runtime/vnode.d.ts +11 -5
  140. package/dist/runtime/vnode.js +6 -3
  141. package/dist/runtime/vnode.js.map +1 -1
  142. package/dist/server/stream.d.ts +2 -0
  143. package/dist/server/stream.js +154 -114
  144. package/dist/server/stream.js.map +1 -1
  145. package/dist/style/css-mixin.d.ts +2 -1
  146. package/dist/style/css-mixin.js.map +1 -1
  147. package/dist/style/index.d.ts +1 -1
  148. package/dist/style/layers.d.ts +2 -0
  149. package/dist/style/layers.js +3 -0
  150. package/dist/style/layers.js.map +1 -0
  151. package/dist/style/stylesheet.d.ts +13 -11
  152. package/dist/style/stylesheet.js +69 -8
  153. package/dist/style/stylesheet.js.map +1 -1
  154. package/dist/theme/contract.d.ts +24 -222
  155. package/dist/theme/contract.js +2 -2
  156. package/dist/theme/contract.js.map +1 -1
  157. package/dist/theme/glyph-contract.d.ts +1 -3
  158. package/dist/theme/glyph-contract.js.map +1 -1
  159. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  160. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  161. package/dist/theme/runtime.js +4 -1
  162. package/dist/theme/runtime.js.map +1 -1
  163. package/dist/utils/scroll-lock.d.ts +3 -3
  164. package/dist/utils/scroll-lock.js.map +1 -1
  165. package/package.json +3 -3
  166. package/src/animation/README.md +316 -0
  167. package/src/animation/animate-layout-mixin.ts +79 -71
  168. package/src/animation/spring.ts +21 -9
  169. package/src/components/accordion/README.md +166 -0
  170. package/src/components/accordion/accordion.tsx +14 -14
  171. package/src/components/accordion/demos/card.demo.tsx +113 -0
  172. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  173. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  174. package/src/components/anchor/README.md +153 -0
  175. package/src/components/anchor/anchor.ts +1 -1
  176. package/src/components/breadcrumbs/README.md +55 -0
  177. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
  178. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  179. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  180. package/src/components/button/README.md +4 -4
  181. package/src/components/button/button.tsx +118 -20
  182. package/src/components/button/demos/aliases.demo.tsx +27 -0
  183. package/src/components/button/demos/basic.demo.tsx +30 -0
  184. package/src/components/button/demos/states.demo.tsx +52 -0
  185. package/src/components/combobox/README.md +1 -1
  186. package/src/components/combobox/combobox.tsx +37 -21
  187. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  188. package/src/components/glyph/README.md +72 -0
  189. package/src/components/glyph/glyph.tsx +2 -2
  190. package/src/components/listbox/README.md +115 -0
  191. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  192. package/src/components/listbox/listbox.ts +106 -110
  193. package/src/components/menu/README.md +96 -0
  194. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  195. package/src/components/menu/demos/overview.demo.tsx +76 -0
  196. package/src/components/menu/menu.tsx +61 -55
  197. package/src/components/popover/README.md +3 -3
  198. package/src/components/popover/demos/overview.demo.tsx +143 -0
  199. package/src/components/popover/popover.ts +90 -79
  200. package/src/components/select/README.md +107 -0
  201. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  202. package/src/components/select/demos/overview.demo.tsx +51 -0
  203. package/src/components/select/select.tsx +53 -36
  204. package/src/components/tabs/tabs.tsx +10 -5
  205. package/src/index.ts +2 -1
  206. package/src/runtime/component.ts +122 -88
  207. package/src/runtime/core/attributes.ts +118 -0
  208. package/src/runtime/core/children.ts +42 -0
  209. package/src/runtime/core/props.ts +187 -0
  210. package/src/runtime/core/vnode.ts +48 -0
  211. package/src/runtime/create-element.ts +2 -1
  212. package/src/runtime/diff-dom.ts +59 -12
  213. package/src/runtime/diff-props.ts +2 -187
  214. package/src/runtime/dom.ts +20 -9
  215. package/src/runtime/frame.ts +71 -55
  216. package/src/runtime/jsx.ts +3 -30
  217. package/src/runtime/mixins/link-mixin.ts +88 -86
  218. package/src/runtime/mixins/mixin.ts +18 -5
  219. package/src/runtime/mixins/on-mixin.ts +20 -12
  220. package/src/runtime/mixins/ref-mixin.ts +4 -5
  221. package/src/runtime/navigation.ts +10 -1
  222. package/src/runtime/reconcile.ts +563 -223
  223. package/src/runtime/run.ts +17 -1
  224. package/src/runtime/scheduler.ts +20 -20
  225. package/src/runtime/stream-protocol.ts +27 -0
  226. package/src/runtime/svg-attributes.ts +1 -0
  227. package/src/runtime/to-vnode.ts +7 -10
  228. package/src/runtime/vnode.ts +16 -9
  229. package/src/server/README.md +90 -0
  230. package/src/server/stream.ts +176 -124
  231. package/src/style/css-mixin.ts +6 -1
  232. package/src/style/index.ts +1 -2
  233. package/src/style/layers.ts +2 -0
  234. package/src/style/stylesheet.ts +88 -11
  235. package/src/test/README.md +107 -0
  236. package/src/test/utils.ts +3 -1
  237. package/src/theme/README.md +103 -0
  238. package/src/theme/contract.ts +37 -5
  239. package/src/theme/glyph-contract.ts +3 -1
  240. package/src/theme/presets/rmx-01/index.ts +4 -2
  241. package/src/theme/runtime.ts +4 -1
  242. package/src/utils/scroll-lock/README.md +33 -0
  243. package/src/utils/scroll-lock.ts +7 -3
@@ -0,0 +1,118 @@
1
+ import { normalizeCssValue } from '../../style/style.ts'
2
+ import { normalizeSvgAttribute } from '../svg-attributes.ts'
3
+ import type { ElementProps } from '../jsx.ts'
4
+
5
+ const ATTRIBUTE_FALLBACK_NAMES = new Set([
6
+ 'width',
7
+ 'height',
8
+ 'href',
9
+ 'list',
10
+ 'form',
11
+ 'tabIndex',
12
+ 'download',
13
+ 'rowSpan',
14
+ 'colSpan',
15
+ 'role',
16
+ 'popover',
17
+ 'translate',
18
+ ])
19
+
20
+ const BOOLEANISH_STRING_ATTRIBUTES = new Set([
21
+ 'autoReverse',
22
+ 'contenteditable',
23
+ 'draggable',
24
+ 'externalResourcesRequired',
25
+ 'focusable',
26
+ 'preserveAlpha',
27
+ 'spellcheck',
28
+ ])
29
+
30
+ export const FRAMEWORK_PROPS = new Set(['children', 'mix', 'key', 'animate', 'innerHTML', 'on'])
31
+
32
+ export const SELF_CLOSING_TAGS = new Set([
33
+ 'area',
34
+ 'base',
35
+ 'br',
36
+ 'col',
37
+ 'embed',
38
+ 'hr',
39
+ 'img',
40
+ 'input',
41
+ 'link',
42
+ 'meta',
43
+ 'param',
44
+ 'source',
45
+ 'track',
46
+ 'wbr',
47
+ ])
48
+
49
+ export function isChildlessElement(name: string): boolean {
50
+ return SELF_CLOSING_TAGS.has(name)
51
+ }
52
+
53
+ export function canUseProperty(
54
+ element: Element,
55
+ name: string,
56
+ isSvg: boolean,
57
+ attr: string,
58
+ ): element is Element & Record<string, unknown> {
59
+ if (isSvg) return false
60
+ if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false
61
+ if (isBooleanishStringAttribute(attr)) return false
62
+ return name in element
63
+ }
64
+
65
+ export function normalizeAttributeName(
66
+ name: string,
67
+ isSvg: boolean,
68
+ ): { ns?: string; attr: string } {
69
+ if (name.startsWith('aria-') || name.startsWith('data-')) return { attr: name }
70
+
71
+ if (name === 'className') return { attr: 'class' }
72
+
73
+ if (!isSvg) {
74
+ if (name === 'htmlFor') return { attr: 'for' }
75
+ if (name === 'tabIndex') return { attr: 'tabindex' }
76
+ if (name === 'acceptCharset') return { attr: 'accept-charset' }
77
+ if (name === 'httpEquiv') return { attr: 'http-equiv' }
78
+ return { attr: name.toLowerCase() }
79
+ }
80
+
81
+ return normalizeSvgAttribute(name)
82
+ }
83
+
84
+ export function isBooleanishStringAttribute(name: string): boolean {
85
+ return BOOLEANISH_STRING_ATTRIBUTES.has(name)
86
+ }
87
+
88
+ export function shouldStringifyBooleanAttribute(name: string): boolean {
89
+ return isBooleanishStringAttribute(name) || name === 'value'
90
+ }
91
+
92
+ export function serializeStyleObject(style: Record<string, unknown>): string {
93
+ let parts: string[] = []
94
+
95
+ for (let [key, value] of Object.entries(style)) {
96
+ if (value == null) continue
97
+ if (typeof value === 'boolean') continue
98
+ if (typeof value === 'number' && !Number.isFinite(value)) continue
99
+
100
+ let cssKey = toKebabCase(key)
101
+ let cssValue = Array.isArray(value) ? value.join(', ') : normalizeCssValue(key, value)
102
+
103
+ parts.push(`${cssKey}: ${cssValue};`)
104
+ }
105
+
106
+ return parts.join(' ')
107
+ }
108
+
109
+ export function getMergedClassName(props: ElementProps): string | undefined {
110
+ let classAttr = typeof props.class === 'string' ? props.class : ''
111
+ let className = typeof props.className === 'string' ? props.className : ''
112
+ let merged = classAttr && className ? `${classAttr} ${className}` : classAttr || className
113
+ return merged || undefined
114
+ }
115
+
116
+ export function toKebabCase(value: string): string {
117
+ return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
118
+ }
@@ -0,0 +1,42 @@
1
+ import type { RemixNode } from '../jsx.ts'
2
+
3
+ export type EmptyChild = boolean | null | undefined
4
+ export type PrimitiveChild = string | number | bigint
5
+
6
+ export function isEmptyChild(value: RemixNode): value is EmptyChild {
7
+ return value == null || typeof value === 'boolean'
8
+ }
9
+
10
+ export function isPrimitiveChild(value: RemixNode): value is PrimitiveChild {
11
+ let type = typeof value
12
+ return type === 'string' || type === 'number' || type === 'bigint'
13
+ }
14
+
15
+ export function normalizeChildren(children: readonly RemixNode[]): RemixNode[] {
16
+ for (let child of children) {
17
+ if (Array.isArray(child)) {
18
+ return (children as unknown[]).flat(Infinity) as RemixNode[]
19
+ }
20
+ }
21
+
22
+ return children as RemixNode[]
23
+ }
24
+
25
+ export function packChildren(children: readonly RemixNode[]): RemixNode | undefined {
26
+ if (children.length === 0) {
27
+ return undefined
28
+ }
29
+
30
+ if (children.length === 1) {
31
+ let child = children[0]
32
+
33
+ if (child === undefined || isEmptyChild(child)) {
34
+ return undefined
35
+ }
36
+
37
+ return Array.isArray(child) ? normalizeChildren(child) : child
38
+ }
39
+
40
+ let normalized = normalizeChildren(children)
41
+ return normalized.length === 0 ? undefined : normalized
42
+ }
@@ -0,0 +1,187 @@
1
+ import type { ElementProps } from '../jsx.ts'
2
+ import {
3
+ canUseProperty,
4
+ getMergedClassName,
5
+ isBooleanishStringAttribute,
6
+ normalizeAttributeName,
7
+ serializeStyleObject,
8
+ toKebabCase,
9
+ } from './attributes.ts'
10
+ import { normalizeCssValue } from '../../style/style.ts'
11
+
12
+ const SVG_NS = 'http://www.w3.org/2000/svg'
13
+
14
+ function isFrameworkProp(name: string): boolean {
15
+ return (
16
+ name === 'children' ||
17
+ name === 'mix' ||
18
+ name === 'key' ||
19
+ name === 'animate' ||
20
+ name === 'innerHTML' ||
21
+ name === 'on'
22
+ )
23
+ }
24
+
25
+ function toLocalName(attrName: string): string {
26
+ let separatorIndex = attrName.indexOf(':')
27
+ if (separatorIndex === -1) return attrName
28
+ return attrName.slice(separatorIndex + 1)
29
+ }
30
+
31
+ function clearRuntimePropertyOnRemoval(dom: Element & Record<string, unknown>, name: string): void {
32
+ try {
33
+ if (name === 'value' || name === 'defaultValue') {
34
+ dom[name] = ''
35
+ return
36
+ }
37
+ if (name === 'checked' || name === 'defaultChecked' || name === 'selected') {
38
+ dom[name] = false
39
+ return
40
+ }
41
+ if (name === 'selectedIndex') {
42
+ dom[name] = -1
43
+ }
44
+ } catch {}
45
+ }
46
+
47
+ /**
48
+ * Patches DOM attributes and selected properties from old props to new props.
49
+ *
50
+ * @param curr Previous host props.
51
+ * @param next Next host props.
52
+ * @param dom Element to patch.
53
+ */
54
+ export function patchHostProps(curr: ElementProps, next: ElementProps, dom: Element): void {
55
+ let isSvg = dom.namespaceURI === SVG_NS
56
+ let currClassName = getMergedClassName(curr)
57
+ let nextClassName = getMergedClassName(next)
58
+
59
+ if (currClassName !== nextClassName) {
60
+ if (nextClassName) {
61
+ dom.setAttribute('class', nextClassName)
62
+ } else {
63
+ dom.removeAttribute('class')
64
+ }
65
+ }
66
+
67
+ for (let name in curr) {
68
+ if (isFrameworkProp(name)) continue
69
+ if (name === 'class' || name === 'className') continue
70
+ if (name in next && next[name] != null) continue
71
+
72
+ let { ns, attr } = normalizeAttributeName(name, isSvg)
73
+ if (canUseProperty(dom, name, isSvg, attr)) {
74
+ clearRuntimePropertyOnRemoval(dom, name)
75
+ }
76
+
77
+ if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
78
+ else dom.removeAttribute(attr)
79
+ }
80
+
81
+ for (let name in next) {
82
+ if (isFrameworkProp(name)) continue
83
+ if (name === 'class' || name === 'className') continue
84
+
85
+ let nextValue = next[name]
86
+ if (nextValue == null) continue
87
+
88
+ let prevValue = curr[name]
89
+ if (prevValue === nextValue) continue
90
+
91
+ if (name === 'style' && isStyleObject(nextValue)) {
92
+ if (isStyleObject(prevValue)) {
93
+ patchStyleObject(dom, prevValue, nextValue)
94
+ } else {
95
+ dom.removeAttribute('style')
96
+ patchStyleObject(dom, undefined, nextValue)
97
+ }
98
+ continue
99
+ }
100
+
101
+ patchHostProp(dom, name, nextValue, isSvg)
102
+ }
103
+ }
104
+
105
+ function patchHostProp(dom: Element, name: string, value: unknown, isSvg: boolean): void {
106
+ let { ns, attr } = normalizeAttributeName(name, isSvg)
107
+
108
+ if (attr === 'style' && isStyleObject(value)) {
109
+ patchStyleObject(dom, undefined, value)
110
+ return
111
+ }
112
+
113
+ if (attr === 'style' && typeof value === 'string') {
114
+ dom.setAttribute('style', value)
115
+ return
116
+ }
117
+
118
+ if (canUseProperty(dom, name, isSvg, attr)) {
119
+ try {
120
+ dom[name] = value == null ? '' : value
121
+ return
122
+ } catch {}
123
+ }
124
+
125
+ if (typeof value === 'function') return
126
+
127
+ let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')
128
+ let isBooleanishString = isBooleanishStringAttribute(attr)
129
+ if (value != null && (value !== false || isAriaOrData || isBooleanishString)) {
130
+ let attrValue = name === 'popover' && value === true ? '' : String(value)
131
+ if (ns) dom.setAttributeNS(ns, attr, attrValue)
132
+ else dom.setAttribute(attr, attrValue)
133
+ } else {
134
+ if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
135
+ else dom.removeAttribute(attr)
136
+ }
137
+ }
138
+
139
+ function isStyleObject(value: unknown): value is Record<string, unknown> {
140
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
141
+ }
142
+
143
+ function patchStyleObject(
144
+ dom: Element,
145
+ curr: Record<string, unknown> | undefined,
146
+ next: Record<string, unknown>,
147
+ ): void {
148
+ if (!(dom instanceof HTMLElement || dom instanceof SVGElement)) {
149
+ dom.setAttribute('style', serializeStyleObject(next))
150
+ return
151
+ }
152
+
153
+ let style = dom.style
154
+ if (curr) {
155
+ for (let name in curr) {
156
+ let nextCssValue = styleValueToCss(name, next[name])
157
+ if (nextCssValue !== undefined) continue
158
+
159
+ let prevCssValue = styleValueToCss(name, curr[name])
160
+ if (prevCssValue === undefined) continue
161
+
162
+ style.removeProperty(toKebabCase(name))
163
+ }
164
+ }
165
+
166
+ for (let name in next) {
167
+ let nextCssValue = styleValueToCss(name, next[name])
168
+ if (nextCssValue === undefined) continue
169
+
170
+ let prevCssValue = curr ? styleValueToCss(name, curr[name]) : undefined
171
+ if (prevCssValue === nextCssValue) continue
172
+
173
+ style.setProperty(toKebabCase(name), nextCssValue)
174
+ }
175
+ }
176
+
177
+ function styleValueToCss(name: string, value: unknown): string | undefined {
178
+ if (value == null) return undefined
179
+ if (typeof value === 'boolean') return undefined
180
+ if (typeof value === 'number' && !Number.isFinite(value)) return undefined
181
+
182
+ if (Array.isArray(value)) {
183
+ return value.join(', ')
184
+ }
185
+
186
+ return normalizeCssValue(name, value)
187
+ }
@@ -0,0 +1,48 @@
1
+ import type { ElementProps, ElementType, RemixElement, RemixNode } from '../jsx.ts'
2
+
3
+ export function createRemixElement(
4
+ type: ElementType,
5
+ props: ElementProps | null | undefined,
6
+ key?: string,
7
+ ): RemixElement {
8
+ return {
9
+ $rmx: true,
10
+ key,
11
+ props: normalizeElementProps(props),
12
+ type,
13
+ }
14
+ }
15
+
16
+ export function isRemixElement(node: RemixNode): node is RemixElement {
17
+ return typeof node === 'object' && node !== null && '$rmx' in node
18
+ }
19
+
20
+ function normalizeElementProps(props: ElementProps | null | undefined): ElementProps {
21
+ if (!props) return {}
22
+ if (!('mix' in props)) return props
23
+
24
+ let { mix, ...rest } = props
25
+ let normalizedMix = normalizeMixValue(mix)
26
+ return normalizedMix === undefined ? rest : { ...rest, mix: normalizedMix }
27
+ }
28
+
29
+ function normalizeMixValue(mix: unknown): unknown[] | undefined {
30
+ if (!mix) return undefined
31
+
32
+ let normalizedMix: unknown[] = []
33
+ flattenMixValue(mix, normalizedMix)
34
+ return normalizedMix.length === 0 ? undefined : normalizedMix
35
+ }
36
+
37
+ function flattenMixValue(mix: unknown, out: unknown[]): void {
38
+ if (!mix) return
39
+
40
+ if (!Array.isArray(mix)) {
41
+ out.push(mix)
42
+ return
43
+ }
44
+
45
+ for (let item of mix) {
46
+ flattenMixValue(item, out)
47
+ }
48
+ }
@@ -1,5 +1,6 @@
1
1
  import { jsx } from './jsx.ts'
2
2
  import type { ElementType, RemixElement } from './jsx.ts'
3
+ import { normalizeChildren } from './core/children.ts'
3
4
 
4
5
  /**
5
6
  * Creates a Remix virtual element from a JSX-like call signature.
@@ -20,7 +21,7 @@ export function createElement(
20
21
  console.error(new Error('mixin elements must not receive children'))
21
22
  }
22
23
  } else {
23
- nextProps.children = children
24
+ nextProps.children = normalizeChildren(children)
24
25
  }
25
26
 
26
27
  if (nextProps.key != null) {
@@ -1,6 +1,12 @@
1
1
  import { invariant } from './invariant.ts'
2
2
  import type { FrameContext } from './frame.ts'
3
3
 
4
+ type HydratedVirtualRootStartMarker = Comment & {
5
+ $rmx: {
6
+ dispose(): void
7
+ }
8
+ }
9
+
4
10
  export function diffNodes(curr: Node[], next: Node[], context: FrameContext) {
5
11
  let parent = curr[0]?.parentNode ?? context.regionParent ?? null
6
12
  invariant(parent, 'Parent node not found')
@@ -23,8 +29,7 @@ export function diffNodes(curr: Node[], next: Node[], context: FrameContext) {
23
29
  parent.appendChild(n)
24
30
  }
25
31
  } else if (c && !n) {
26
- disposeRemovedSubFrames(c, context)
27
- parent.removeChild(c)
32
+ removeNode(c, parent, context)
28
33
  } else if (c && n) {
29
34
  // Skip hydrated client-entry boundary ranges; hydration pass re-renders
30
35
  // roots with new props from incoming payload
@@ -71,7 +76,10 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
71
76
  // Comment -> Comment
72
77
  if (isCommentNode(current) && isCommentNode(next)) {
73
78
  let newData = next.data
74
- if (current.data !== newData) current.data = newData
79
+ if (current.data !== newData) {
80
+ if (isFrameStartMarker(current)) disposeFrameStartMarker(current, context)
81
+ current.data = newData
82
+ }
75
83
  return
76
84
  }
77
85
 
@@ -80,7 +88,10 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
80
88
  // Different tags: replace
81
89
  if (current.tagName !== next.tagName) {
82
90
  let parent = current.parentNode
83
- if (parent) parent.replaceChild(next, current)
91
+ if (parent) {
92
+ parent.insertBefore(next, current)
93
+ removeNode(current, parent, context)
94
+ }
84
95
  return
85
96
  }
86
97
 
@@ -93,7 +104,10 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
93
104
 
94
105
  // Type mismatch: replace
95
106
  let parent = current.parentNode
96
- if (parent) parent.replaceChild(next, current)
107
+ if (parent) {
108
+ parent.insertBefore(next, current)
109
+ removeNode(current, parent, context)
110
+ }
97
111
  }
98
112
 
99
113
  function diffElementAttributes(current: Element, next: Element): void {
@@ -315,8 +329,7 @@ function diffElementChildren(current: Element, next: Element, context: FrameCont
315
329
  for (let i = 0; i < currentChildren.length; i++) {
316
330
  if (!used[i]) {
317
331
  let nodeToRemove = currentChildren[i]
318
- disposeRemovedSubFrames(nodeToRemove, context)
319
- current.removeChild(currentChildren[i])
332
+ removeNode(nodeToRemove, current, context)
320
333
  }
321
334
  }
322
335
  }
@@ -375,6 +388,32 @@ function isFrameStartMarker(node: Node): node is Comment {
375
388
  return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
376
389
  }
377
390
 
391
+ function removeNode(node: Node, parent: ParentNode, context: FrameContext): void {
392
+ disposeRemovedVirtualRoots(node)
393
+ disposeRemovedSubFrames(node, context)
394
+
395
+ if (node.parentNode === parent) {
396
+ parent.removeChild(node)
397
+ }
398
+ }
399
+
400
+ function disposeRemovedVirtualRoots(node: Node): void {
401
+ let stack: Node[] = [node]
402
+ while (stack.length > 0) {
403
+ let next = stack.pop()
404
+ if (!next) continue
405
+
406
+ if (isHydratedVirtualRootStartMarker(next)) {
407
+ next.$rmx.dispose()
408
+ continue
409
+ }
410
+
411
+ for (let child of Array.from(next.childNodes)) {
412
+ stack.push(child)
413
+ }
414
+ }
415
+ }
416
+
378
417
  function disposeRemovedSubFrames(node: Node, context: FrameContext): void {
379
418
  let stack: Node[] = [node]
380
419
  while (stack.length > 0) {
@@ -382,11 +421,7 @@ function disposeRemovedSubFrames(node: Node, context: FrameContext): void {
382
421
  if (!next) continue
383
422
 
384
423
  if (isFrameStartMarker(next)) {
385
- let subFrame = context.frameInstances.get(next)
386
- if (subFrame) {
387
- subFrame.dispose()
388
- context.frameInstances.delete(next)
389
- }
424
+ disposeFrameStartMarker(next, context)
390
425
  }
391
426
 
392
427
  for (let child of Array.from(next.childNodes)) {
@@ -395,10 +430,22 @@ function disposeRemovedSubFrames(node: Node, context: FrameContext): void {
395
430
  }
396
431
  }
397
432
 
433
+ function disposeFrameStartMarker(marker: Comment, context: FrameContext): void {
434
+ let subFrame = context.frameInstances.get(marker)
435
+ if (subFrame) {
436
+ subFrame.dispose()
437
+ context.frameInstances.delete(marker)
438
+ }
439
+ }
440
+
398
441
  function isVirtualRootStartMarker(node: Node): node is Comment {
399
442
  return isCommentNode(node) && node.data.trim().startsWith('rmx:h:')
400
443
  }
401
444
 
445
+ function isHydratedVirtualRootStartMarker(node: Node): node is HydratedVirtualRootStartMarker {
446
+ return isVirtualRootStartMarker(node) && '$rmx' in node
447
+ }
448
+
402
449
  function isVirtualRootEndMarker(node: Node): node is Comment {
403
450
  return isCommentNode(node) && node.data.trim() === '/rmx:h'
404
451
  }