@remix-run/ui 0.1.1 → 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 (195) 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 +41 -47
  99. package/dist/runtime/frame.js.map +1 -1
  100. package/dist/runtime/jsx.js +2 -28
  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.js +9 -1
  108. package/dist/runtime/navigation.js.map +1 -1
  109. package/dist/runtime/reconcile.js +433 -192
  110. package/dist/runtime/reconcile.js.map +1 -1
  111. package/dist/runtime/run.d.ts +17 -2
  112. package/dist/runtime/run.js.map +1 -1
  113. package/dist/runtime/stream-protocol.d.ts +8 -0
  114. package/dist/runtime/stream-protocol.js +20 -0
  115. package/dist/runtime/stream-protocol.js.map +1 -0
  116. package/dist/runtime/to-vnode.js +5 -9
  117. package/dist/runtime/to-vnode.js.map +1 -1
  118. package/dist/runtime/vnode.d.ts +5 -4
  119. package/dist/runtime/vnode.js +2 -3
  120. package/dist/runtime/vnode.js.map +1 -1
  121. package/dist/server/stream.js +76 -106
  122. package/dist/server/stream.js.map +1 -1
  123. package/dist/style/layers.d.ts +2 -0
  124. package/dist/style/layers.js +3 -0
  125. package/dist/style/layers.js.map +1 -0
  126. package/dist/style/stylesheet.d.ts +3 -1
  127. package/dist/style/stylesheet.js +69 -8
  128. package/dist/style/stylesheet.js.map +1 -1
  129. package/dist/theme/glyph-contract.d.ts +1 -1
  130. package/dist/theme/runtime.js +4 -1
  131. package/dist/theme/runtime.js.map +1 -1
  132. package/package.json +3 -3
  133. package/src/animation/README.md +314 -0
  134. package/src/animation/animate-layout-mixin.ts +79 -71
  135. package/src/components/accordion/README.md +166 -0
  136. package/src/components/accordion/accordion.tsx +8 -10
  137. package/src/components/accordion/demos/card.demo.tsx +113 -0
  138. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  139. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  140. package/src/components/anchor/README.md +153 -0
  141. package/src/components/breadcrumbs/README.md +55 -0
  142. package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
  143. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  144. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  145. package/src/components/button/README.md +4 -4
  146. package/src/components/button/button.tsx +88 -2
  147. package/src/components/button/demos/aliases.demo.tsx +27 -0
  148. package/src/components/button/demos/basic.demo.tsx +30 -0
  149. package/src/components/button/demos/states.demo.tsx +52 -0
  150. package/src/components/combobox/combobox.tsx +8 -10
  151. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  152. package/src/components/glyph/README.md +72 -0
  153. package/src/components/listbox/README.md +115 -0
  154. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  155. package/src/components/listbox/listbox.ts +23 -32
  156. package/src/components/menu/README.md +96 -0
  157. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  158. package/src/components/menu/demos/overview.demo.tsx +76 -0
  159. package/src/components/menu/menu.tsx +35 -41
  160. package/src/components/popover/README.md +3 -3
  161. package/src/components/popover/demos/overview.demo.tsx +143 -0
  162. package/src/components/select/README.md +107 -0
  163. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  164. package/src/components/select/demos/overview.demo.tsx +51 -0
  165. package/src/components/select/select.tsx +28 -28
  166. package/src/components/tabs/tabs.tsx +10 -5
  167. package/src/index.ts +1 -1
  168. package/src/runtime/component.ts +94 -82
  169. package/src/runtime/core/attributes.ts +97 -0
  170. package/src/runtime/core/children.ts +42 -0
  171. package/src/runtime/core/props.ts +185 -0
  172. package/src/runtime/core/vnode.ts +48 -0
  173. package/src/runtime/create-element.ts +2 -1
  174. package/src/runtime/diff-dom.ts +59 -12
  175. package/src/runtime/diff-props.ts +2 -187
  176. package/src/runtime/dom.ts +4 -0
  177. package/src/runtime/frame.ts +71 -55
  178. package/src/runtime/jsx.ts +2 -29
  179. package/src/runtime/mixins/mixin.ts +9 -4
  180. package/src/runtime/mixins/on-mixin.ts +20 -12
  181. package/src/runtime/navigation.ts +10 -1
  182. package/src/runtime/reconcile.ts +540 -209
  183. package/src/runtime/run.ts +17 -1
  184. package/src/runtime/stream-protocol.ts +27 -0
  185. package/src/runtime/to-vnode.ts +5 -8
  186. package/src/runtime/vnode.ts +6 -9
  187. package/src/server/README.md +88 -0
  188. package/src/server/stream.ts +99 -116
  189. package/src/style/layers.ts +2 -0
  190. package/src/style/stylesheet.ts +75 -10
  191. package/src/test/README.md +107 -0
  192. package/src/test/utils.ts +3 -1
  193. package/src/theme/README.md +103 -0
  194. package/src/theme/runtime.ts +4 -1
  195. package/src/utils/scroll-lock/README.md +33 -0
@@ -24,7 +24,7 @@ import {
24
24
  findContextFromAncestry,
25
25
  } from './vnode.ts'
26
26
  import { invariant } from './invariant.ts'
27
- import { diffHostProps } from './diff-props.ts'
27
+ import { patchHostProps } from './core/props.ts'
28
28
  import type { StyleManager } from '../style/index.ts'
29
29
  import type { ElementProps } from './jsx.ts'
30
30
  import { skipComments, logHydrationMismatch } from './client-entries.ts'
@@ -42,13 +42,10 @@ import {
42
42
  type MixinRuntimeBinding,
43
43
  type MixinRuntimeState,
44
44
  } from './mixins/mixin.ts'
