@rlabs-inc/tui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -13
- package/index.ts +11 -5
- package/package.json +2 -2
- package/src/api/mount.ts +42 -27
- package/src/engine/arrays/core.ts +13 -21
- package/src/engine/arrays/dimensions.ts +22 -32
- package/src/engine/arrays/index.ts +88 -86
- package/src/engine/arrays/interaction.ts +34 -48
- package/src/engine/arrays/layout.ts +67 -92
- package/src/engine/arrays/spacing.ts +37 -52
- package/src/engine/arrays/text.ts +23 -31
- package/src/engine/arrays/visual.ts +56 -75
- package/src/engine/inheritance.ts +18 -18
- package/src/engine/registry.ts +15 -0
- package/src/pipeline/frameBuffer.ts +26 -26
- package/src/pipeline/layout/index.ts +2 -2
- package/src/pipeline/layout/titan-engine.ts +112 -84
- package/src/primitives/animation.ts +194 -0
- package/src/primitives/box.ts +74 -86
- package/src/primitives/each.ts +87 -0
- package/src/primitives/index.ts +7 -0
- package/src/primitives/scope.ts +215 -0
- package/src/primitives/show.ts +77 -0
- package/src/primitives/text.ts +63 -59
- package/src/primitives/types.ts +1 -1
- package/src/primitives/when.ts +102 -0
- package/src/renderer/append-region.ts +303 -0
- package/src/renderer/index.ts +4 -2
- package/src/renderer/output.ts +11 -34
- package/src/state/focus.ts +16 -5
- package/src/state/global-keys.ts +184 -0
- package/src/state/index.ts +44 -8
- package/src/state/input.ts +534 -0
- package/src/state/keyboard.ts +98 -674
- package/src/state/mouse.ts +163 -340
- package/src/state/scroll.ts +7 -9
- package/src/types/index.ts +6 -0
- package/src/renderer/input.ts +0 -518
package/README.md
CHANGED
|
@@ -57,17 +57,6 @@ setInterval(() => count.value++, 1000)
|
|
|
57
57
|
- Bindings for prop connections
|
|
58
58
|
- Zero reconciliation - reactivity IS the update mechanism
|
|
59
59
|
|
|
60
|
-
### Svelte-like .tui Files (Compiler)
|
|
61
|
-
```html
|
|
62
|
-
<script>
|
|
63
|
-
const name = signal('World')
|
|
64
|
-
</script>
|
|
65
|
-
|
|
66
|
-
<box width={40} height={3}>
|
|
67
|
-
<text content={`Hello, ${name.value}!`} />
|
|
68
|
-
</box>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
60
|
### State Modules
|
|
72
61
|
- **keyboard** - Key events, shortcuts, input buffering
|
|
73
62
|
- **mouse** - Click, hover, drag, wheel events
|
|
@@ -110,6 +99,8 @@ bun run examples/tests/03-layout-flex.ts
|
|
|
110
99
|
|
|
111
100
|
## Primitives
|
|
112
101
|
|
|
102
|
+
### UI Primitives
|
|
103
|
+
|
|
113
104
|
| Primitive | Status | Description |
|
|
114
105
|
|-----------|--------|-------------|
|
|
115
106
|
| `box` | Complete | Container with flexbox layout |
|
|
@@ -118,13 +109,135 @@ bun run examples/tests/03-layout-flex.ts
|
|
|
118
109
|
| `select` | Planned | Dropdown selection |
|
|
119
110
|
| `progress` | Planned | Progress bar |
|
|
120
111
|
|
|
112
|
+
### Template Primitives
|
|
113
|
+
|
|
114
|
+
Reactive control flow for dynamic UIs - no manual effects needed!
|
|
115
|
+
|
|
116
|
+
| Primitive | Purpose | Description |
|
|
117
|
+
|-----------|---------|-------------|
|
|
118
|
+
| `each()` | Lists | Reactive list rendering with keyed reconciliation |
|
|
119
|
+
| `show()` | Conditionals | Show/hide components based on condition |
|
|
120
|
+
| `when()` | Async | Suspense-like pending/success/error states |
|
|
121
|
+
|
|
122
|
+
#### `each()` - Reactive Lists
|
|
123
|
+
|
|
124
|
+
Renders a list of components that automatically updates when the array changes.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { each, box, text, signal } from '@rlabs-inc/tui'
|
|
128
|
+
|
|
129
|
+
const todos = signal([
|
|
130
|
+
{ id: '1', text: 'Learn TUI', done: false },
|
|
131
|
+
{ id: '2', text: 'Build app', done: false },
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
box({
|
|
135
|
+
children: () => {
|
|
136
|
+
each(
|
|
137
|
+
() => todos.value, // Reactive array getter
|
|
138
|
+
(todo) => box({ // Render function per item
|
|
139
|
+
id: `todo-${todo.id}`, // Stable ID for reconciliation
|
|
140
|
+
children: () => {
|
|
141
|
+
text({ content: () => todo.text }) // Props can be reactive too!
|
|
142
|
+
}
|
|
143
|
+
}),
|
|
144
|
+
{ key: (todo) => todo.id } // Key function for efficient updates
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Add item - UI updates automatically
|
|
150
|
+
todos.value = [...todos.value, { id: '3', text: 'Deploy', done: false }]
|
|
151
|
+
|
|
152
|
+
// Remove item - component is cleaned up automatically
|
|
153
|
+
todos.value = todos.value.filter(t => t.id !== '1')
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `show()` - Conditional Rendering
|
|
157
|
+
|
|
158
|
+
Shows or hides components based on a reactive condition.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { show, box, text, signal } from '@rlabs-inc/tui'
|
|
162
|
+
|
|
163
|
+
const isLoggedIn = signal(false)
|
|
164
|
+
|
|
165
|
+
box({
|
|
166
|
+
children: () => {
|
|
167
|
+
show(
|
|
168
|
+
() => isLoggedIn.value, // Condition getter
|
|
169
|
+
() => box({ // Render when true
|
|
170
|
+
children: () => {
|
|
171
|
+
text({ content: 'Welcome back!' })
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
() => text({ content: 'Please log in' }) // Optional: render when false
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Toggle - UI switches automatically
|
|
180
|
+
isLoggedIn.value = true
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### `when()` - Async/Suspense
|
|
184
|
+
|
|
185
|
+
Handles async operations with loading, success, and error states.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { when, box, text, signal } from '@rlabs-inc/tui'
|
|
189
|
+
|
|
190
|
+
const userId = signal('123')
|
|
191
|
+
|
|
192
|
+
// Fetch function that returns a promise
|
|
193
|
+
const fetchUser = (id: string) =>
|
|
194
|
+
fetch(`/api/users/${id}`).then(r => r.json())
|
|
195
|
+
|
|
196
|
+
box({
|
|
197
|
+
children: () => {
|
|
198
|
+
when(
|
|
199
|
+
() => fetchUser(userId.value), // Promise getter (re-runs on userId change)
|
|
200
|
+
{
|
|
201
|
+
pending: () => text({ content: 'Loading...' }),
|
|
202
|
+
then: (user) => box({
|
|
203
|
+
children: () => {
|
|
204
|
+
text({ content: `Name: ${user.name}` })
|
|
205
|
+
text({ content: `Email: ${user.email}` })
|
|
206
|
+
}
|
|
207
|
+
}),
|
|
208
|
+
catch: (error) => text({
|
|
209
|
+
content: `Error: ${error.message}`,
|
|
210
|
+
fg: 'red'
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Change userId - triggers new fetch, shows loading, then result
|
|
218
|
+
userId.value = '456'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### How Template Primitives Work
|
|
222
|
+
|
|
223
|
+
All template primitives follow the same elegant pattern:
|
|
224
|
+
|
|
225
|
+
1. **Capture parent context** at creation time
|
|
226
|
+
2. **Initial render synchronously** (correct parent hierarchy)
|
|
227
|
+
3. **Internal effect** tracks reactive dependencies
|
|
228
|
+
4. **Reconcile on change** (create new, cleanup removed)
|
|
229
|
+
|
|
230
|
+
This means:
|
|
231
|
+
- User code stays clean - no manual effects
|
|
232
|
+
- Props inside templates are fully reactive
|
|
233
|
+
- Cleanup is automatic
|
|
234
|
+
- Performance is optimal (only affected components update)
|
|
235
|
+
|
|
121
236
|
## Test Coverage
|
|
122
237
|
|
|
123
|
-
- **130 tests** passing
|
|
124
238
|
- TITAN layout engine: 48 tests
|
|
125
239
|
- Parallel arrays: 17 tests
|
|
126
240
|
- Focus manager: 29 tests
|
|
127
|
-
- Compiler: 36 tests (unit + integration)
|
|
128
241
|
|
|
129
242
|
## Requirements
|
|
130
243
|
|
package/index.ts
CHANGED
|
@@ -9,21 +9,24 @@
|
|
|
9
9
|
export { mount } from './src/api'
|
|
10
10
|
|
|
11
11
|
// Primitives - UI building blocks
|
|
12
|
-
export { box, text } from './src/primitives'
|
|
13
|
-
export type { BoxProps, TextProps, Cleanup } from './src/primitives'
|
|
12
|
+
export { box, text, each, show, when, scoped, onCleanup, useAnimation, AnimationFrames } from './src/primitives'
|
|
13
|
+
export type { BoxProps, TextProps, Cleanup, AnimationOptions } from './src/primitives'
|
|
14
14
|
|
|
15
15
|
// State modules - Input handling
|
|
16
|
-
export { keyboard } from './src/state/keyboard'
|
|
16
|
+
export { keyboard, lastKey, lastEvent } from './src/state/keyboard'
|
|
17
|
+
export { mouse, hitGrid, lastMouseEvent, mouseX, mouseY, isMouseDown } from './src/state/mouse'
|
|
17
18
|
export { focusManager, focusedIndex } from './src/state/focus'
|
|
18
19
|
export { scroll } from './src/state/scroll'
|
|
19
|
-
export {
|
|
20
|
+
export { globalKeys } from './src/state/global-keys'
|
|
21
|
+
export { cursor } from './src/state/cursor'
|
|
20
22
|
|
|
21
23
|
// Types
|
|
22
24
|
export * from './src/types'
|
|
23
25
|
export * from './src/types/color'
|
|
24
26
|
|
|
25
27
|
// Signals re-export for convenience
|
|
26
|
-
export { signal, state, derived, effect, bind, signals } from '@rlabs-inc/signals'
|
|
28
|
+
export { signal, state, derived, effect, bind, signals, batch, reactiveProps } from '@rlabs-inc/signals'
|
|
29
|
+
export type { PropInput, PropsInput, ReactiveProps } from '@rlabs-inc/signals'
|
|
27
30
|
|
|
28
31
|
// Theme
|
|
29
32
|
export {
|
|
@@ -43,3 +46,6 @@ export * from './src/renderer'
|
|
|
43
46
|
|
|
44
47
|
// Engine (advanced)
|
|
45
48
|
export * from './src/engine'
|
|
49
|
+
|
|
50
|
+
// Layout (advanced - for debugging)
|
|
51
|
+
export { layoutDerived } from './src/pipeline/layout'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rlabs-inc/tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The Terminal UI Framework for TypeScript/Bun - Blazing-fast, fine-grained reactive terminal UI with complete flexbox layout",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"typescript": "^5.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@rlabs-inc/signals": "^1.
|
|
57
|
+
"@rlabs-inc/signals": "^1.8.2"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/api/mount.ts
CHANGED
|
@@ -28,12 +28,13 @@ import {
|
|
|
28
28
|
DiffRenderer,
|
|
29
29
|
InlineRenderer,
|
|
30
30
|
} from '../renderer/output'
|
|
31
|
+
import { AppendRegionRenderer } from '../renderer/append-region'
|
|
31
32
|
import * as ansi from '../renderer/ansi'
|
|
32
33
|
import { frameBufferDerived } from '../pipeline/frameBuffer'
|
|
33
34
|
import { layoutDerived, terminalWidth, terminalHeight, updateTerminalSize, renderMode } from '../pipeline/layout'
|
|
34
35
|
import { resetRegistry } from '../engine/registry'
|
|
35
|
-
import { hitGrid, clearHitGrid } from '../state/mouse'
|
|
36
|
-
import {
|
|
36
|
+
import { hitGrid, clearHitGrid, mouse } from '../state/mouse'
|
|
37
|
+
import { globalKeys } from '../state/global-keys'
|
|
37
38
|
|
|
38
39
|
// =============================================================================
|
|
39
40
|
// MOUNT
|
|
@@ -54,6 +55,7 @@ export async function mount(
|
|
|
54
55
|
mode = 'fullscreen',
|
|
55
56
|
mouse = true,
|
|
56
57
|
kittyKeyboard = true,
|
|
58
|
+
getStaticHeight,
|
|
57
59
|
} = options
|
|
58
60
|
|
|
59
61
|
// Set render mode signal BEFORE creating components
|
|
@@ -63,9 +65,10 @@ export async function mount(
|
|
|
63
65
|
// Create renderer based on mode
|
|
64
66
|
// Fullscreen uses DiffRenderer (absolute positioning)
|
|
65
67
|
// Inline uses InlineRenderer (eraseLines + sequential write)
|
|
66
|
-
// Append uses
|
|
68
|
+
// Append uses AppendRegionRenderer (two-region: static + reactive)
|
|
67
69
|
const diffRenderer = new DiffRenderer()
|
|
68
70
|
const inlineRenderer = new InlineRenderer()
|
|
71
|
+
const appendRegionRenderer = new AppendRegionRenderer()
|
|
69
72
|
|
|
70
73
|
// Mode-specific state
|
|
71
74
|
let previousHeight = 0 // For append mode: track last rendered height
|
|
@@ -87,9 +90,7 @@ export async function mount(
|
|
|
87
90
|
|
|
88
91
|
setupSequence.push(ansi.hideCursor)
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
setupSequence.push(ansi.enableMouse)
|
|
92
|
-
}
|
|
93
|
+
// Mouse tracking is handled by globalKeys.initialize() via mouse.enableTracking()
|
|
93
94
|
|
|
94
95
|
if (kittyKeyboard) {
|
|
95
96
|
setupSequence.push(ansi.enableKittyKeyboard)
|
|
@@ -101,9 +102,8 @@ export async function mount(
|
|
|
101
102
|
// Write setup sequence
|
|
102
103
|
process.stdout.write(setupSequence.join(''))
|
|
103
104
|
|
|
104
|
-
// Initialize
|
|
105
|
-
|
|
106
|
-
keyboard.initialize()
|
|
105
|
+
// Initialize global input system (stdin, keyboard, mouse, shortcuts)
|
|
106
|
+
globalKeys.initialize({ enableMouse: mouse })
|
|
107
107
|
|
|
108
108
|
// Handle resize
|
|
109
109
|
const handleResize = () => {
|
|
@@ -127,20 +127,29 @@ export async function mount(
|
|
|
127
127
|
// Create the component tree
|
|
128
128
|
root()
|
|
129
129
|
|
|
130
|
+
// Global error handlers for debugging
|
|
131
|
+
process.on('uncaughtException', (err) => {
|
|
132
|
+
console.error('[TUI] Uncaught exception:', err)
|
|
133
|
+
})
|
|
134
|
+
process.on('unhandledRejection', (err) => {
|
|
135
|
+
console.error('[TUI] Unhandled rejection:', err)
|
|
136
|
+
})
|
|
137
|
+
|
|
130
138
|
// THE ONE RENDER EFFECT
|
|
131
139
|
// This is where the magic happens - reactive rendering!
|
|
132
140
|
// Side effects (HitGrid) are applied HERE, not in the derived.
|
|
133
141
|
let stopEffect: (() => void) | null = null
|
|
134
142
|
|
|
135
143
|
stopEffect = effect(() => {
|
|
144
|
+
try {
|
|
136
145
|
const start = Bun.nanoseconds()
|
|
137
146
|
|
|
138
147
|
// Time layout separately (reading .value triggers computation if needed)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
const layoutStart = Bun.nanoseconds()
|
|
149
|
+
const computedLayout = layoutDerived.value
|
|
150
|
+
const layoutNs = Bun.nanoseconds() - layoutStart
|
|
142
151
|
|
|
143
|
-
// Time buffer computation
|
|
152
|
+
// Time buffer computation (layout is cached, so this is just framebuffer)
|
|
144
153
|
const bufferStart = Bun.nanoseconds()
|
|
145
154
|
const { buffer, hitRegions, terminalSize } = frameBufferDerived.value
|
|
146
155
|
const bufferNs = Bun.nanoseconds() - bufferStart
|
|
@@ -161,7 +170,13 @@ export async function mount(
|
|
|
161
170
|
diffRenderer.render(buffer)
|
|
162
171
|
} else if (mode === 'inline') {
|
|
163
172
|
inlineRenderer.render(buffer)
|
|
173
|
+
} else if (mode === 'append') {
|
|
174
|
+
// Append mode: use two-region renderer
|
|
175
|
+
// staticHeight determined by getStaticHeight callback or defaults to 0
|
|
176
|
+
const staticHeight = getStaticHeight ? getStaticHeight() : 0
|
|
177
|
+
appendRegionRenderer.render(buffer, { staticHeight })
|
|
164
178
|
} else {
|
|
179
|
+
// Fallback to inline for unknown modes
|
|
165
180
|
inlineRenderer.render(buffer)
|
|
166
181
|
}
|
|
167
182
|
const renderNs = Bun.nanoseconds() - renderStart
|
|
@@ -169,12 +184,14 @@ export async function mount(
|
|
|
169
184
|
const totalNs = Bun.nanoseconds() - start
|
|
170
185
|
|
|
171
186
|
// Show all timing stats in window title
|
|
172
|
-
|
|
187
|
+
const layoutMs = layoutNs / 1_000_000
|
|
173
188
|
const bufferMs = bufferNs / 1_000_000
|
|
174
189
|
const renderMs = renderNs / 1_000_000
|
|
175
190
|
const totalMs = totalNs / 1_000_000
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
process.stdout.write(`\x1b]0;TUI | h:${computedLayout.contentHeight} | layout: ${layoutMs.toFixed(3)}ms | buffer: ${bufferMs.toFixed(3)}ms | render: ${renderMs.toFixed(3)}ms | total: ${totalMs.toFixed(3)}ms\x07`)
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error('[TUI] Render effect error:', err)
|
|
194
|
+
}
|
|
178
195
|
})
|
|
179
196
|
|
|
180
197
|
// Cleanup function
|
|
@@ -185,8 +202,13 @@ export async function mount(
|
|
|
185
202
|
stopEffect = null
|
|
186
203
|
}
|
|
187
204
|
|
|
188
|
-
// Cleanup
|
|
189
|
-
|
|
205
|
+
// Cleanup global input system
|
|
206
|
+
globalKeys.cleanup()
|
|
207
|
+
|
|
208
|
+
// Cleanup append region renderer if used
|
|
209
|
+
if (mode === 'append') {
|
|
210
|
+
appendRegionRenderer.cleanup()
|
|
211
|
+
}
|
|
190
212
|
|
|
191
213
|
// Remove resize listener
|
|
192
214
|
process.stdout.removeListener('resize', handleResize)
|
|
@@ -201,11 +223,7 @@ export async function mount(
|
|
|
201
223
|
restoreSequence.push(ansi.disableKittyKeyboard)
|
|
202
224
|
}
|
|
203
225
|
|
|
204
|
-
|
|
205
|
-
restoreSequence.push(ansi.disableMouse)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
restoreSequence.push(ansi.showCursor)
|
|
226
|
+
// Mouse disable and cursor show handled by globalKeys.cleanup()
|
|
209
227
|
restoreSequence.push(ansi.reset)
|
|
210
228
|
|
|
211
229
|
if (mode === 'fullscreen') {
|
|
@@ -218,10 +236,7 @@ export async function mount(
|
|
|
218
236
|
|
|
219
237
|
process.stdout.write(restoreSequence.join(''))
|
|
220
238
|
|
|
221
|
-
//
|
|
222
|
-
if (process.stdin.isTTY) {
|
|
223
|
-
process.stdin.setRawMode(false)
|
|
224
|
-
}
|
|
239
|
+
// Raw mode disabled by globalKeys.cleanup() -> input.cleanup()
|
|
225
240
|
|
|
226
241
|
// Reset registry for clean slate
|
|
227
242
|
resetRegistry()
|
|
@@ -8,53 +8,45 @@
|
|
|
8
8
|
*
|
|
9
9
|
* NOTE: Focus state (focusable, tabIndex) is in interaction.ts
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* componentType is the exception - it stores values directly, not bindings.
|
|
11
|
+
* Uses slotArray for stable reactive cells that NEVER get replaced.
|
|
12
|
+
* componentType is the exception - it stores values directly, not reactively.
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
|
-
import {
|
|
15
|
+
import { slotArray, type SlotArray } from '@rlabs-inc/signals'
|
|
17
16
|
import { ComponentType } from '../../types'
|
|
18
17
|
import type { ComponentTypeValue } from '../../types'
|
|
19
18
|
|
|
20
|
-
/** Component type (box, text, input, etc.) - stores values directly */
|
|
19
|
+
/** Component type (box, text, input, etc.) - stores values directly (not reactive) */
|
|
21
20
|
export const componentType: ComponentTypeValue[] = []
|
|
22
21
|
|
|
23
22
|
/** Parent component index (-1 for root) */
|
|
24
|
-
export const parentIndex:
|
|
23
|
+
export const parentIndex: SlotArray<number> = slotArray<number>(-1)
|
|
25
24
|
|
|
26
25
|
/** Is component visible (0/false = hidden, 1/true = visible) */
|
|
27
|
-
export const visible:
|
|
26
|
+
export const visible: SlotArray<number | boolean> = slotArray<number | boolean>(1)
|
|
28
27
|
|
|
29
28
|
/** Component ID (for debugging and lookups) */
|
|
30
|
-
export const componentId:
|
|
29
|
+
export const componentId: SlotArray<string> = slotArray<string>('')
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* Ensure array has capacity for the given index.
|
|
34
33
|
* Called by registry when allocating.
|
|
35
|
-
*
|
|
36
|
-
* LAZY BINDING: We push undefined here, not bindings.
|
|
37
|
-
* Primitives create bindings only for props they actually use.
|
|
38
|
-
* This reduces memory from ~70 bindings/component to ~5-10.
|
|
39
34
|
*/
|
|
40
35
|
export function ensureCapacity(index: number): void {
|
|
41
36
|
while (componentType.length <= index) {
|
|
42
37
|
componentType.push(ComponentType.NONE)
|
|
43
|
-
parentIndex.push(undefined as any)
|
|
44
|
-
visible.push(undefined as any)
|
|
45
|
-
componentId.push(undefined as any)
|
|
46
38
|
}
|
|
39
|
+
parentIndex.ensureCapacity(index)
|
|
40
|
+
visible.ensureCapacity(index)
|
|
41
|
+
componentId.ensureCapacity(index)
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
/** Clear values at index (called when releasing) */
|
|
50
45
|
export function clearAtIndex(index: number): void {
|
|
51
46
|
if (index < componentType.length) {
|
|
52
47
|
componentType[index] = ComponentType.NONE
|
|
53
|
-
disconnectBinding(parentIndex[index])
|
|
54
|
-
disconnectBinding(visible[index])
|
|
55
|
-
disconnectBinding(componentId[index])
|
|
56
|
-
parentIndex[index] = undefined as any
|
|
57
|
-
visible[index] = undefined as any
|
|
58
|
-
componentId[index] = undefined as any
|
|
59
48
|
}
|
|
49
|
+
parentIndex.clear(index)
|
|
50
|
+
visible.clear(index)
|
|
51
|
+
componentId.clear(index)
|
|
60
52
|
}
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* These are INPUT values that components write.
|
|
6
6
|
* The layout derived READS these and RETURNS computed positions.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* state() proxies snapshot getter values, breaking reactivity.
|
|
8
|
+
* Uses slotArray for stable reactive cells that NEVER get replaced.
|
|
10
9
|
*
|
|
11
10
|
* Supports both absolute and percentage dimensions:
|
|
12
11
|
* - number: Absolute value in cells (e.g., 50)
|
|
@@ -17,52 +16,43 @@
|
|
|
17
16
|
* Note: 0 means "auto" for width/height, "no constraint" for min/max.
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
|
-
import {
|
|
19
|
+
import { slotArray, type SlotArray } from '@rlabs-inc/signals'
|
|
21
20
|
import type { Dimension } from '../../types'
|
|
22
21
|
|
|
23
22
|
/** Requested width (0 = auto, '100%' = full parent width) */
|
|
24
|
-
export const width:
|
|
23
|
+
export const width: SlotArray<Dimension> = slotArray<Dimension>(0)
|
|
25
24
|
|
|
26
25
|
/** Requested height (0 = auto, '100%' = full parent height) */
|
|
27
|
-
export const height:
|
|
26
|
+
export const height: SlotArray<Dimension> = slotArray<Dimension>(0)
|
|
28
27
|
|
|
29
28
|
/** Minimum width constraint */
|
|
30
|
-
export const minWidth:
|
|
29
|
+
export const minWidth: SlotArray<Dimension> = slotArray<Dimension>(0)
|
|
31
30
|
|
|
32
31
|
/** Minimum height constraint */
|
|
33
|
-
export const minHeight:
|
|
32
|
+
export const minHeight: SlotArray<Dimension> = slotArray<Dimension>(0)
|
|
34
33
|
|
|
35
34
|
/** Maximum width constraint (0 = no max) */
|
|
36
|
-
export const maxWidth:
|
|
35
|
+
export const maxWidth: SlotArray<Dimension> = slotArray<Dimension>(0)
|
|
37
36
|
|
|
38
37
|
/** Maximum height constraint (0 = no max) */
|
|
39
|
-
export const maxHeight:
|
|
38
|
+
export const maxHeight: SlotArray<Dimension> = slotArray<Dimension>(0)
|
|
40
39
|
|
|
41
|
-
/**
|
|
40
|
+
/** Ensure capacity for all dimension arrays */
|
|
42
41
|
export function ensureCapacity(index: number): void {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
maxHeight.push(undefined as any)
|
|
50
|
-
}
|
|
42
|
+
width.ensureCapacity(index)
|
|
43
|
+
height.ensureCapacity(index)
|
|
44
|
+
minWidth.ensureCapacity(index)
|
|
45
|
+
minHeight.ensureCapacity(index)
|
|
46
|
+
maxWidth.ensureCapacity(index)
|
|
47
|
+
maxHeight.ensureCapacity(index)
|
|
51
48
|
}
|
|
52
49
|
|
|
50
|
+
/** Clear slot at index (reset to default) */
|
|
53
51
|
export function clearAtIndex(index: number): void {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
disconnectBinding(maxHeight[index])
|
|
61
|
-
width[index] = undefined as any
|
|
62
|
-
height[index] = undefined as any
|
|
63
|
-
minWidth[index] = undefined as any
|
|
64
|
-
minHeight[index] = undefined as any
|
|
65
|
-
maxWidth[index] = undefined as any
|
|
66
|
-
maxHeight[index] = undefined as any
|
|
67
|
-
}
|
|
52
|
+
width.clear(index)
|
|
53
|
+
height.clear(index)
|
|
54
|
+
minWidth.clear(index)
|
|
55
|
+
minHeight.clear(index)
|
|
56
|
+
maxWidth.clear(index)
|
|
57
|
+
maxHeight.clear(index)
|
|
68
58
|
}
|