@rlabs-inc/tui 0.1.0 → 0.2.1

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 (39) hide show
  1. package/README.md +126 -13
  2. package/index.ts +11 -5
  3. package/package.json +2 -2
  4. package/src/api/history.ts +451 -0
  5. package/src/api/mount.ts +66 -31
  6. package/src/engine/arrays/core.ts +13 -21
  7. package/src/engine/arrays/dimensions.ts +22 -32
  8. package/src/engine/arrays/index.ts +88 -86
  9. package/src/engine/arrays/interaction.ts +34 -48
  10. package/src/engine/arrays/layout.ts +67 -92
  11. package/src/engine/arrays/spacing.ts +37 -52
  12. package/src/engine/arrays/text.ts +23 -31
  13. package/src/engine/arrays/visual.ts +56 -75
  14. package/src/engine/inheritance.ts +18 -18
  15. package/src/engine/registry.ts +15 -0
  16. package/src/pipeline/frameBuffer.ts +26 -26
  17. package/src/pipeline/layout/index.ts +2 -2
  18. package/src/pipeline/layout/titan-engine.ts +112 -84
  19. package/src/primitives/animation.ts +194 -0
  20. package/src/primitives/box.ts +74 -86
  21. package/src/primitives/each.ts +87 -0
  22. package/src/primitives/index.ts +7 -0
  23. package/src/primitives/scope.ts +215 -0
  24. package/src/primitives/show.ts +77 -0
  25. package/src/primitives/text.ts +63 -59
  26. package/src/primitives/types.ts +1 -1
  27. package/src/primitives/when.ts +102 -0
  28. package/src/renderer/append-region.ts +159 -0
  29. package/src/renderer/index.ts +4 -2
  30. package/src/renderer/output.ts +11 -34
  31. package/src/state/focus.ts +16 -5
  32. package/src/state/global-keys.ts +184 -0
  33. package/src/state/index.ts +44 -8
  34. package/src/state/input.ts +534 -0
  35. package/src/state/keyboard.ts +98 -674
  36. package/src/state/mouse.ts +163 -340
  37. package/src/state/scroll.ts +7 -9
  38. package/src/types/index.ts +23 -2
  39. package/src/renderer/input.ts +0 -518
@@ -4,8 +4,7 @@
4
4
  * Colors, borders, and visual styling.
5
5
  * Colors stored as RGBA objects for alpha blending support.
6
6
  *
7
- * CRITICAL: Use regular arrays (NOT state!) to preserve binding getters.
8
- * state() proxies snapshot getter values, breaking reactivity.
7
+ * Uses slotArray for stable reactive cells that NEVER get replaced.
9
8
  *
10
9
  * Border styles:
11
10
  * 0 = none
@@ -23,118 +22,100 @@
23
22
  * Per-side borders can have independent styles.
24
23
  */
25
24
 
26
- import { bind, disconnectBinding, type Binding } from '@rlabs-inc/signals'
25
+ import { slotArray, type SlotArray } from '@rlabs-inc/signals'
27
26
  import type { RGBA } from '../../types'
28
27
 
29
28
  // =============================================================================
30
- // COLORS - Regular arrays to preserve binding reactivity
29
+ // DEFAULT VALUES
30
+ // =============================================================================
31
+
32
+ const DEFAULT_FOCUS_COLOR: RGBA = { r: 100, g: 149, b: 237, a: 255 } // cornflowerblue
33
+
34
+ // =============================================================================
35
+ // COLORS - SlotArrays for stable reactive cells
31
36
  // =============================================================================
32
37
 
33
38
  /** Foreground color (text) - null means inherit from parent */
34
- export const fgColor: Binding<RGBA | null>[] = []
39
+ export const fgColor: SlotArray<RGBA | null> = slotArray<RGBA | null>(null)
35
40
 
36
41
  /** Background color - null means transparent/inherit */
37
- export const bgColor: Binding<RGBA | null>[] = []
42
+ export const bgColor: SlotArray<RGBA | null> = slotArray<RGBA | null>(null)
38
43
 
39
44
  /** Opacity 0-1 (1 = fully opaque) */
40
- export const opacity: Binding<number>[] = []
45
+ export const opacity: SlotArray<number> = slotArray<number>(1)
41
46
 
42
47
  // =============================================================================
43
48
  // BORDERS - Per-side independent styles
