@remix-run/ui 0.1.0 → 0.1.2

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 (200) hide show
  1. package/README.md +24 -3
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/components/accordion/accordion.d.ts +3 -3
  5. package/dist/components/accordion/accordion.js +0 -2
  6. package/dist/components/accordion/accordion.js.map +1 -1
  7. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  8. package/dist/components/accordion/demos/card.demo.js +58 -0
  9. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  10. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  12. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/overview.demo.js +19 -0
  15. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  16. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  17. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  18. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  19. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  20. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  21. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  22. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  23. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  24. package/dist/components/button/button.d.ts +82 -0
  25. package/dist/components/button/button.js +60 -2
  26. package/dist/components/button/button.js.map +1 -1
  27. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  28. package/dist/components/button/demos/aliases.demo.js +18 -0
  29. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  30. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  31. package/dist/components/button/demos/basic.demo.js +19 -0
  32. package/dist/components/button/demos/basic.demo.js.map +1 -0
  33. package/dist/components/button/demos/states.demo.d.ts +6 -0
  34. package/dist/components/button/demos/states.demo.js +29 -0
  35. package/dist/components/button/demos/states.demo.js.map +1 -0
  36. package/dist/components/combobox/combobox.d.ts +3 -3
  37. package/dist/components/combobox/combobox.js +0 -2
  38. package/dist/components/combobox/combobox.js.map +1 -1
  39. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  40. package/dist/components/combobox/demos/overview.demo.js +153 -0
  41. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  42. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  43. package/dist/components/listbox/demos/overview.demo.js +64 -0
  44. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  45. package/dist/components/listbox/listbox.d.ts +1 -6
  46. package/dist/components/listbox/listbox.js +21 -33
  47. package/dist/components/listbox/listbox.js.map +1 -1
  48. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  49. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  50. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  51. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  52. package/dist/components/menu/demos/overview.demo.js +32 -0
  53. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  54. package/dist/components/menu/menu.d.ts +5 -14
  55. package/dist/components/menu/menu.js +23 -38
  56. package/dist/components/menu/menu.js.map +1 -1
  57. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  58. package/dist/components/popover/demos/overview.demo.js +64 -0
  59. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  60. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  61. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  62. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  63. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  64. package/dist/components/select/demos/overview.demo.js +27 -0
  65. package/dist/components/select/demos/overview.demo.js.map +1 -0
  66. package/dist/components/select/select.d.ts +3 -3
  67. package/dist/components/select/select.js +17 -26
  68. package/dist/components/select/select.js.map +1 -1
  69. package/dist/components/tabs/tabs.d.ts +3 -3
  70. package/dist/components/tabs/tabs.js +0 -2
  71. package/dist/components/tabs/tabs.js.map +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/runtime/component.d.ts +8 -5
  75. package/dist/runtime/component.js +84 -80
  76. package/dist/runtime/component.js.map +1 -1
  77. package/dist/runtime/core/attributes.d.ts +12 -0
  78. package/dist/runtime/core/attributes.js +85 -0
  79. package/dist/runtime/core/attributes.js.map +1 -0
  80. package/dist/runtime/core/children.d.ts +7 -0
  81. package/dist/runtime/core/children.js +30 -0
  82. package/dist/runtime/core/children.js.map +1 -0
  83. package/dist/runtime/core/props.d.ts +9 -0
  84. package/dist/runtime/core/props.js +169 -0
  85. package/dist/runtime/core/props.js.map +1 -0
  86. package/dist/runtime/core/vnode.d.ts +3 -0
  87. package/dist/runtime/core/vnode.js +39 -0
  88. package/dist/runtime/core/vnode.js.map +1 -0
  89. package/dist/runtime/create-element.js +2 -1
  90. package/dist/runtime/create-element.js.map +1 -1
  91. package/dist/runtime/diff-dom.js +47 -14
  92. package/dist/runtime/diff-dom.js.map +1 -1
  93. package/dist/runtime/diff-props.d.ts +1 -2
  94. package/dist/runtime/diff-props.js +2 -181
  95. package/dist/runtime/diff-props.js.map +1 -1
  96. package/dist/runtime/dom.d.ts +4 -0
  97. package/dist/runtime/frame.d.ts +23 -2
  98. package/dist/runtime/frame.js +51 -52
  99. package/dist/runtime/frame.js.map +1 -1
  100. package/dist/runtime/jsx.js +2 -27
  101. package/dist/runtime/jsx.js.map +1 -1
  102. package/dist/runtime/mixins/mixin.js +9 -4
  103. package/dist/runtime/mixins/mixin.js.map +1 -1
  104. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  105. package/dist/runtime/mixins/on-mixin.js +9 -2
  106. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  107. package/dist/runtime/navigation.d.ts +6 -0
  108. package/dist/runtime/navigation.js +16 -3
  109. package/dist/runtime/navigation.js.map +1 -1
  110. package/dist/runtime/reconcile.js +444 -181
  111. package/dist/runtime/reconcile.js.map +1 -1
  112. package/dist/runtime/run.d.ts +17 -2
  113. package/dist/runtime/run.js.map +1 -1
  114. package/dist/runtime/scheduler.js +10 -0
  115. package/dist/runtime/scheduler.js.map +1 -1
  116. package/dist/runtime/stream-protocol.d.ts +8 -0
  117. package/dist/runtime/stream-protocol.js +20 -0
  118. package/dist/runtime/stream-protocol.js.map +1 -0
  119. package/dist/runtime/to-vnode.js +17 -18
  120. package/dist/runtime/to-vnode.js.map +1 -1
  121. package/dist/runtime/vnode.d.ts +5 -4
  122. package/dist/runtime/vnode.js +2 -3
  123. package/dist/runtime/vnode.js.map +1 -1
  124. package/dist/server/stream.js +80 -106
  125. package/dist/server/stream.js.map +1 -1
  126. package/dist/style/layers.d.ts +2 -0
  127. package/dist/style/layers.js +3 -0
  128. package/dist/style/layers.js.map +1 -0
  129. package/dist/style/stylesheet.d.ts +3 -1
  130. package/dist/style/stylesheet.js +69 -8
  131. package/dist/style/stylesheet.js.map +1 -1
  132. package/dist/theme/glyph-contract.d.ts +1 -1
  133. package/dist/theme/runtime.js +4 -1
  134. package/dist/theme/runtime.js.map +1 -1
  135. package/package.json +6 -7
  136. package/src/animation/README.md +314 -0
  137. package/src/animation/animate-layout-mixin.ts +79 -71
  138. package/src/components/accordion/README.md +166 -0
  139. package/src/components/accordion/accordion.tsx +8 -10
  140. package/src/components/accordion/demos/card.demo.tsx +113 -0
  141. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  142. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  143. package/src/components/anchor/README.md +153 -0
  144. package/src/components/breadcrumbs/README.md +55 -0
  145. package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
  146. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  147. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  148. package/src/components/button/README.md +4 -4
  149. package/src/components/button/button.tsx +88 -2
  150. package/src/components/button/demos/aliases.demo.tsx +27 -0
  151. package/src/components/button/demos/basic.demo.tsx +30 -0
  152. package/src/components/button/demos/states.demo.tsx +52 -0
  153. package/src/components/combobox/combobox.tsx +8 -10
  154. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  155. package/src/components/glyph/README.md +72 -0
  156. package/src/components/listbox/README.md +115 -0
  157. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  158. package/src/components/listbox/listbox.ts +23 -32
  159. package/src/components/menu/README.md +96 -0
  160. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  161. package/src/components/menu/demos/overview.demo.tsx +76 -0
  162. package/src/components/menu/menu.tsx +35 -41
  163. package/src/components/popover/README.md +3 -3
  164. package/src/components/popover/demos/overview.demo.tsx +143 -0
  165. package/src/components/select/README.md +107 -0
  166. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  167. package/src/components/select/demos/overview.demo.tsx +51 -0
  168. package/src/components/select/select.tsx +28 -28
  169. package/src/components/tabs/tabs.tsx +10 -5
  170. package/src/index.ts +1 -1
  171. package/src/runtime/component.ts +94 -82
  172. package/src/runtime/core/attributes.ts +97 -0
  173. package/src/runtime/core/children.ts +42 -0
  174. package/src/runtime/core/props.ts +185 -0
  175. package/src/runtime/core/vnode.ts +48 -0
  176. package/src/runtime/create-element.ts +2 -1
  177. package/src/runtime/diff-dom.ts +59 -12
  178. package/src/runtime/diff-props.ts +2 -187
  179. package/src/runtime/dom.ts +4 -0
  180. package/src/runtime/frame.ts +84 -61
  181. package/src/runtime/jsx.ts +2 -30
  182. package/src/runtime/mixins/mixin.ts +9 -4
  183. package/src/runtime/mixins/on-mixin.ts +20 -12
  184. package/src/runtime/navigation.ts +24 -3
  185. package/src/runtime/reconcile.ts +552 -186
  186. package/src/runtime/run.ts +17 -1
  187. package/src/runtime/scheduler.ts +7 -0
  188. package/src/runtime/stream-protocol.ts +27 -0
  189. package/src/runtime/to-vnode.ts +15 -17
  190. package/src/runtime/vnode.ts +6 -9
  191. package/src/server/README.md +88 -0
  192. package/src/server/stream.ts +105 -117
  193. package/src/style/layers.ts +2 -0
  194. package/src/style/stylesheet.ts +75 -10
  195. package/src/test/README.md +107 -0
  196. package/src/test/utils.ts +3 -1
  197. package/src/theme/README.md +103 -0
  198. package/src/theme/runtime.ts +4 -1
  199. package/src/utils/scroll-lock/README.md +33 -0
  200. package/src/test/setup.ts +0 -7
