@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
@@ -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())
@@ -332,6 +371,8 @@ function bindNodeMixRuntime(
332
371
  }
333
372
 
334
373
  function isHeadHostNode(node: HostNode): boolean {
374
+ if (node.type === 'head') return true
375
+ if (node.type.length !== 4) return false
335
376
  return node.type.toLowerCase() === 'head'
336
377
  }
337
378
 
@@ -460,8 +501,11 @@ function diffHost(
460
501
  let mixState = curr._mixState as MixinRuntimeState | undefined
461
502
  let currProps = getHostProps(curr)
462
503
  let nextProps = resolveNodeMixProps(next, frame, scheduler, mixState)
463
- if (shouldDispatchInlineMixinLifecycle(curr._dom)) {
464
- dispatchMixinBeforeUpdate(next._mixState as MixinRuntimeState | undefined)
504
+ let nextMixState = next._mixState as MixinRuntimeState | undefined
505
+ let shouldDispatchMixinLifecycle =
506
+ (nextMixState?.runners.length ?? 0) > 0 && shouldDispatchInlineMixinLifecycle(curr._dom)
507
+ if (shouldDispatchMixinLifecycle) {
508
+ dispatchMixinBeforeUpdate(nextMixState)
465
509
  }
466
510
 
467
511
  // Handle innerHTML prop BEFORE diffChildren to avoid clearing children
@@ -485,23 +529,25 @@ function diffHost(
485
529
  next,
486
530
  rootTarget,
487
531
  )
488
- diffHostProps(currProps, nextProps, curr._dom)
532
+ patchHostProps(currProps, nextProps, curr._dom)
489
533
 
490
534
  next._dom = curr._dom
491
535
  next._parent = vParent
492
536
  next._controller = curr._controller
537
+ next._directEventState = curr._directEventState
493
538
  next._controlledState = curr._controlledState
539
+ syncDirectEventListeners(next as CommittedHostNode)
494
540
 
495
541
  if (next._controlledState || shouldTrackControlledReflection(nextProps)) {
496
542
  ensureControlledReflection(next as CommittedHostNode, scheduler)
497
543
  syncControlledReflection(next as CommittedHostNode, nextProps)
498
544
  }
499
545
 
500
- bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
501
- if (shouldDispatchInlineMixinLifecycle(curr._dom)) {
502
- scheduler.enqueueCommitPhase([
503
- () => dispatchMixinCommit(next._mixState as MixinRuntimeState | undefined),
504
- ])
546
+ if (next._mixState) {
547
+ bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
548
+ }
549
+ if (shouldDispatchMixinLifecycle) {
550
+ scheduler.enqueueCommitPhase([() => dispatchMixinCommit(nextMixState)])
505
551
  }
506
552
 
507
553
  return
@@ -512,12 +558,115 @@ function setupHostNode(node: HostNode, dom: Element, scheduler: Scheduler): void
512
558
  let props = getHostProps(node)
513
559
  let committedNode = node as CommittedHostNode
514
560
 
561
+ syncDirectEventListeners(committedNode)
562
+
515
563
  if (shouldTrackControlledReflection(props)) {
516
564
  ensureControlledReflection(committedNode, scheduler)
517
565
  syncControlledReflection(committedNode, props)
518
566
  }
519
567
  }
520
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
+
521
670
  function diffText(curr: CommittedTextNode, next: TextNode, vParent: VNode) {
522
671
  if (curr._text !== next._text) {
523
672
  curr._dom.textContent = next._text
@@ -618,9 +767,11 @@ function insert(
618
767
  rootTarget,
619
768
  childCursor,
620
769
  )
621
- diffHostProps({}, hostProps, targetHead)
770
+ patchHostProps({}, hostProps, targetHead)
622
771
  setupHostNode(node, targetHead, scheduler)
623
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
772
+ if (node._mixState) {
773
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
774
+ }
624
775
  return cursor
625
776
  }
626
777
  }
@@ -638,7 +789,7 @@ function insert(
638
789
  let cursorTag = node._svg ? cursor.tagName : cursor.tagName.toLowerCase()
639
790
  if (cursorTag === node.type) {
640
791
  let nextCursor = cursor.nextSibling
641
- diffHostProps({}, hostProps, cursor)
792
+ patchHostProps({}, hostProps, cursor)
642
793
 
643
794
  // Handle innerHTML prop
644
795
  if (hostProps.innerHTML != null) {
@@ -660,7 +811,9 @@ function insert(
660
811
  }
661
812
 
662
813
  setupHostNode(node, cursor, scheduler)
663
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
814
+ if (node._mixState) {
815
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
816
+ }
664
817
  return nextCursor
665
818
  } else {
666
819
  // Type mismatch - try single-advance retry to handle browser extension injections
@@ -671,7 +824,7 @@ function insert(
671
824
  if (nextTag === node.type) {
672
825
  let nextCursor = nextSibling.nextSibling
673
826
  // Found a match after skipping - adopt it and leave skipped node in place
674
- diffHostProps({}, hostProps, nextSibling)
827
+ patchHostProps({}, hostProps, nextSibling)
675
828
 
676
829
  if (hostProps.innerHTML != null) {
677
830
  nextSibling.innerHTML = hostProps.innerHTML as string
@@ -691,7 +844,9 @@ function insert(
691
844
  }
692
845
 
693
846
  setupHostNode(node, nextSibling, scheduler)
694
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
847
+ if (node._mixState) {
848
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
849
+ }
695
850
  return nextCursor
696
851
  }
697
852
  }
@@ -703,7 +858,7 @@ function insert(
703
858
  let dom = node._svg
704
859
  ? document.createElementNS(SVG_NS, node.type)
705
860
  : document.createElement(node.type)
706
- diffHostProps({}, hostProps, dom)
861
+ patchHostProps({}, hostProps, dom)
707
862
 
708
863
  // Handle innerHTML prop
709
864
  if (hostProps.innerHTML != null) {
@@ -713,7 +868,9 @@ function insert(
713
868
  }
714
869
 
715
870
  setupHostNode(node, dom, scheduler)
716
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
871
+ if (node._mixState) {
872
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
873
+ }
717
874
  doInsert(dom)
718
875
  return cursor
719
876
  }
@@ -1223,6 +1380,7 @@ function cleanupDescendants(node: VNode, scheduler: Scheduler, styles: StyleMana
1223
1380
  }
1224
1381
 
1225
1382
  teardownMixins(node._mixState as MixinRuntimeState | undefined)
1383
+ teardownDirectEventListeners(node)
1226
1384
  teardownControlledReflection(node)
1227
1385
  if (node._controller) node._controller.abort()
1228
1386
  return
@@ -1320,6 +1478,7 @@ function performHostNodeRemoval(
1320
1478
  }
1321
1479
 
1322
1480
  teardownMixins(node._mixState as MixinRuntimeState | undefined)
1481
+ teardownDirectEventListeners(node)
1323
1482
  teardownControlledReflection(node)
1324
1483
  // Never remove the real document.head node when reconciling a <head> vnode.
1325
1484
  if (!isHeadHostNode(node)) {
@@ -1340,42 +1499,12 @@ function diffChildren(
1340
1499
  cursor?: Node | null,
1341
1500
  anchor?: Node,
1342
1501
  ) {
1343
- let nextLength = next.length
1344
-
1345
- // Warn when duplicate keys are present among siblings. Duplicate keys are
1346
- // still processed (last one wins), but they make keyed diffing ambiguous.
1347
- let hasKeys = false
1348
- let seenKeys: Set<string> | undefined
1349
- let duplicateKeys: Set<string> | undefined
1350
- for (let i = 0; i < nextLength; i++) {
1351
- let node = next[i]
1352
- if (node && node.key != null) {
1353
- hasKeys = true
1354
- if (!seenKeys) {
1355
- seenKeys = new Set([node.key])
1356
- continue
1357
- }
1358
- if (seenKeys.has(node.key)) {
1359
- if (!duplicateKeys) {
1360
- duplicateKeys = new Set()
1361
- }
1362
- duplicateKeys.add(node.key)
1363
- } else {
1364
- seenKeys.add(node.key)
1365
- }
1366
- }
1367
- }
1502
+ let hasKeys = hasKeyedChildren(next)
1368
1503
 
1369
- if (duplicateKeys?.size) {
1370
- let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
1371
- console.warn(
1372
- `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
1373
- )
1374
- }
1375
-
1376
- // Initial mount / hydration: delegate to insert() for each child so that
1377
- // hydration cursors and creation logic remain centralized there.
1378
1504
  if (curr === null) {
1505
+ if (hasKeys) {
1506
+ warnDuplicateKeys(next)
1507
+ }
1379
1508
  for (let node of next) {
1380
1509
  cursor = insert(
1381
1510
  node,
@@ -1393,14 +1522,23 @@ function diffChildren(
1393
1522
  return cursor
1394
1523
  }
1395
1524
 
1396
- 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
+ }
1397
1538
 
1398
- // Detect if any keys are present in the new children. If not, we can fall
1399
- // back to the simpler index-based diff which is cheaper and matches
1400
- // pre-existing behavior.
1401
1539
  if (!hasKeys) {
1402
- for (let i = 0; i < nextLength; i++) {
1403
- 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
1404
1542
  diffVNodes(
1405
1543
  currentNode,
1406
1544
  next[i],
@@ -1415,8 +1553,8 @@ function diffChildren(
1415
1553
  )
1416
1554
  }
1417
1555
 
1418
- if (currLength > nextLength) {
1419
- for (let i = nextLength; i < currLength; i++) {
1556
+ if (curr.length > next.length) {
1557
+ for (let i = next.length; i < curr.length; i++) {
1420
1558
  let node = curr[i]
1421
1559
  if (node) remove(node, domParent, scheduler, styles)
1422
1560
  }
@@ -1426,128 +1564,137 @@ function diffChildren(
1426
1564
  return
1427
1565
  }
1428
1566
 
1429
- // --- O(n + m) keyed diff with Map-based lookup ------------------------------
1567
+ patchKeyedChildren(
1568
+ curr,
1569
+ next,
1570
+ domParent,
1571
+ frame,
1572
+ scheduler,
1573
+ styles,
1574
+ vParent,
1575
+ rootTarget,
1576
+ cursor,
1577
+ anchor,
1578
+ )
1430
1579
 
1431
- let oldChildren = curr
1432
- let oldChildrenLength = currLength
1433
- let remainingOldChildren = oldChildrenLength
1580
+ return
1581
+ }
1434
1582
 
1435
- // Build key → index map for O(1) lookup: O(m)
1436
- let oldKeyMap = new Map<string, number>()
1437
- for (let i = 0; i < oldChildrenLength; i++) {
1438
- let c = oldChildren[i]
1439
- if (c) {
1440
- c._flags = 0
1441
- if (c.key != null) {
1442
- oldKeyMap.set(c.key, i)
1443
- }
1444
- }
1583
+ function parentUsesInnerHTML(parent: VNode): boolean {
1584
+ return isHostNode(parent) && getHostProps(parent).innerHTML != null
1585
+ }
1586
+
1587
+ function canBulkClearChildren(children: VNode[]): boolean {
1588
+ for (let child of children) {
1589
+ if (!canBulkClearNode(child)) return false
1445
1590
  }
1591
+ return true
1592
+ }
1446
1593
 
1447
- let skew = 0
1448
- let newChildren: VNode[] = new Array(nextLength)
1594
+ function canBulkClearNode(node: VNode): boolean {
1595
+ if (isCommittedTextNode(node)) return true
1449
1596
 
1450
- // First pass: match new children to old ones using Map lookup: O(n)
1451
- for (let i = 0; i < nextLength; i++) {
1452
- let childVNode = next[i]
1453
- if (!childVNode) {
1454
- newChildren[i] = childVNode
1455
- continue
1597
+ if (isCommittedHostNode(node)) {
1598
+ if (node._mixState) return false
1599
+ for (let child of node._children) {
1600
+ if (!canBulkClearNode(child)) return false
1456
1601
  }
1602
+ return true
1603
+ }
1457
1604
 
1458
- newChildren[i] = childVNode
1459
- childVNode._parent = vParent
1605
+ if (isFragmentNode(node)) {
1606
+ return canBulkClearChildren(node._children)
1607
+ }
1460
1608
 
1461
- let skewedIndex = i + skew
1462
- let matchingIndex = -1
1609
+ if (isCommittedComponentNode(node)) {
1610
+ return canBulkClearNode(node._content)
1611
+ }
1463
1612
 
1464
- let key = childVNode.key
1465
- let type = childVNode.type
1613
+ return false
1614
+ }
1466
1615
 
1467
- if (key != null) {
1468
- // O(1) Map lookup for keyed children
1469
- let mapIndex = oldKeyMap.get(key)
1470
- if (mapIndex !== undefined) {
1471
- let candidate = oldChildren[mapIndex]
1472
- let candidateFlags = candidate?._flags ?? 0
1473
- if (candidate && (candidateFlags & MATCHED) === 0 && candidate.type === type) {
1474
- matchingIndex = mapIndex
1475
- }
1476
- }
1477
- } else {
1478
- // Non-keyed children use positional identity only - no searching
1479
- let searchVNode = oldChildren[skewedIndex]
1480
- let searchFlags = searchVNode?._flags ?? 0
1481
- let available = searchVNode != null && (searchFlags & MATCHED) === 0
1482
- if (available && searchVNode.key == null && type === searchVNode.type) {
1483
- matchingIndex = skewedIndex
1484
- }
1485
- }
1616
+ function hasKeyedChildren(children: VNode[]): boolean {
1617
+ for (let node of children) {
1618
+ if (node.key != null) return true
1619
+ }
1620
+ return false
1621
+ }
1486
1622
 
1487
- childVNode._index = matchingIndex
1623
+ function warnDuplicateKeys(children: VNode[]): void {
1624
+ let seenKeys: Set<string> | undefined
1625
+ let duplicateKeys: Set<string> | undefined
1488
1626
 
1489
- let matchedOldVNode: VNode | null = null
1490
- if (matchingIndex !== -1) {
1491
- matchedOldVNode = oldChildren[matchingIndex]
1492
- remainingOldChildren--
1493
- if (matchedOldVNode) {
1494
- matchedOldVNode._flags = (matchedOldVNode._flags ?? 0) | MATCHED
1495
- }
1627
+ for (let node of children) {
1628
+ if (node.key == null) continue
1629
+
1630
+ if (!seenKeys) {
1631
+ seenKeys = new Set([node.key])
1632
+ continue
1496
1633
  }
1497
1634
 
1498
- // Determine whether this is a mount vs move and mark INSERT_VNODE
1499
- let oldDom = matchedOldVNode && findFirstDomAnchor(matchedOldVNode)
1500
- let isMounting = !matchedOldVNode || !oldDom
1501
- if (isMounting) {
1502
- if (matchingIndex === -1) {
1503
- // Adjust skew similar to Preact when lengths differ
1504
- if (nextLength > oldChildrenLength) {
1505
- skew--
1506
- } else if (nextLength < oldChildrenLength) {
1507
- skew++
1508
- }
1509
- }
1635
+ if (seenKeys.has(node.key)) {
1636
+ duplicateKeys ??= new Set()
1637
+ duplicateKeys.add(node.key)
1638
+ } else {
1639
+ seenKeys.add(node.key)
1640
+ }
1641
+ }
1510
1642
 
1511
- childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
1512
- } else if (matchingIndex !== i + skew) {
1513
- if (matchingIndex === i + skew - 1) {
1514
- skew--
1515
- } else if (matchingIndex === i + skew + 1) {
1516
- skew++
1517
- } else {
1518
- if (matchingIndex! > i + skew) skew--
1519
- else skew++
1520
- childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
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
+ }
1650
+
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
1521
1679
  }
1522
1680
  }
1523
- }
1524
1681
 
1525
- // Unmount any old children that weren't matched
1526
- if (remainingOldChildren) {
1527
- for (let i = 0; i < oldChildrenLength; i++) {
1528
- let oldVNode = oldChildren[i]
1529
- if (oldVNode && ((oldVNode._flags ?? 0) & MATCHED) === 0) {
1530
- 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)
1531
1685
  }
1532
1686
  }
1533
1687
  }
1534
1688
 
1535
- // Second pass: diff matched pairs and place/move DOM nodes in the correct
1536
- // order, similar to Preact's diffChildren + insert.
1537
- vParent._children = newChildren
1689
+ vParent._children = next
1538
1690
 
1539
- let lastPlaced: Node | null = null
1540
-
1541
- for (let i = 0; i < nextLength; i++) {
1542
- let childVNode = newChildren[i]
1543
- if (!childVNode) continue
1544
-
1545
- let idx = childVNode._index ?? -1
1546
- 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
1547
1694
 
1548
1695
  diffVNodes(
1549
- oldVNode,
1550
- childVNode,
1696
+ oldNode,
1697
+ next[index],
1551
1698
  domParent,
1552
1699
  frame,
1553
1700
  scheduler,
@@ -1557,40 +1704,255 @@ function diffChildren(
1557
1704
  anchor,
1558
1705
  cursor,
1559
1706
  )
1707
+ }
1560
1708
 
1561
- let shouldPlace = (childVNode._flags ?? 0) & INSERT_VNODE
1562
- let firstDom = findFirstDomAnchor(childVNode)
1563
- let lastDom = firstDom ? findLastDomAnchor(childVNode) : null
1564
- if (shouldPlace && firstDom && lastDom && firstDom.parentNode === domParent) {
1565
- let target: Node | null
1566
- if (lastPlaced === null) {
1567
- if (vParent._rangeStart && vParent._rangeStart.parentNode === domParent) {
1568
- target = vParent._rangeStart.nextSibling
1569
- } else {
1570
- 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
1571
1750
  }
1572
- } else {
1573
- target = lastPlaced.nextSibling
1574
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
+ }
1575
1793
 
1576
- if (target === null && anchor) target = anchor
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
1810
+
1811
+ let oldNode = curr[oldIndex]
1812
+ if (oldNode.key === nextNode.key && oldNode.type === nextNode.type) {
1813
+ matches.push({ oldIndex })
1814
+ oldIndex++
1815
+ continue
1816
+ }
1577
1817
 
1578
- // If target lies within the range we're moving, skip the move.
1579
- if (target && domRangeContainsNode(firstDom, lastDom, target)) {
1580
- // no-op
1581
- } else if (firstDom !== target) {
1582
- moveDomRange(domParent, firstDom, lastDom, target)
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
+ }
1853
+
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
1583
1929
  }
1584
1930
  }
1585
1931
 
1586
- if (lastDom) lastPlaced = lastDom
1932
+ predecessors[index] = low > 0 ? tails[low - 1] : -1
1933
+ tails[low] = index
1934
+ }
1587
1935
 
1588
- // Clear internal flags for next diff
1589
- childVNode._flags = 0
1590
- 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
1591
1941
  }
1592
1942
 
1593
- 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)
1594
1956
  }
1595
1957
 
1596
1958
  export function findFirstDomAnchor(node: VNode | null | undefined): Node | null {
@@ -1686,6 +2048,7 @@ function reclaimPersistedMixinNode(
1686
2048
  newNode._parent = vParent
1687
2049
  newNode._controller = persistedNode._controller
1688
2050
  newNode._mixState = persistedNode._mixState
2051
+ newNode._directEventState = persistedNode._directEventState
1689
2052
  newNode._controlledState = persistedNode._controlledState
1690
2053
 
1691
2054
  let prevProps = getHostProps(persistedNode)
@@ -1698,7 +2061,8 @@ function reclaimPersistedMixinNode(
1698
2061
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1699
2062
  dispatchMixinBeforeUpdate(newNode._mixState as MixinRuntimeState | undefined)
1700
2063
  }
1701
- diffHostProps(prevProps, nextProps, persistedNode._dom)
2064
+ patchHostProps(prevProps, nextProps, persistedNode._dom)
2065
+ syncDirectEventListeners(newNode as CommittedHostNode)
1702
2066
  ensureControlledReflection(newNode as CommittedHostNode, scheduler)
1703
2067
  syncControlledReflection(newNode as CommittedHostNode, nextProps)
1704
2068
 
@@ -1713,7 +2077,9 @@ function reclaimPersistedMixinNode(
1713
2077
  rootTarget,
1714
2078
  )
1715
2079
 
1716
- bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
2080
+ if (newNode._mixState) {
2081
+ bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
2082
+ }
1717
2083
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1718
2084
  scheduler.enqueueCommitPhase([
1719
2085
  () => dispatchMixinCommit(newNode._mixState as MixinRuntimeState | undefined),