44
49
  // =============================================================================
45
50
 
46
51
  /** Default border style for all sides (0-10) */
47
- export const borderStyle: Binding<number>[] = []
52
+ export const borderStyle: SlotArray<number> = slotArray<number>(0)
48
53
 
49
54
  /** Default border color for all sides - null means use foreground */
50
- export const borderColor: Binding<RGBA | null>[] = []
55
+ export const borderColor: SlotArray<RGBA | null> = slotArray<RGBA | null>(null)
51
56
 
52
57
  /** Top border style (0=none or inherit from borderStyle, 1-10=specific style) */
53
- export const borderTop: Binding<number>[] = []
58
+ export const borderTop: SlotArray<number> = slotArray<number>(0)
54
59
 
55
60
  /** Right border style */
56
- export const borderRight: Binding<number>[] = []
61
+ export const borderRight: SlotArray<number> = slotArray<number>(0)
57
62
 
58
63
  /** Bottom border style */
59
- export const borderBottom: Binding<number>[] = []
64
+ export const borderBottom: SlotArray<number> = slotArray<number>(0)
60
65
 
61
66
  /** Left border style */
62
- export const borderLeft: Binding<number>[] = []
67
+ export const borderLeft: SlotArray<number> = slotArray<number>(0)
63
68
 
64
69
  /** Per-side border colors - null means use borderColor or foreground */
65
- export const borderColorTop: Binding<RGBA | null>[] = []
66
- export const borderColorRight: Binding<RGBA | null>[] = []
67
- export const borderColorBottom: Binding<RGBA | null>[] = []
68
- export const borderColorLeft: Binding<RGBA | null>[] = []
70
+ export const borderColorTop: SlotArray<RGBA | null> = slotArray<RGBA | null>(null)
71
+ export const borderColorRight: SlotArray<RGBA | null> = slotArray<RGBA | null>(null)
72
+ export const borderColorBottom: SlotArray<RGBA | null> = slotArray<RGBA | null>(null)
73
+ export const borderColorLeft: SlotArray<RGBA | null> = slotArray<RGBA | null>(null)
69
74
 
70
75
  // =============================================================================
71
76
  // FOCUS RING
72
77
  // =============================================================================
73
78
 
74
79
  /** Show focus ring when focused (1=yes, 0=no) */
75
- export const showFocusRing: Binding<number>[] = []
80
+ export const showFocusRing: SlotArray<number> = slotArray<number>(0)
76
81
 
77
82
  /** Focus ring color */
78
- export const focusRingColor: Binding<RGBA>[] = []
79
-
80
- // =============================================================================
81
- // DEFAULT VALUES
82
- // =============================================================================
83
-
84
- const DEFAULT_FOCUS_COLOR: RGBA = { r: 100, g: 149, b: 237, a: 255 } // cornflowerblue
83
+ export const focusRingColor: SlotArray<RGBA> = slotArray<RGBA>(DEFAULT_FOCUS_COLOR)
85
84
 
86
- /** LAZY BINDING: Push undefined, primitives create bindings for used props only */
85
+ /** Ensure capacity for all visual arrays */
87
86
  export function ensureCapacity(index: number): void {
88
- while (fgColor.length <= index) {
89
- fgColor.push(undefined as any)
90
- bgColor.push(undefined as any)
91
- opacity.push(undefined as any)
92
- borderStyle.push(undefined as any)
93
- borderColor.push(undefined as any)
94
- borderTop.push(undefined as any)
95
- borderRight.push(undefined as any)
96
- borderBottom.push(undefined as any)
97
- borderLeft.push(undefined as any)
98
- borderColorTop.push(undefined as any)
99
- borderColorRight.push(undefined as any)
100
- borderColorBottom.push(undefined as any)
101
- borderColorLeft.push(undefined as any)
102
- showFocusRing.push(undefined as any)
103
- focusRingColor.push(undefined as any)
104
- }
87
+ fgColor.ensureCapacity(index)
88
+ bgColor.ensureCapacity(index)
89
+ opacity.ensureCapacity(index)
90
+ borderStyle.ensureCapacity(index)
91
+ borderColor.ensureCapacity(index)
92
+ borderTop.ensureCapacity(index)
93
+ borderRight.ensureCapacity(index)
94
+ borderBottom.ensureCapacity(index)
95
+ borderLeft.ensureCapacity(index)
96
+ borderColorTop.ensureCapacity(index)
97
+ borderColorRight.ensureCapacity(index)
98
+ borderColorBottom.ensureCapacity(index)
99
+ borderColorLeft.ensureCapacity(index)
100
+ showFocusRing.ensureCapacity(index)
101
+ focusRingColor.ensureCapacity(index)
105
102
  }