@@ -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
  }
@@ -1,200 +1,15 @@
1
1
  import { invariant } from './invariant.ts'
2
- import { createStyleManager, normalizeCssValue } from '../style/index.ts'
2
+ import { createStyleManager } from '../style/index.ts'
3
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
4
 
9
5
  let globalStyleManager =
10
6
  typeof window !== 'undefined' ? createStyleManager() : (null as unknown as StyleManager)
11
7
 
12
8
  export { type StyleManager }
9
+ export { patchHostProps as diffHostProps } from './core/props.ts'
13
10
 
14
11
  export let defaultStyleManager: StyleManager = globalStyleManager
15
12
 
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
13
  /**
199
14
  * Reset the global style state. For testing only - not exported from index.ts.
200
15
  */
@@ -3418,6 +3418,10 @@ export interface TemplateHTMLProps<eventTarget extends EventTarget = HTMLTemplat
3418
3418
  */
3419
3419
  export interface TextareaHTMLProps<eventTarget extends EventTarget = HTMLTextAreaElement>
3420
3420
  extends HTMLProps<eventTarget> {
3421
+ /** Textarea content comes from `value` or `defaultValue`. */
3422
+ children?: never
3423
+ /** Textarea content comes from `value` or `defaultValue`. */
3424
+ innerHTML?: never
3421
3425
  /** The `autocomplete` HTML attribute. */
3422
3426
  autocomplete?: Trackable<string | undefined>
3423
3427
  /** The `autoComplete` HTML attribute. */
@@ -8,6 +8,7 @@ import type { Scheduler, VirtualRoot } from './vdom.ts'
8
8
  import { createRangeRoot, createRoot } from './vdom.ts'
9
9
  import { diffNodes } from './diff-dom.ts'
10
10
  import { createStyleManager, type StyleManager } from '../style/index.ts'
11
+ import { findFlushMarker, type FlushKind } from './stream-protocol.ts'
11
12
 
12
13
  type FrameRoot = [Comment, Comment] | Element | Document | DocumentFragment
13
14
 
@@ -39,12 +40,31 @@ type FrameMarkerData = FrameData & {
39
40
  type PendingClientEntries = Map<Comment, [Comment, RemixElement]>
40
41
 
41
42
  /**
42
- * Loads a client entry module for hydration.
43
+ * Loads a named client-entry export for hydration.
44
+ *
45
+ * @param moduleUrl Browser-resolvable URL for the module that contains the client entry.
46
+ * @param exportName Named export to read from the loaded module.
47
+ * @returns The exported component function, or a promise for it.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * run({
52
+ * async loadModule(moduleUrl, exportName) {
53
+ * let mod = await import(moduleUrl)
54
+ * return mod[exportName]
55
+ * },
56
+ * })
57
+ * ```
43
58
  */
44
59
  export type LoadModule = (moduleUrl: string, exportName: string) => Promise<Function> | Function
45
60
 
46
61
  /**
47
- * Resolves frame content for the given frame source.
62
+ * Resolves content for a browser-loaded frame.
63
+ *
64
+ * @param src Source string from the `<Frame src>` prop.
65
+ * @param signal Abort signal for the active frame load or reload.
66
+ * @param target Optional name of the frame being reloaded.
67
+ * @returns HTML, a stream of HTML bytes, or Remix node content to render into the frame.
48
68
  */
49
69
  export type ResolveFrame = (
50
70
  src: string,
@@ -58,6 +78,11 @@ type FrameTemplateListener = (fragment: DocumentFragment) => void
58
78
 
59
79
  const bufferedFrameTemplates = new Map<string, DocumentFragment[]>()
60
80
  const frameTemplateListeners = new Map<string, Set<FrameTemplateListener>>()
81
+ const DOCTYPE_PATTERN = /<!doctype(?:\s[^>]*)?>/gi
82
+
83
+ function stripDoctypeMarkup(html: string): string {
84
+ return html.replace(DOCTYPE_PATTERN, '')
85
+ }
61
86
 
62
87
  function syncElementAttributes(target: Element, source: Element) {
63
88
  for (let attribute of Array.from(target.attributes)) {
@@ -133,6 +158,7 @@ export type Frame = {
133
158
  }
134
159
 
135
160
  type RenderOptions = {
161
+ flushKind?: FlushKind
136
162
  initialHydrationTracker?: InitialHydrationTracker
137
163
  signal?: AbortSignal
138
164
  }
@@ -206,9 +232,9 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
206
232
  if (options?.signal?.aborted) return
207
233
 
208
234
  if (content instanceof ReadableStream) {
209
- await renderFrameStream(content, container.doc, async (html) => {
235
+ await renderFrameStream(content, container.doc, async (html, flushKind) => {
210
236
  if (options?.signal?.aborted) return
211
- await render(html, options)
237
+ await render(html, { ...options, flushKind })
212
238
  })
213
239
  return
214
240
  }
@@ -232,37 +258,42 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
232
258
  contentRoot = undefined
233
259
  }
234
260
 
261
+ if (typeof content === 'string') {
262
+ let flushed = await consumeFlushBatches(content, async (html, flushKind) => {
263
+ await render(html, { ...options, flushKind })
264
+ })
265
+ if (flushed.applied) {
266
+ if (flushed.remainder !== '') {
267
+ await render(flushed.remainder, { ...options, flushKind: 'fragment' })
268
+ }
269
+ return
270
+ }
271
+ }
272
+
273
+ let htmlContent = typeof content === 'string' ? stripDoctypeMarkup(content) : undefined
274
+
235
275
  let isFullDocumentReload =
236
276
  container.root instanceof Document &&
237
- typeof content === 'string' &&
238
- isFullDocumentHtml(content)
239
-
240
- if (isFullDocumentReload && typeof content === 'string') {
241
- // Full-document reload should tear down existing hydrated roots and subframes
242
- // before diffing fresh HTML, otherwise stale component instances can survive
243
- // on detached DOM nodes.
244
- let previousBodyNodes = Array.from(container.doc.body.childNodes)
245
- removeVirtualRoots(previousBodyNodes)
246
- disposeSubFrames(previousBodyNodes, context)
247
- let parsed = new DOMParser().parseFromString(content, 'text/html')
277
+ htmlContent !== undefined &&
278
+ options?.flushKind === 'document'
279
+
280
+ if (isFullDocumentReload && htmlContent !== undefined) {
281
+ let parsed = new DOMParser().parseFromString(htmlContent, 'text/html')
248
282
  mergeRmxDataFromDocument(context.data, parsed)
249
- context.styleManager.reset()
250
- context.styleManager.adoptServerStyles(
283
+ context.styleManager.replaceServerStyles(
251
284
  collectFrameServerStyleTags(createElementContainer(parsed)),
252
285
  )
253
286
 
254
287
  syncElementAttributes(container.doc.documentElement, parsed.documentElement)
255
- syncElementAttributes(container.doc.head, parsed.head)
256
- syncElementAttributes(container.doc.body, parsed.body)
257
288
 
258
- diffNodes(Array.from(container.doc.head.childNodes), Array.from(parsed.head.childNodes), {
289
+ diffNodes([container.doc.head], [parsed.head], {
259
290
  ...context,
260
- regionParent: container.doc.head,
291
+ regionParent: container.doc.documentElement,
261
292
  regionTailRef: null,
262
293
  })
263
- diffNodes(Array.from(container.doc.body.childNodes), Array.from(parsed.body.childNodes), {
294
+ diffNodes([container.doc.body], [parsed.body], {
264
295
  ...context,
265
- regionParent: container.doc.body,
296
+ regionParent: container.doc.documentElement,
266
297
  regionTailRef: null,
267
298
  })
268
299
 
@@ -274,9 +305,8 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
274
305
  }
275
306
 
276
307
  let fragment =
277
- typeof content === 'string' ? createFragmentFromString(container.doc, content) : content
278
- context.styleManager.reset()
279
- context.styleManager.adoptServerStyles(
308
+ htmlContent !== undefined ? createFragmentFromString(container.doc, htmlContent) : content
309
+ context.styleManager.replaceServerStyles(
280
310
  collectFrameServerStyleTags(createElementContainer(fragment)),
281
311
  )
282
312
  removeEmptyHeads(fragment)
@@ -337,8 +367,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
337
367
  async function hydrateInitial(): Promise<void> {
338
368
  let initialHydrationTracker = createInitialHydrationTracker()
339
369
 
340
- context.styleManager.reset()
341
- context.styleManager.adoptServerStyles(collectFrameServerStyleTags(container))
370
+ context.styleManager.replaceServerStyles(collectFrameServerStyleTags(container))
342
371
  createSubFrames(container.childNodes, context)
343
372
  scheduleHydrationInContainer(container, context, initialHydrationTracker)
344
373
 
@@ -979,13 +1008,12 @@ function extractTemplatesFromBuffer(
979
1008
  async function renderFrameStream(
980
1009
  stream: ReadableStream<Uint8Array>,
981
1010
  doc: Document,
982
- applyHtml: (html: string) => Promise<void>,
1011
+ applyHtml: (html: string, flushKind: FlushKind) => Promise<void>,
983
1012
  ): Promise<void> {
984
1013
  let reader = stream.getReader()
985
1014
  let decoder = new TextDecoder()
986
1015
  let buffer = ''
987
1016
  let html = ''
988
- let appliedLength = 0
989
1017
  let appliedOnce = false
990
1018
 
991
1019
  try {
@@ -999,13 +1027,9 @@ async function renderFrameStream(
999
1027
 
1000
1028
  if (parsed.html !== '') {
1001
1029
  html += parsed.html
1002
- let htmlMarkers = collectHtmlMarkerSummary(html)
1003
- if (!hasBalancedMarkerSummary(htmlMarkers)) {
1004
- continue
1005
- }
1006
- await applyHtml(html)
1007
- appliedLength = html.length
1008
- appliedOnce = true
1030
+ let flushed = await consumeFlushBatches(html, applyHtml)
1031
+ appliedOnce = flushed.applied || appliedOnce
1032
+ html = flushed.remainder
1009
1033
  }
1010
1034
  }
1011
1035
 
@@ -1018,21 +1042,40 @@ async function renderFrameStream(
1018
1042
  buffer = ''
1019
1043
  }
1020
1044
 
1021
- if (html !== '' && html.length > appliedLength) {
1022
- await applyHtml(html)
1045
+ if (html !== '') {
1046
+ await applyHtml(html, 'fragment')
1023
1047
  appliedOnce = true
1024
1048
  }
1025
1049
 
1026
1050
  // A frame stream can legitimately resolve to empty content. Ensure the
1027
1051
  // existing frame region is cleared instead of treated as a no-op.
1028
1052
  if (html === '' && !appliedOnce) {
1029
- await applyHtml('')
1053
+ await applyHtml('', 'fragment')
1030
1054
  }
1031
1055
  } finally {
1032
1056
  reader.releaseLock()
1033
1057
  }
1034
1058
  }
1035
1059
 
1060
+ async function consumeFlushBatches(
1061
+ html: string,
1062
+ applyHtml: (html: string, flushKind: FlushKind) => Promise<void>,
1063
+ ): Promise<{ applied: boolean; remainder: string }> {
1064
+ let applied = false
1065
+ let cursor = 0
1066
+ let marker = findFlushMarker(html, cursor)
1067
+
1068
+ while (marker) {
1069
+ let batch = html.slice(cursor, marker.index)
1070
+ await applyHtml(batch, marker.kind)
1071
+ applied = true
1072
+ cursor = marker.endIndex
1073
+ marker = findFlushMarker(html, cursor)
1074
+ }
1075
+
1076
+ return { applied, remainder: html.slice(cursor) }
1077
+ }
1078
+
1036
1079
  type FrameContainer = {
1037
1080
  doc: Document
1038
1081
  root: ParentNode
@@ -1085,7 +1128,7 @@ function createCommentContainer([start, end]: [Comment, Comment]): FrameContaine
1085
1128
 
1086
1129
  function createFragmentFromString(doc: Document, content: string): DocumentFragment {
1087
1130
  let template = doc.createElement('template')
1088
- template.innerHTML = content.trim()
1131
+ template.innerHTML = stripDoctypeMarkup(content).trim()
1089
1132
  return template.content
1090
1133
  }
1091
1134
 
@@ -1097,11 +1140,6 @@ function isRemixNodeFrameContent(content: InternalFrameContent): content is Remi
1097
1140
  )
1098
1141
  }
1099
1142
 
1100
- function isFullDocumentHtml(content: string): boolean {
1101
- let trimmed = content.trimStart()
1102
- return /^<!doctype html\b/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)
1103
- }
1104
-
1105
1143
  type HydrationMarker = {
1106
1144
  id: string
1107
1145
  start: Comment
@@ -1194,18 +1232,3 @@ function findEndMarker(
1194
1232
 
1195
1233
  throw new Error('End marker not found')
1196
1234
  }
1197
-
1198
- function collectHtmlMarkerSummary(html: string): Record<string, number> {
1199
- return {
1200
- frameStarts: html.match(/<!--\s*rmx:f:/g)?.length ?? 0,
1201
- frameEnds: html.match(/<!--\s*\/rmx:f\s*-->/g)?.length ?? 0,
1202
- hydrationStarts: html.match(/<!--\s*rmx:h:/g)?.length ?? 0,
1203
- hydrationEnds: html.match(/<!--\s*\/rmx:h\s*-->/g)?.length ?? 0,
1204
- }
1205
- }
1206
-
1207
- function hasBalancedMarkerSummary(summary: Record<string, number>): boolean {
1208
- return (
1209
- summary.frameStarts === summary.frameEnds && summary.hydrationStarts === summary.hydrationEnds
1210
- )
1211
- }
@@ -1,5 +1,6 @@
1
1
  import type * as dom from './dom.d.ts'
2
2
  import type { Handle, RenderFn } from './component.ts'
3
+ import { createRemixElement } from './core/vnode.ts'
3
4
 
4
5
  /**
5
6
  * Any valid element type accepted by JSX or {@link import('./create-element.ts').createElement}.
@@ -102,7 +103,7 @@ export type Props<T extends keyof JSX.IntrinsicElements> = NormalizeMixProp<
102
103
  */
103
104
  export function jsx(type: ElementType, props: ElementProps, key?: string): RemixElement
104
105
  export function jsx(type: ElementType, props: ElementProps, key?: string): RemixElement {
105
- return { type, props: normalizeElementProps(props), key, $rmx: true }
106
+ return createRemixElement(type, props, key)
106
107
  }
107
108
 
108
109
  export { jsx as jsxDEV, jsx as jsxs }
@@ -367,32 +368,3 @@ declare global {
367
368
  IntrinsicHTMLElements {}
368
369
  }
369
370
  }
370
-
371
- function normalizeElementProps(props: ElementProps | null | undefined): ElementProps {
372
- if (!props) return {}
373
- if (!('mix' in props)) return props
374
-
375
- let { mix, ...rest } = props
376
- let normalizedMix = normalizeMixValue(mix)
377
- return normalizedMix === undefined ? rest : { ...rest, mix: normalizedMix }
378
- }
379
-
380
- function normalizeMixValue(mix: unknown): unknown[] | undefined {
381
- if (!mix) return undefined
382
-
383
- let normalizedMix = flattenMixValue(mix)
384
- return normalizedMix.length === 0 ? undefined : normalizedMix
385
- }
386
-
387
- function flattenMixValue(mix: unknown): unknown[] {
388
- if (!mix) return []
389
- if (!Array.isArray(mix)) return [mix]
390
-
391
- let flattened: unknown[] = []
392
-
393
- for (let item of mix) {
394
- flattened.push(...flattenMixValue(item))
395
- }
396
-
397
- return flattened
398
- }