45
+ import { isOnMixinDescriptor, type OnMixinDescriptor } from './mixins/on-mixin.ts'
45
46
 
46
47
  const SVG_NS = 'http://www.w3.org/2000/svg'
47
48
 
48
- // Internal diffing flags (modeled after Preact)
49
- const INSERT_VNODE = 1 << 0
50
- const MATCHED = 1 << 1
51
-
52
49
  let idCounter = 0
53
50
  let persistedRemovalToken = 0
54
51
  const persistedMixinNodes = new Set<CommittedHostNode>()
@@ -116,6 +113,20 @@ type ControlledReflectionState = {
116
113
  onChange: () => void
117
114
  }
118
115
 
116
+ type DirectEventBinding = {
117
+ type: string
118
+ handler: (event: Event, signal: AbortSignal) => void | Promise<void>
119
+ capture: boolean
120
+ stableHandler: ((event: Event) => void) | null
121
+ reentry: AbortController | null
122
+ }
123
+
124
+ type DirectEventState = {
125
+ bindings: DirectEventBinding[]
126
+ }
127
+
128
+ const EMPTY_DIRECT_EVENT_DESCRIPTORS: OnMixinDescriptor[] = []
129
+
119
130
  function shouldRestoreControlledReflectionOnInput(
120
131
  node: CommittedHostNode,
121
132
  state: ControlledReflectionState,
@@ -258,6 +269,18 @@ function resolveNodeMixProps(
258
269
  state?: MixinRuntimeState,
259
270
  ): ElementProps {
260
271
  let mix = node.props.mix
272
+ let directEventDescriptors = resolveDirectEventDescriptors(mix)
273
+ if (directEventDescriptors) {
274
+ if (state) {
275
+ teardownMixins(state)
276
+ }
277
+ node._mixState = undefined
278
+ node._mixedProps = node.props
279
+ node._directEventDescriptors = directEventDescriptors
280
+ return node.props
281
+ }
282
+
283
+ node._directEventDescriptors = undefined
261
284
  if (state == null && (mix == null || (Array.isArray(mix) && mix.length === 0))) {
262
285
  node._mixState = undefined
263
286
  node._mixedProps = node.props
@@ -283,6 +306,22 @@ function resolveNodeMixProps(
283
306
  return resolved.props
284
307
  }
285
308
 
309
+ function resolveDirectEventDescriptors(mix: ElementProps['mix']): OnMixinDescriptor[] | null {
310
+ if (!mix) return EMPTY_DIRECT_EVENT_DESCRIPTORS
311
+ if (!Array.isArray(mix)) {
312
+ return isOnMixinDescriptor(mix) ? [mix] : null
313
+ }
314
+
315
+ return areOnMixinDescriptors(mix) ? mix : null
316
+ }
317
+
318
+ function areOnMixinDescriptors(descriptors: unknown[]): descriptors is OnMixinDescriptor[] {
319
+ for (let item of descriptors) {
320
+ if (!isOnMixinDescriptor(item)) return false
321
+ }
322
+ return true
323
+ }
324
+
286
325
  function enqueueMixinBindingUpdate(
287
326
  this: MixinRuntimeBinding,
288
327
  done: (signal: AbortSignal) => void,
@@ -299,7 +338,7 @@ function enqueueMixinBindingUpdate(
299
338
  dispatchMixinBeforeUpdate(state)
300
339
  let prevProps = getHostProps(node)
301
340
  let nextProps = resolveNodeMixProps(node, this.frame, this.scheduler, state)
302
- diffHostProps(prevProps, nextProps, this.node)
341
+ patchHostProps(prevProps, nextProps, this.node)
303
342
 
304
343
  dispatchMixinCommit(state)
305
344
  done(state ? getMixinRuntimeSignal(state) : AbortSignal.abort())
@@ -490,19 +529,23 @@ function diffHost(
490
529
  next,
491
530
  rootTarget,
492
531
  )
493
- diffHostProps(currProps, nextProps, curr._dom)
532
+ patchHostProps(currProps, nextProps, curr._dom)
494
533
 
495
534
  next._dom = curr._dom
496
535
  next._parent = vParent
497
536
  next._controller = curr._controller
537
+ next._directEventState = curr._directEventState
498
538
  next._controlledState = curr._controlledState
539
+ syncDirectEventListeners(next as CommittedHostNode)
499
540
 
500
541
  if (next._controlledState || shouldTrackControlledReflection(nextProps)) {
501
542
  ensureControlledReflection(next as CommittedHostNode, scheduler)
502
543
  syncControlledReflection(next as CommittedHostNode, nextProps)
503
544
  }
504
545
 
505
- bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
546
+ if (next._mixState) {
547
+ bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
548
+ }
506
549
  if (shouldDispatchMixinLifecycle) {
507
550
  scheduler.enqueueCommitPhase([() => dispatchMixinCommit(nextMixState)])
508
551
  }
@@ -515,12 +558,115 @@ function setupHostNode(node: HostNode, dom: Element, scheduler: Scheduler): void
515
558
  let props = getHostProps(node)
516
559
  let committedNode = node as CommittedHostNode
517
560
 
561
+ syncDirectEventListeners(committedNode)
562
+
518
563
  if (shouldTrackControlledReflection(props)) {
519
564
  ensureControlledReflection(committedNode, scheduler)
520
565
  syncControlledReflection(committedNode, props)
521
566
  }
522
567
  }
523
568
 
569
+ function syncDirectEventListeners(node: CommittedHostNode): void {
570
+ let descriptors = node._directEventDescriptors as OnMixinDescriptor[] | undefined
571
+ if (!descriptors) {
572
+ teardownDirectEventListeners(node)
573
+ return
574
+ }
575
+ if (descriptors.length === 0) {
576
+ teardownDirectEventListeners(node)
577
+ return
578
+ }
579
+
580
+ let state = node._directEventState as DirectEventState | undefined
581
+ if (!state) {
582
+ state = { bindings: [] }
583
+ node._directEventState = state
584
+ }
585
+
586
+ let bindings = state.bindings
587
+ for (let index = 0; index < descriptors.length; index++) {
588
+ let descriptor = descriptors[index]
589
+ let [type, handler, captureBoolean = false] = descriptor.args
590
+ let binding = bindings[index]
591
+
592
+ if (!binding) {
593
+ binding = createDirectEventBinding(type, handler, captureBoolean)
594
+ bindings[index] = binding
595
+ attachDirectEventBinding(node._dom, binding)
596
+ continue
597
+ }
598
+
599
+ if (binding.type !== type || binding.capture !== captureBoolean) {
600
+ removeDirectEventBinding(node._dom, binding)
601
+ binding.type = type
602
+ binding.capture = captureBoolean
603
+ attachDirectEventBinding(node._dom, binding)
604
+ }
605
+
606
+ binding.handler = handler
607
+ }
608
+
609
+ for (let index = descriptors.length; index < bindings.length; index++) {
610
+ removeDirectEventBinding(node._dom, bindings[index])
611
+ }
612
+ bindings.length = descriptors.length
613
+ }
614
+
615
+ function createDirectEventBinding(
616
+ type: string,
617
+ handler: DirectEventBinding['handler'],
618
+ capture: boolean,
619
+ ): DirectEventBinding {
620
+ let binding: DirectEventBinding = {
621
+ type,
622
+ handler,
623
+ capture,
624
+ reentry: null,
625
+ stableHandler: null,
626
+ }
627
+
628
+ return binding
629
+ }
630
+
631
+ function getStableDirectEventHandler(binding: DirectEventBinding): (event: Event) => void {
632
+ if (binding.stableHandler) return binding.stableHandler
633
+
634
+ binding.stableHandler = (event) => {
635
+ invokeDirectEventBinding(binding, event)
636
+ }
637
+ return binding.stableHandler
638
+ }
639
+
640
+ function attachDirectEventBinding(dom: Element, binding: DirectEventBinding): void {
641
+ dom.addEventListener(binding.type, getStableDirectEventHandler(binding), binding.capture)
642
+ }
643
+
644
+ function removeDirectEventBinding(dom: Element, binding: DirectEventBinding): void {
645
+ if (binding.stableHandler) {
646
+ dom.removeEventListener(binding.type, binding.stableHandler, binding.capture)
647
+ }
648
+ binding.reentry?.abort(new DOMException('', 'AbortError'))
649
+ binding.reentry = null
650
+ }
651
+
652
+ function teardownDirectEventListeners(node: CommittedHostNode): void {
653
+ let state = node._directEventState as DirectEventState | undefined
654
+ if (!state) return
655
+
656
+ for (let binding of state.bindings) {
657
+ removeDirectEventBinding(node._dom, binding)
658
+ }
659
+
660
+ state.bindings.length = 0
661
+ node._directEventState = undefined
662
+ }
663
+
664
+ function invokeDirectEventBinding(binding: DirectEventBinding, event: Event): void {
665
+ binding.reentry?.abort(new DOMException('', 'EventReentry'))
666
+ binding.reentry = new AbortController()
667
+ void binding.handler(event, binding.reentry.signal)
668
+ }
669
+
524
670
  function diffText(curr: CommittedTextNode, next: TextNode, vParent: VNode) {
525
671
  if (curr._text !== next._text) {
526
672
  curr._dom.textContent = next._text
@@ -621,9 +767,11 @@ function insert(
621
767
  rootTarget,
622
768
  childCursor,
623
769
  )
624
- diffHostProps({}, hostProps, targetHead)
770
+ patchHostProps({}, hostProps, targetHead)
625
771
  setupHostNode(node, targetHead, scheduler)
626
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
772
+ if (node._mixState) {
773
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
774
+ }
627
775
  return cursor
628
776
  }
629
777
  }
@@ -641,7 +789,7 @@ function insert(
641
789
  let cursorTag = node._svg ? cursor.tagName : cursor.tagName.toLowerCase()
642
790
  if (cursorTag === node.type) {
643
791
  let nextCursor = cursor.nextSibling
644
- diffHostProps({}, hostProps, cursor)
792
+ patchHostProps({}, hostProps, cursor)
645
793
 
646
794
  // Handle innerHTML prop
647
795
  if (hostProps.innerHTML != null) {
@@ -663,7 +811,9 @@ function insert(
663
811
  }
664
812
 
665
813
  setupHostNode(node, cursor, scheduler)
666
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
814
+ if (node._mixState) {
815
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
816
+ }
667
817
  return nextCursor
668
818
  } else {
669
819
  // Type mismatch - try single-advance retry to handle browser extension injections
@@ -674,7 +824,7 @@ function insert(
674
824
  if (nextTag === node.type) {
675
825
  let nextCursor = nextSibling.nextSibling
676
826
  // Found a match after skipping - adopt it and leave skipped node in place
677
- diffHostProps({}, hostProps, nextSibling)
827
+ patchHostProps({}, hostProps, nextSibling)
678
828
 
679
829
  if (hostProps.innerHTML != null) {
680
830
  nextSibling.innerHTML = hostProps.innerHTML as string
@@ -694,7 +844,9 @@ function insert(
694
844
  }
695
845
 
696
846
  setupHostNode(node, nextSibling, scheduler)
697
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
847
+ if (node._mixState) {
848
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
849
+ }
698
850
  return nextCursor
699
851
  }
700
852
  }
@@ -706,7 +858,7 @@ function insert(
706
858
  let dom = node._svg
707
859
  ? document.createElementNS(SVG_NS, node.type)
708
860
  : document.createElement(node.type)
709
- diffHostProps({}, hostProps, dom)
861
+ patchHostProps({}, hostProps, dom)
710
862
 
711
863
  // Handle innerHTML prop
712
864
  if (hostProps.innerHTML != null) {
@@ -716,7 +868,9 @@ function insert(
716
868
  }
717
869
 
718
870
  setupHostNode(node, dom, scheduler)
719
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
871
+ if (node._mixState) {
872
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
873
+ }
720
874
  doInsert(dom)
721
875
  return cursor
722
876
  }
@@ -1226,6 +1380,7 @@ function cleanupDescendants(node: VNode, scheduler: Scheduler, styles: StyleMana
1226
1380
  }
1227
1381
 
1228
1382
  teardownMixins(node._mixState as MixinRuntimeState | undefined)
1383
+ teardownDirectEventListeners(node)
1229
1384
  teardownControlledReflection(node)
1230
1385
  if (node._controller) node._controller.abort()
1231
1386
  return
@@ -1323,6 +1478,7 @@ function performHostNodeRemoval(
1323
1478
  }
1324
1479
 
1325
1480
  teardownMixins(node._mixState as MixinRuntimeState | undefined)
1481
+ teardownDirectEventListeners(node)
1326
1482
  teardownControlledReflection(node)
1327
1483
  // Never remove the real document.head node when reconciling a <head> vnode.
1328
1484
  if (!isHeadHostNode(node)) {
@@ -1343,42 +1499,12 @@ function diffChildren(
1343
1499
  cursor?: Node | null,
1344
1500
  anchor?: Node,
1345
1501
  ) {
1346
- let nextLength = next.length
1502
+ let hasKeys = hasKeyedChildren(next)
1347
1503
 
1348
- // Warn when duplicate keys are present among siblings. Duplicate keys are
1349
- // still processed (last one wins), but they make keyed diffing ambiguous.
1350
- let hasKeys = false
1351
- let seenKeys: Set<string> | undefined
1352
- let duplicateKeys: Set<string> | undefined
1353
- for (let i = 0; i < nextLength; i++) {
1354
- let node = next[i]
1355
- if (node && node.key != null) {
1356
- hasKeys = true
1357
- if (!seenKeys) {
1358
- seenKeys = new Set([node.key])
1359
- continue
1360
- }
1361
- if (seenKeys.has(node.key)) {
1362
- if (!duplicateKeys) {
1363
- duplicateKeys = new Set()
1364
- }
1365
- duplicateKeys.add(node.key)
1366
- } else {
1367
- seenKeys.add(node.key)
1368
- }
1369
- }
1370
- }
1371
-
1372
- if (duplicateKeys?.size) {
1373
- let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
1374
- console.warn(
1375
- `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
1376
- )
1377
- }
1378
-
1379
- // Initial mount / hydration: delegate to insert() for each child so that
1380
- // hydration cursors and creation logic remain centralized there.
1381
1504
  if (curr === null) {
1505
+ if (hasKeys) {
1506
+ warnDuplicateKeys(next)
1507
+ }
1382
1508
  for (let node of next) {
1383
1509
  cursor = insert(
1384
1510
  node,
@@ -1396,14 +1522,23 @@ function diffChildren(
1396
1522
  return cursor
1397
1523
  }
1398
1524
 
1399
- let currLength = curr.length
1525
+ if (
1526
+ next.length === 0 &&
1527
+ anchor === undefined &&
1528
+ !parentUsesInnerHTML(vParent) &&
1529
+ canBulkClearChildren(curr)
1530
+ ) {
1531
+ for (let node of curr) {
1532
+ cleanupDescendants(node, scheduler, styles)
1533
+ }
1534
+ domParent.textContent = ''
1535
+ vParent._children = next
1536
+ return
1537
+ }
1400
1538
 
1401
- // Detect if any keys are present in the new children. If not, we can fall
1402
- // back to the simpler index-based diff which is cheaper and matches
1403
- // pre-existing behavior.
1404
1539
  if (!hasKeys) {
1405
- for (let i = 0; i < nextLength; i++) {
1406
- let currentNode = i < currLength ? curr[i] : null
1540
+ for (let i = 0; i < next.length; i++) {
1541
+ let currentNode = i < curr.length ? curr[i] : null
1407
1542
  diffVNodes(
1408
1543
  currentNode,
1409
1544
  next[i],
@@ -1418,8 +1553,8 @@ function diffChildren(
1418
1553
  )
1419
1554
  }
1420
1555
 
1421
- if (currLength > nextLength) {
1422
- for (let i = nextLength; i < currLength; i++) {
1556
+ if (curr.length > next.length) {
1557
+ for (let i = next.length; i < curr.length; i++) {
1423
1558
  let node = curr[i]
1424
1559
  if (node) remove(node, domParent, scheduler, styles)
1425
1560
  }
@@ -1429,160 +1564,137 @@ function diffChildren(
1429
1564
  return
1430
1565
  }
1431
1566
 
1432
- if (currLength === nextLength) {
1433
- let canDiffByPosition = true
1434
- for (let i = 0; i < nextLength; i++) {
1435
- let currentNode = curr[i]
1436
- let nextNode = next[i]
1437
- if (!currentNode || currentNode.key !== nextNode.key || currentNode.type !== nextNode.type) {
1438
- canDiffByPosition = false
1439
- break
1440
- }
1441
- }
1567
+ patchKeyedChildren(
1568
+ curr,
1569
+ next,
1570
+ domParent,
1571
+ frame,
1572
+ scheduler,
1573
+ styles,
1574
+ vParent,
1575
+ rootTarget,
1576
+ cursor,
1577
+ anchor,
1578
+ )
1442
1579
 
1443
- if (canDiffByPosition) {
1444
- for (let i = 0; i < nextLength; i++) {
1445
- diffVNodes(
1446
- curr[i],
1447
- next[i],
1448
- domParent,
1449
- frame,
1450
- scheduler,
1451
- styles,
1452
- vParent,
1453
- rootTarget,
1454
- anchor,
1455
- cursor,
1456
- )
1457
- }
1580
+ return
1581
+ }
1458
1582
 
1459
- vParent._children = next
1460
- return
1461
- }
1462
- }
1583
+ function parentUsesInnerHTML(parent: VNode): boolean {
1584
+ return isHostNode(parent) && getHostProps(parent).innerHTML != null
1585
+ }
1463
1586
 
1464
- // --- O(n + m) keyed diff with Map-based lookup ------------------------------
1587
+ function canBulkClearChildren(children: VNode[]): boolean {
1588
+ for (let child of children) {
1589
+ if (!canBulkClearNode(child)) return false
1590
+ }
1591
+ return true
1592
+ }
1465
1593
 
1466
- let oldChildren = curr
1467
- let oldChildrenLength = currLength
1468
- let remainingOldChildren = oldChildrenLength
1594
+ function canBulkClearNode(node: VNode): boolean {
1595
+ if (isCommittedTextNode(node)) return true
1469
1596
 
1470
- // Build key → index map for O(1) lookup: O(m)
1471
- let oldKeyMap = new Map<string, number>()
1472
- for (let i = 0; i < oldChildrenLength; i++) {
1473
- let c = oldChildren[i]
1474
- if (c) {
1475
- c._flags = 0
1476
- if (c.key != null) {
1477
- oldKeyMap.set(c.key, i)
1478
- }
1597
+ if (isCommittedHostNode(node)) {
1598
+ if (node._mixState) return false
1599
+ for (let child of node._children) {
1600
+ if (!canBulkClearNode(child)) return false
1479
1601
  }
1602
+ return true
1480
1603
  }
1481
1604
 
1482
- let skew = 0
1483
- let newChildren: VNode[] = new Array(nextLength)
1605
+ if (isFragmentNode(node)) {
1606
+ return canBulkClearChildren(node._children)
1607
+ }
1484
1608
 
1485
- // First pass: match new children to old ones using Map lookup: O(n)
1486
- for (let i = 0; i < nextLength; i++) {
1487
- let childVNode = next[i]
1488
- if (!childVNode) {
1489
- newChildren[i] = childVNode
1490
- continue
1491
- }
1609
+ if (isCommittedComponentNode(node)) {
1610
+ return canBulkClearNode(node._content)
1611
+ }
1492
1612
 
1493
- newChildren[i] = childVNode
1494
- childVNode._parent = vParent
1613
+ return false
1614
+ }
1495
1615
 
1496
- let skewedIndex = i + skew
1497
- let matchingIndex = -1
1616
+ function hasKeyedChildren(children: VNode[]): boolean {
1617
+ for (let node of children) {
1618
+ if (node.key != null) return true
1619
+ }
1620
+ return false
1621
+ }
1498
1622
 
1499
- let key = childVNode.key
1500
- let type = childVNode.type
1623
+ function warnDuplicateKeys(children: VNode[]): void {
1624
+ let seenKeys: Set<string> | undefined
1625
+ let duplicateKeys: Set<string> | undefined
1501
1626
 
1502
- if (key != null) {
1503
- // O(1) Map lookup for keyed children
1504
- let mapIndex = oldKeyMap.get(key)
1505
- if (mapIndex !== undefined) {
1506
- let candidate = oldChildren[mapIndex]
1507
- let candidateFlags = candidate?._flags ?? 0
1508
- if (candidate && (candidateFlags & MATCHED) === 0 && candidate.type === type) {
1509
- matchingIndex = mapIndex
1510
- }
1511
- }
1512
- } else {
1513
- // Non-keyed children use positional identity only - no searching
1514
- let searchVNode = oldChildren[skewedIndex]
1515
- let searchFlags = searchVNode?._flags ?? 0
1516
- let available = searchVNode != null && (searchFlags & MATCHED) === 0
1517
- if (available && searchVNode.key == null && type === searchVNode.type) {
1518
- matchingIndex = skewedIndex
1519
- }
1520
- }
1627
+ for (let node of children) {
1628
+ if (node.key == null) continue
1521
1629
 
1522
- childVNode._index = matchingIndex
1630
+ if (!seenKeys) {
1631
+ seenKeys = new Set([node.key])
1632
+ continue
1633
+ }
1523
1634
 
1524
- let matchedOldVNode: VNode | null = null
1525
- if (matchingIndex !== -1) {
1526
- matchedOldVNode = oldChildren[matchingIndex]
1527
- remainingOldChildren--
1528
- if (matchedOldVNode) {
1529
- matchedOldVNode._flags = (matchedOldVNode._flags ?? 0) | MATCHED
1530
- }
1635
+ if (seenKeys.has(node.key)) {
1636
+ duplicateKeys ??= new Set()
1637
+ duplicateKeys.add(node.key)
1638
+ } else {
1639
+ seenKeys.add(node.key)
1531
1640
  }
1641
+ }
1532
1642
 
1533
- // Determine whether this is a mount vs move and mark INSERT_VNODE
1534
- let oldDom = matchedOldVNode && findFirstDomAnchor(matchedOldVNode)
1535
- let isMounting = !matchedOldVNode || !oldDom
1536
- if (isMounting) {
1537
- if (matchingIndex === -1) {
1538
- // Adjust skew similar to Preact when lengths differ
1539
- if (nextLength > oldChildrenLength) {
1540
- skew--
1541
- } else if (nextLength < oldChildrenLength) {
1542
- skew++
1543
- }
1544
- }
1643
+ if (duplicateKeys?.size) {
1644
+ let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
1645
+ console.warn(
1646
+ `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
1647
+ )
1648
+ }
1649
+ }
1545
1650
 
1546
- childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
1547
- } else if (matchingIndex !== i + skew) {
1548
- if (matchingIndex === i + skew - 1) {
1549
- skew--
1550
- } else if (matchingIndex === i + skew + 1) {
1551
- skew++
1552
- } else {
1553
- if (matchingIndex! > i + skew) skew--
1554
- else skew++
1555
- childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
1651
+ function patchKeyedChildren(
1652
+ curr: VNode[],
1653
+ next: VNode[],
1654
+ domParent: ParentNode,
1655
+ frame: FrameHandle,
1656
+ scheduler: Scheduler,
1657
+ styles: StyleManager,
1658
+ vParent: VNode,
1659
+ rootTarget: EventTarget,
1660
+ cursor?: Node | null,
1661
+ anchor?: Node,
1662
+ ): void {
1663
+ let matches =
1664
+ matchKeyedChildrenInOrder(curr, next) ??
1665
+ matchKeyedChildrenAfterSingleRemoval(curr, next) ??
1666
+ matchKeyedChildrenAfterPairSwap(curr, next)
1667
+ if (!matches) {
1668
+ warnDuplicateKeys(next)
1669
+ matches = matchKeyedChildren(curr, next)
1670
+ }
1671
+
1672
+ let matchAnalysis = analyzeKeyedChildMatches(curr.length, matches)
1673
+ if (matchAnalysis.hasRemovals) {
1674
+ let usedOldIndexes = new Uint8Array(curr.length)
1675
+
1676
+ for (let match of matches) {
1677
+ if (match.oldIndex >= 0) {
1678
+ usedOldIndexes[match.oldIndex] = 1
1556
1679
  }
1557
1680
  }
1558
- }
1559
1681
 
1560
- // Unmount any old children that weren't matched
1561
- if (remainingOldChildren) {
1562
- for (let i = 0; i < oldChildrenLength; i++) {
1563
- let oldVNode = oldChildren[i]
1564
- if (oldVNode && ((oldVNode._flags ?? 0) & MATCHED) === 0) {
1565
- remove(oldVNode, domParent, scheduler, styles)
1682
+ for (let oldIndex = 0; oldIndex < curr.length; oldIndex++) {
1683
+ if (usedOldIndexes[oldIndex] === 0) {
1684
+ remove(curr[oldIndex], domParent, scheduler, styles)
1566
1685
  }
1567
1686
  }
1568
1687
  }
1569
1688
 
1570
- // Second pass: diff matched pairs and place/move DOM nodes in the correct
1571
- // order, similar to Preact's diffChildren + insert.
1572
- vParent._children = newChildren
1573
-
1574
- let lastPlaced: Node | null = null
1575
-
1576
- for (let i = 0; i < nextLength; i++) {
1577
- let childVNode = newChildren[i]
1578
- if (!childVNode) continue
1689
+ vParent._children = next
1579
1690
 
1580
- let idx = childVNode._index ?? -1
1581
- let oldVNode = idx >= 0 ? oldChildren[idx] : null
1691
+ for (let index = 0; index < next.length; index++) {
1692
+ let match = matches[index]
1693
+ let oldNode = match.oldIndex >= 0 ? curr[match.oldIndex] : null
1582
1694
 
1583
1695
  diffVNodes(
1584
- oldVNode,
1585
- childVNode,
1696
+ oldNode,
1697
+ next[index],
1586
1698
  domParent,
1587
1699
  frame,
1588
1700
  scheduler,
@@ -1592,40 +1704,255 @@ function diffChildren(
1592
1704
  anchor,
1593
1705
  cursor,
1594
1706
  )
1707
+ }
1595
1708
 
1596
- let shouldPlace = (childVNode._flags ?? 0) & INSERT_VNODE
1597
- let firstDom = findFirstDomAnchor(childVNode)
1598
- let lastDom = firstDom ? findLastDomAnchor(childVNode) : null
1599
- if (shouldPlace && firstDom && lastDom && firstDom.parentNode === domParent) {
1600
- let target: Node | null
1601
- if (lastPlaced === null) {
1602
- if (vParent._rangeStart && vParent._rangeStart.parentNode === domParent) {
1603
- target = vParent._rangeStart.nextSibling
1604
- } else {
1605
- target = domParent.firstChild
1709
+ if (matchAnalysis.canSkipPlacement) {
1710
+ return
1711
+ }
1712
+
1713
+ let stableIndexes = lisMatches(matches)
1714
+ let stableCursor = stableIndexes.length - 1
1715
+ let placementAnchor: Node | null = anchor ?? null
1716
+
1717
+ for (let index = next.length - 1; index >= 0; index--) {
1718
+ let nextNode = next[index]
1719
+ let isStable = stableIndexes[stableCursor] === index
1720
+
1721
+ if (isStable) {
1722
+ stableCursor--
1723
+ } else {
1724
+ placeVNode(nextNode, domParent, placementAnchor)
1725
+ }
1726
+
1727
+ placementAnchor = findFirstDomAnchor(nextNode) ?? placementAnchor
1728
+ }
1729
+ }
1730
+
1731
+ function matchKeyedChildren(curr: VNode[], next: VNode[]): Array<{ oldIndex: number }> {
1732
+ let oldKeyMap = new Map<string, number>()
1733
+ let usedOldIndexes = new Set<number>()
1734
+ let unkeyedSearchStart = 0
1735
+
1736
+ for (let index = 0; index < curr.length; index++) {
1737
+ let key = curr[index].key
1738
+ if (key != null) oldKeyMap.set(key, index)
1739
+ }
1740
+
1741
+ return next.map((nextNode) => {
1742
+ let oldIndex = -1
1743
+
1744
+ if (nextNode.key != null) {
1745
+ let keyedOldIndex = oldKeyMap.get(nextNode.key)
1746
+ if (keyedOldIndex !== undefined) {
1747
+ let oldNode = curr[keyedOldIndex]
1748
+ if (!usedOldIndexes.has(keyedOldIndex) && oldNode.type === nextNode.type) {
1749
+ oldIndex = keyedOldIndex
1606
1750
  }
1607
- } else {
1608
- target = lastPlaced.nextSibling
1609
1751
  }
1752
+ } else {
1753
+ for (let index = unkeyedSearchStart; index < curr.length; index++) {
1754
+ let oldNode = curr[index]
1755
+ if (usedOldIndexes.has(index) || oldNode.key != null || oldNode.type !== nextNode.type) {
1756
+ continue
1757
+ }
1758
+
1759
+ oldIndex = index
1760
+ unkeyedSearchStart = index + 1
1761
+ break
1762
+ }
1763
+ }
1764
+
1765
+ if (oldIndex >= 0) usedOldIndexes.add(oldIndex)
1766
+ return { oldIndex }
1767
+ })
1768
+ }
1769
+
1770
+ function matchKeyedChildrenInOrder(
1771
+ curr: VNode[],
1772
+ next: VNode[],
1773
+ ): Array<{ oldIndex: number }> | null {
1774
+ let length = Math.min(curr.length, next.length)
1775
+ let matches: Array<{ oldIndex: number }> = []
1776
+
1777
+ for (let index = 0; index < length; index++) {
1778
+ let nextNode = next[index]
1779
+ if (nextNode.key == null) return null
1780
+
1781
+ let oldNode = curr[index]
1782
+ if (oldNode.key !== nextNode.key || oldNode.type !== nextNode.type) {
1783
+ return null
1784
+ }
1785
+
1786
+ matches.push({ oldIndex: index })
1787
+ }
1788
+
1789
+ for (let index = length; index < next.length; index++) {
1790
+ if (next[index].key == null) return null
1791
+ matches.push({ oldIndex: -1 })
1792
+ }
1793
+
1794
+ return matches
1795
+ }
1796
+
1797
+ function matchKeyedChildrenAfterSingleRemoval(
1798
+ curr: VNode[],
1799
+ next: VNode[],
1800
+ ): Array<{ oldIndex: number }> | null {
1801
+ if (curr.length !== next.length + 1) return null
1802
+
1803
+ let matches: Array<{ oldIndex: number }> = []
1804
+ let oldIndex = 0
1805
+ let skippedOldNode = false
1806
+
1807
+ for (let nextIndex = 0; nextIndex < next.length; nextIndex++) {
1808
+ let nextNode = next[nextIndex]
1809
+ if (nextNode.key == null) return null
1610
1810
 
1611
- if (target === null && anchor) target = anchor
1811
+ let oldNode = curr[oldIndex]
1812
+ if (oldNode.key === nextNode.key && oldNode.type === nextNode.type) {
1813
+ matches.push({ oldIndex })
1814
+ oldIndex++
1815
+ continue
1816
+ }
1817
+
1818
+ if (skippedOldNode) return null
1819
+ skippedOldNode = true
1820
+ oldIndex++
1821
+
1822
+ oldNode = curr[oldIndex]
1823
+ if (oldNode.key !== nextNode.key || oldNode.type !== nextNode.type) {
1824
+ return null
1825
+ }
1826
+
1827
+ matches.push({ oldIndex })
1828
+ oldIndex++
1829
+ }
1830
+
1831
+ return matches
1832
+ }
1833
+
1834
+ function matchKeyedChildrenAfterPairSwap(
1835
+ curr: VNode[],
1836
+ next: VNode[],
1837
+ ): Array<{ oldIndex: number }> | null {
1838
+ if (curr.length !== next.length) return null
1839
+
1840
+ let matches: Array<{ oldIndex: number }> = []
1841
+ let firstMismatch = -1
1842
+ let secondMismatch = -1
1843
+
1844
+ for (let index = 0; index < next.length; index++) {
1845
+ let nextNode = next[index]
1846
+ if (nextNode.key == null) return null
1847
+
1848
+ let oldNode = curr[index]
1849
+ if (oldNode.key === nextNode.key && oldNode.type === nextNode.type) {
1850
+ matches.push({ oldIndex: index })
1851
+ continue
1852
+ }
1612
1853
 
1613
- // If target lies within the range we're moving, skip the move.
1614
- if (target && domRangeContainsNode(firstDom, lastDom, target)) {
1615
- // no-op
1616
- } else if (firstDom !== target) {
1617
- moveDomRange(domParent, firstDom, lastDom, target)
1854
+ if (firstMismatch === -1) {
1855
+ firstMismatch = index
1856
+ } else if (secondMismatch === -1) {
1857
+ secondMismatch = index
1858
+ } else {
1859
+ return null
1860
+ }
1861
+ matches.push({ oldIndex: -1 })
1862
+ }
1863
+
1864
+ if (firstMismatch === -1) return matches
1865
+ if (secondMismatch === -1) return null
1866
+
1867
+ let firstOldNode = curr[firstMismatch]
1868
+ let secondOldNode = curr[secondMismatch]
1869
+ let firstNextNode = next[firstMismatch]
1870
+ let secondNextNode = next[secondMismatch]
1871
+
1872
+ if (
1873
+ firstOldNode.key !== secondNextNode.key ||
1874
+ firstOldNode.type !== secondNextNode.type ||
1875
+ secondOldNode.key !== firstNextNode.key ||
1876
+ secondOldNode.type !== firstNextNode.type
1877
+ ) {
1878
+ return null
1879
+ }
1880
+
1881
+ matches[firstMismatch] = { oldIndex: secondMismatch }
1882
+ matches[secondMismatch] = { oldIndex: firstMismatch }
1883
+ return matches
1884
+ }
1885
+
1886
+ function analyzeKeyedChildMatches(
1887
+ currentLength: number,
1888
+ matches: ReadonlyArray<{ oldIndex: number }>,
1889
+ ): { hasRemovals: boolean; canSkipPlacement: boolean } {
1890
+ let hasRemovals = matches.length !== currentLength
1891
+ let canSkipPlacement = true
1892
+ let lastOldIndex = -1
1893
+ let sawNewNode = false
1894
+
1895
+ for (let match of matches) {
1896
+ if (match.oldIndex < 0) {
1897
+ hasRemovals = true
1898
+ sawNewNode = true
1899
+ continue
1900
+ }
1901
+
1902
+ if (sawNewNode || match.oldIndex < lastOldIndex) {
1903
+ canSkipPlacement = false
1904
+ }
1905
+ lastOldIndex = match.oldIndex
1906
+ }
1907
+
1908
+ return { hasRemovals, canSkipPlacement }
1909
+ }
1910
+
1911
+ function lisMatches(matches: ReadonlyArray<{ oldIndex: number }>): number[] {
1912
+ let predecessors = Array.from<number>({ length: matches.length })
1913
+ let tails: number[] = []
1914
+
1915
+ for (let index = 0; index < matches.length; index++) {
1916
+ let value = matches[index].oldIndex + 1
1917
+ if (value === 0) continue
1918
+
1919
+ let low = 0
1920
+ let high = tails.length
1921
+
1922
+ while (low < high) {
1923
+ let middle = (low + high) >> 1
1924
+
1925
+ if (matches[tails[middle]].oldIndex + 1 < value) {
1926
+ low = middle + 1
1927
+ } else {
1928
+ high = middle
1618
1929
  }
1619
1930
  }
1620
1931
 
1621
- if (lastDom) lastPlaced = lastDom
1932
+ predecessors[index] = low > 0 ? tails[low - 1] : -1
1933
+ tails[low] = index
1934
+ }
1622
1935
 
1623
- // Clear internal flags for next diff
1624
- childVNode._flags = 0
1625
- childVNode._index = undefined
1936
+ let cursor = tails.at(-1) ?? -1
1937
+
1938
+ for (let index = tails.length - 1; index >= 0; index--) {
1939
+ tails[index] = cursor
1940
+ cursor = predecessors[cursor] ?? -1
1626
1941
  }
1627
1942
 
1628
- return
1943
+ return tails
1944
+ }
1945
+
1946
+ function placeVNode(node: VNode, domParent: ParentNode, anchor: Node | null): void {
1947
+ let firstDom = findFirstDomAnchor(node)
1948
+ if (!firstDom || firstDom.parentNode !== domParent) return
1949
+
1950
+ let lastDom = findLastDomAnchor(node)
1951
+ if (!lastDom) return
1952
+ if (anchor && domRangeContainsNode(firstDom, lastDom, anchor)) return
1953
+ if (firstDom === anchor) return
1954
+
1955
+ moveDomRange(domParent, firstDom, lastDom, anchor)
1629
1956
  }
1630
1957
 
1631
1958
  export function findFirstDomAnchor(node: VNode | null | undefined): Node | null {
@@ -1721,6 +2048,7 @@ function reclaimPersistedMixinNode(
1721
2048
  newNode._parent = vParent
1722
2049
  newNode._controller = persistedNode._controller
1723
2050
  newNode._mixState = persistedNode._mixState
2051
+ newNode._directEventState = persistedNode._directEventState
1724
2052
  newNode._controlledState = persistedNode._controlledState
1725
2053
 
1726
2054
  let prevProps = getHostProps(persistedNode)
@@ -1733,7 +2061,8 @@ function reclaimPersistedMixinNode(
1733
2061
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1734
2062
  dispatchMixinBeforeUpdate(newNode._mixState as MixinRuntimeState | undefined)
1735
2063
  }
1736
- diffHostProps(prevProps, nextProps, persistedNode._dom)
2064
+ patchHostProps(prevProps, nextProps, persistedNode._dom)
2065
+ syncDirectEventListeners(newNode as CommittedHostNode)
1737
2066
  ensureControlledReflection(newNode as CommittedHostNode, scheduler)
1738
2067
  syncControlledReflection(newNode as CommittedHostNode, nextProps)
1739
2068
 
@@ -1748,7 +2077,9 @@ function reclaimPersistedMixinNode(
1748
2077
  rootTarget,
1749
2078
  )
1750
2079
 
1751
- bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
2080
+ if (newNode._mixState) {
2081
+ bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
2082
+ }
1752
2083
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1753
2084
  scheduler.enqueueCommitPhase([
1754
2085
  () => dispatchMixinCommit(newNode._mixState as MixinRuntimeState | undefined),