106
103
 
104
+ /** Clear slot at index (reset to default) */
107
105
  export function clearAtIndex(index: number): void {
108
- if (index < fgColor.length) {
109
- disconnectBinding(fgColor[index])
110
- disconnectBinding(bgColor[index])
111
- disconnectBinding(opacity[index])
112
- disconnectBinding(borderStyle[index])
113
- disconnectBinding(borderColor[index])
114
- disconnectBinding(borderTop[index])
115
- disconnectBinding(borderRight[index])
116
- disconnectBinding(borderBottom[index])
117
- disconnectBinding(borderLeft[index])
118
- disconnectBinding(borderColorTop[index])
119
- disconnectBinding(borderColorRight[index])
120
- disconnectBinding(borderColorBottom[index])
121
- disconnectBinding(borderColorLeft[index])
122
- disconnectBinding(showFocusRing[index])
123
- disconnectBinding(focusRingColor[index])
124
- fgColor[index] = undefined as any
125
- bgColor[index] = undefined as any
126
- opacity[index] = undefined as any
127
- borderStyle[index] = undefined as any
128
- borderColor[index] = undefined as any
129
- borderTop[index] = undefined as any
130
- borderRight[index] = undefined as any
131
- borderBottom[index] = undefined as any
132
- borderLeft[index] = undefined as any
133
- borderColorTop[index] = undefined as any
134
- borderColorRight[index] = undefined as any
135
- borderColorBottom[index] = undefined as any
136
- borderColorLeft[index] = undefined as any
137
- showFocusRing[index] = undefined as any
138
- focusRingColor[index] = undefined as any
139
- }
106
+ fgColor.clear(index)
107
+ bgColor.clear(index)
108
+ opacity.clear(index)
109
+ borderStyle.clear(index)
110
+ borderColor.clear(index)
111
+ borderTop.clear(index)
112
+ borderRight.clear(index)
113
+ borderBottom.clear(index)
114
+ borderLeft.clear(index)
115
+ borderColorTop.clear(index)
116
+ borderColorRight.clear(index)
117
+ borderColorBottom.clear(index)
118
+ borderColorLeft.clear(index)
119
+ showFocusRing.clear(index)
120
+ focusRingColor.clear(index)
140
121
  }
@@ -19,9 +19,9 @@ export function getInheritedFg(index: number): RGBA {
19
19
  let current: number = index
20
20
 
21
21
  while (current >= 0) {
22
- const fg = unwrap(visual.fgColor[current])
22
+ const fg = visual.fgColor[current]
23
23
  if (fg !== null && fg !== undefined) return fg
24
- const parent = unwrap(core.parentIndex[current])
24
+ const parent = core.parentIndex[current]
25
25
  if (parent === undefined || parent < 0) break
26
26
  current = parent
27
27
  }
@@ -37,9 +37,9 @@ export function getInheritedBg(index: number): RGBA {
37
37
  let current: number = index
38
38
 
39
39
  while (current >= 0) {
40
- const bg = unwrap(visual.bgColor[current])
40
+ const bg = visual.bgColor[current]
41
41
  if (bg !== null && bg !== undefined) return bg
42
- const parent = unwrap(core.parentIndex[current])
42
+ const parent = core.parentIndex[current]
43
43
  if (parent === undefined || parent < 0) break
44
44
  current = parent
45
45
  }
@@ -59,11 +59,11 @@ export function getInheritedBorderColor(index: number, side: 'top' | 'right' | '
59
59
  left: visual.borderColorLeft,
60
60
  }[side]
61
61
 
62
- const color = unwrap(colorArray[index])
62
+ const color = colorArray[index]
63
63
  if (color !== null && color !== undefined) return color
64
64
 
65
65
  // Try unified border color
66
- const unifiedColor = unwrap(visual.borderColor[index])
66
+ const unifiedColor = visual.borderColor[index]
67
67
  if (unifiedColor !== null && unifiedColor !== undefined) return unifiedColor
68
68
 
69
69
  // Fall back to foreground color
@@ -80,14 +80,14 @@ export function getBorderColors(index: number): {
80
80
  left: RGBA
81
81
  } {
82
82
  const fg = getInheritedFg(index)
83
- const unified = unwrap(visual.borderColor[index])
83
+ const unified = visual.borderColor[index]
84
84
  const fallback = unified ?? fg
85
85
 
86
86
  return {
87
- top: unwrap(visual.borderColorTop[index]) ?? fallback,
88
- right: unwrap(visual.borderColorRight[index]) ?? fallback,
89
- bottom: unwrap(visual.borderColorBottom[index]) ?? fallback,
90
- left: unwrap(visual.borderColorLeft[index]) ?? fallback,
87
+ top: visual.borderColorTop[index] ?? fallback,
88
+ right: visual.borderColorRight[index] ?? fallback,
89
+ bottom: visual.borderColorBottom[index] ?? fallback,
90
+ left: visual.borderColorLeft[index] ?? fallback,
91
91
  }
92
92
  }
93
93
 
@@ -101,13 +101,13 @@ export function getBorderStyles(index: number): {
101
101
  bottom: number
102
102
  left: number
103
103
  } {
104
- const unified = unwrap(visual.borderStyle[index]) || 0
104
+ const unified = visual.borderStyle[index] || 0
105
105
 
106
106
  return {
107
- top: unwrap(visual.borderTop[index]) || unified,
108
- right: unwrap(visual.borderRight[index]) || unified,
109
- bottom: unwrap(visual.borderBottom[index]) || unified,
110
- left: unwrap(visual.borderLeft[index]) || unified,
107
+ top: visual.borderTop[index] || unified,
108
+ right: visual.borderRight[index] || unified,
109
+ bottom: visual.borderBottom[index] || unified,
110
+ left: visual.borderLeft[index] || unified,
111
111
  }
112
112
  }
113
113
 
@@ -127,11 +127,11 @@ export function getEffectiveOpacity(index: number): number {
127
127
  let current: number | undefined = index
128
128
 
129
129
  while (current !== undefined && current >= 0) {
130
- const nodeOpacity = unwrap(visual.opacity[current])
130
+ const nodeOpacity = visual.opacity[current]
131
131
  if (nodeOpacity !== undefined && nodeOpacity !== 1) {
132
132
  opacity *= nodeOpacity
133
133
  }
134
- current = unwrap(core.parentIndex[current])
134
+ current = core.parentIndex[current]
135
135
  }
136
136
 
137
137
  return opacity
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { ReactiveSet } from '@rlabs-inc/signals'
14
14
  import { ensureAllCapacity, clearAllAtIndex, resetAllArrays } from './arrays'
15
+ import { parentIndex as parentIndexArray } from './arrays/core'
15
16
  import { resetTitanArrays } from '../pipeline/layout/titan-engine'
16
17
 
17
18
  // =============================================================================
@@ -101,6 +102,7 @@ export function allocateIndex(id?: string): number {
101
102
 
102
103
  /**
103
104
  * Release an index back to the pool.
105
+ * Also recursively releases all children!
104
106
  *
105
107
  * @param index - The index to release.
106
108
  */
@@ -108,6 +110,19 @@ export function releaseIndex(index: number): void {
108
110
  const id = indexToId.get(index)
109
111
  if (id === undefined) return
110
112
 
113
+ // FIRST: Find and release all children (recursive!)
114
+ // We collect children first to avoid modifying while iterating
115
+ const children: number[] = []
116
+ for (const childIndex of allocatedIndices) {
117
+ if (parentIndexArray[childIndex] === index) {
118
+ children.push(childIndex)
119
+ }
120
+ }
121
+ // Release children recursively
122
+ for (const childIndex of children) {
123
+ releaseIndex(childIndex)
124
+ }
125
+
111
126
  // Clean up mappings
112
127
  idToIndex.delete(id)
113
128
  indexToId.delete(index)
@@ -16,7 +16,7 @@
16
16
  * HitGrid updates are returned as data to be applied by the render effect.
17
17
  */
18
18
 
19
- import { derived, unwrap } from '@rlabs-inc/signals'
19
+ import { derived, neverEquals } from '@rlabs-inc/signals'
20
20
  import type { FrameBuffer, RGBA } from '../types'
21
21
  import { ComponentType } from '../types'
22
22
  import { Colors, TERMINAL_DEFAULT, rgbaBlend, rgbaLerp } from '../types/color'
@@ -46,10 +46,10 @@ import {
46
46
  // Import arrays
47
47
  import * as core from '../engine/arrays/core'
48
48
  import * as visual from '../engine/arrays/visual'
49
- import * as text from '../engine/arrays/text'
50
- import * as spacing from '../engine/arrays/spacing'
51
- import * as layout from '../engine/arrays/layout'
52
- import * as interaction from '../engine/arrays/interaction'
49
+ import *as text from '../engine/arrays/text'
50
+ import *as spacing from '../engine/arrays/spacing'
51
+ import *as layout from '../engine/arrays/layout'
52
+ import *as interaction from '../engine/arrays/interaction'
53
53
 
54
54
  // Import layout derived
55
55
  import { layoutDerived, terminalWidth, terminalHeight, renderMode } from './layout'
@@ -119,10 +119,10 @@ export const frameBufferDerived = derived((): FrameBufferResult => {
119
119
 
120
120
  for (const i of indices) {
121
121
  if (core.componentType[i] === ComponentType.NONE) continue
122
- const vis = unwrap(core.visible[i])
122
+ const vis = core.visible[i]
123
123
  if (vis === 0 || vis === false) continue
124
124
 
125
- const parent = unwrap(core.parentIndex[i]) ?? -1
125
+ const parent = core.parentIndex[i] ?? -1
126
126
  if (parent === -1) {
127
127
  rootIndices.push(i)
128
128
  } else {
@@ -136,11 +136,11 @@ export const frameBufferDerived = derived((): FrameBufferResult => {
136
136
  }
137
137
 
138
138
  // Sort roots by zIndex
139
- rootIndices.sort((a, b) => (unwrap(layout.zIndex[a]) || 0) - (unwrap(layout.zIndex[b]) || 0))
139
+ rootIndices.sort((a, b) => (layout.zIndex[a] || 0) - (layout.zIndex[b] || 0))
140
140
 
141
141
  // Sort children by zIndex
142
142
  for (const children of childMap.values()) {
143
- children.sort((a, b) => (unwrap(layout.zIndex[a]) || 0) - (unwrap(layout.zIndex[b]) || 0))
143
+ children.sort((a, b) => (layout.zIndex[a] || 0) - (layout.zIndex[b] || 0))
144
144
  }
145
145
 
146
146
  // Render tree recursively
@@ -180,7 +180,7 @@ function renderComponent(
180
180
  parentScrollX: number
181
181
  ): void {
182
182
  // Skip invisible/invalid components
183
- const vis = unwrap(core.visible[index])
183
+ const vis = core.visible[index]
184
184
  if (vis === 0 || vis === false) return
185
185
  if (core.componentType[index] === ComponentType.NONE) return
186
186
 
@@ -239,10 +239,10 @@ function renderComponent(
239
239
  }
240
240
 
241
241
  // Calculate content area (inside borders and padding)
242
- const padTop = (unwrap(spacing.paddingTop[index]) || 0) + (hasAnyBorder && borderStyles.top > 0 ? 1 : 0)
243
- const padRight = (unwrap(spacing.paddingRight[index]) || 0) + (hasAnyBorder && borderStyles.right > 0 ? 1 : 0)
244
- const padBottom = (unwrap(spacing.paddingBottom[index]) || 0) + (hasAnyBorder && borderStyles.bottom > 0 ? 1 : 0)
245
- const padLeft = (unwrap(spacing.paddingLeft[index]) || 0) + (hasAnyBorder && borderStyles.left > 0 ? 1 : 0)
242
+ const padTop = (spacing.paddingTop[index] || 0) + (hasAnyBorder && borderStyles.top > 0 ? 1 : 0)
243
+ const padRight = (spacing.paddingRight[index] || 0) + (hasAnyBorder && borderStyles.right > 0 ? 1 : 0)
244
+ const padBottom = (spacing.paddingBottom[index] || 0) + (hasAnyBorder && borderStyles.bottom > 0 ? 1 : 0)
245
+ const padLeft = (spacing.paddingLeft[index] || 0) + (hasAnyBorder && borderStyles.left > 0 ? 1 : 0)
246
246
 
247
247
  const contentX = x + padLeft
248
248
  const contentY = y + padTop
@@ -292,8 +292,8 @@ function renderComponent(
292
292
 
293
293
  // Get this component's scroll offset (scrollable comes from layout, offset from interaction)
294
294
  const isScrollable = (computedLayout.scrollable[index] ?? 0) === 1
295
- const scrollY = isScrollable ? (unwrap(interaction.scrollOffsetY[index]) || 0) : 0
296
- const scrollX = isScrollable ? (unwrap(interaction.scrollOffsetX[index]) || 0) : 0
295
+ const scrollY = isScrollable ? (interaction.scrollOffsetY[index] || 0) : 0
296
+ const scrollX = isScrollable ? (interaction.scrollOffsetX[index] || 0) : 0
297
297
 
298
298
  // Accumulated scroll for children
299
299
  const childScrollY = parentScrollY + scrollY
@@ -331,13 +331,13 @@ function renderText(
331
331
  fg: RGBA,
332
332
  clip: ClipRect
333
333
  ): void {
334
+ // Read through slotArray proxy - same pattern as color reads in inheritance.ts
334
335
  const rawValue = text.textContent[index]
335
- const unwrapped = unwrap(rawValue)
336
- const content = unwrapped == null ? '' : String(unwrapped)
336
+ const content = rawValue == null ? '' : String(rawValue)
337
337
  if (!content) return
338
338
 
339
- const attrs = unwrap(text.textAttrs[index]) || 0
340
- const align = unwrap(text.textAlign[index]) || 0
339
+ const attrs = text.textAttrs[index] || 0
340
+ const align = text.textAlign[index] || 0
341
341
 
342
342
  // Word wrap the text
343
343
  const lines = wrapText(content, w)
@@ -376,9 +376,9 @@ function renderInput(
376
376
  fg: RGBA,
377
377
  clip: ClipRect
378
378
  ): void {
379
- const content = unwrap(text.textContent[index]) || ''
380
- const attrs = unwrap(text.textAttrs[index]) || 0
381
- const cursorPos = unwrap(interaction.cursorPosition[index]) || 0
379
+ const content = text.textContent[index] || '' // SlotArray auto-unwraps
380
+ const attrs = text.textAttrs[index] || 0
381
+ const cursorPos = interaction.cursorPosition[index] || 0
382
382
 
383
383
  if (w <= 0) return
384
384
 
@@ -427,7 +427,7 @@ function renderProgress(
427
427
  fg: RGBA,
428
428
  clip?: ClipRect
429
429
  ): void {
430
- const valueStr = unwrap(text.textContent[index]) || '0'
430
+ const valueStr = text.textContent[index] || '0' // SlotArray auto-unwraps
431
431
  const progress = Math.max(0, Math.min(1, parseFloat(valueStr) || 0))
432
432
  const filled = Math.round(progress * w)
433
433
 
@@ -458,8 +458,8 @@ function renderSelect(
458
458
  fg: RGBA,
459
459
  clip: ClipRect
460
460
  ): void {
461
- const content = unwrap(text.textContent[index]) || ''
462
- const attrs = unwrap(text.textAttrs[index]) || 0
461
+ const content = text.textContent[index] || '' // SlotArray auto-unwraps
462
+ const attrs = text.textAttrs[index] || 0
463
463
 
464
464
  // For now, just show current selection
465
465
  const displayText = truncateText(content, w - 2) // Leave room for dropdown indicator
@@ -15,7 +15,7 @@
15
15
  * - Return plain output arrays
16
16
  */
17
17
 
18
- import { derived, signal } from '@rlabs-inc/signals'
18
+ import { derived, signal, neverEquals } from '@rlabs-inc/signals'
19
19
  import { getAllocatedIndices } from '../../engine/registry'
20
20
  import { computeLayoutTitan, resetTitanArrays } from './titan-engine'
21
21
  import type { ComputedLayout } from './types'
@@ -87,7 +87,7 @@ export const layoutDerived = derived((): ComputedLayout => {
87
87
  // TITAN ENGINE: Read arrays, compute, return.
88
88
  // Reactivity tracks dependencies as we read - no manual tracking needed.
89
89
  return computeLayoutTitan(tw, th, indices, constrainHeight)
90
- })
90
+ }, { equals: neverEquals })
91
91
 
92
92
  // =============================================================================
93
93
  // EXPORTS