@rlabs-inc/tui 0.1.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 +141 -0
- package/index.ts +45 -0
- package/package.json +59 -0
- package/src/api/index.ts +7 -0
- package/src/api/mount.ts +230 -0
- package/src/engine/arrays/core.ts +60 -0
- package/src/engine/arrays/dimensions.ts +68 -0
- package/src/engine/arrays/index.ts +166 -0
- package/src/engine/arrays/interaction.ts +112 -0
- package/src/engine/arrays/layout.ts +175 -0
- package/src/engine/arrays/spacing.ts +100 -0
- package/src/engine/arrays/text.ts +55 -0
- package/src/engine/arrays/visual.ts +140 -0
- package/src/engine/index.ts +25 -0
- package/src/engine/inheritance.ts +138 -0
- package/src/engine/registry.ts +180 -0
- package/src/pipeline/frameBuffer.ts +473 -0
- package/src/pipeline/layout/index.ts +105 -0
- package/src/pipeline/layout/titan-engine.ts +798 -0
- package/src/pipeline/layout/types.ts +194 -0
- package/src/pipeline/layout/utils/hierarchy.ts +202 -0
- package/src/pipeline/layout/utils/math.ts +134 -0
- package/src/pipeline/layout/utils/text-measure.ts +160 -0
- package/src/pipeline/layout.ts +30 -0
- package/src/primitives/box.ts +312 -0
- package/src/primitives/index.ts +12 -0
- package/src/primitives/text.ts +199 -0
- package/src/primitives/types.ts +222 -0
- package/src/primitives/utils.ts +37 -0
- package/src/renderer/ansi.ts +625 -0
- package/src/renderer/buffer.ts +667 -0
- package/src/renderer/index.ts +40 -0
- package/src/renderer/input.ts +518 -0
- package/src/renderer/output.ts +451 -0
- package/src/state/cursor.ts +176 -0
- package/src/state/focus.ts +241 -0
- package/src/state/index.ts +43 -0
- package/src/state/keyboard.ts +771 -0
- package/src/state/mouse.ts +524 -0
- package/src/state/scroll.ts +341 -0
- package/src/state/theme.ts +687 -0
- package/src/types/color.ts +401 -0
- package/src/types/index.ts +316 -0
- package/src/utils/text.ts +471 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Layout Module
|
|
3
|
+
*
|
|
4
|
+
* RE-EXPORTS from the new terminal-native layout engine.
|
|
5
|
+
*
|
|
6
|
+
* The Yoga-based implementation has been replaced with our custom engine:
|
|
7
|
+
* - O(n) flat array iteration (no recursion)
|
|
8
|
+
* - 1000+ nested levels without breaking a sweat
|
|
9
|
+
* - Integer-only math for terminal cells
|
|
10
|
+
* - Full Flexbox + positioning support
|
|
11
|
+
*
|
|
12
|
+
* See src/pipeline/layout/ for the implementation.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Re-export everything from the new layout module
|
|
16
|
+
export {
|
|
17
|
+
layoutDerived,
|
|
18
|
+
terminalWidth,
|
|
19
|
+
terminalHeight,
|
|
20
|
+
updateTerminalSize,
|
|
21
|
+
renderMode,
|
|
22
|
+
type ComputedLayout,
|
|
23
|
+
FlexDirection,
|
|
24
|
+
FlexWrap,
|
|
25
|
+
JustifyContent,
|
|
26
|
+
AlignItems,
|
|
27
|
+
Position,
|
|
28
|
+
Display,
|
|
29
|
+
Overflow,
|
|
30
|
+
} from './layout/index'
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Box Primitive
|
|
3
|
+
*
|
|
4
|
+
* Container component with flexbox layout, borders, and background.
|
|
5
|
+
* Children inherit parent context automatically.
|
|
6
|
+
*
|
|
7
|
+
* REACTIVITY: Props are passed directly to bind() to preserve reactive links.
|
|
8
|
+
* Don't extract values before binding - that breaks the connection!
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```ts
|
|
12
|
+
* const width = signal(40)
|
|
13
|
+
* box({
|
|
14
|
+
* width, // Reactive! Changes to width.value update the UI
|
|
15
|
+
* height: 10, // Static
|
|
16
|
+
* border: 1,
|
|
17
|
+
* children: () => {
|
|
18
|
+
* text({ content: 'Hello!' })
|
|
19
|
+
* }
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { bind, BINDING_SYMBOL } from '@rlabs-inc/signals'
|
|
25
|
+
import { ComponentType } from '../types'
|
|
26
|
+
import {
|
|
27
|
+
allocateIndex,
|
|
28
|
+
releaseIndex,
|
|
29
|
+
getCurrentParentIndex,
|
|
30
|
+
pushParentContext,
|
|
31
|
+
popParentContext,
|
|
32
|
+
} from '../engine/registry'
|
|
33
|
+
import { cleanupIndex as cleanupKeyboardListeners } from '../state/keyboard'
|
|
34
|
+
import { getVariantStyle } from '../state/theme'
|
|
35
|
+
|
|
36
|
+
// Import arrays
|
|
37
|
+
import * as core from '../engine/arrays/core'
|
|
38
|
+
import * as dimensions from '../engine/arrays/dimensions'
|
|
39
|
+
import * as spacing from '../engine/arrays/spacing'
|
|
40
|
+
import * as layout from '../engine/arrays/layout'
|
|
41
|
+
import * as visual from '../engine/arrays/visual'
|
|
42
|
+
import * as interaction from '../engine/arrays/interaction'
|
|
43
|
+
|
|
44
|
+
// Import types
|
|
45
|
+
import type { BoxProps, Cleanup } from './types'
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// HELPERS - For enum conversions only, not for extracting values!
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
/** Convert flex direction string to number */
|
|
52
|
+
function flexDirectionToNum(dir: string | undefined): number {
|
|
53
|
+
switch (dir) {
|
|
54
|
+
case 'row': return 1
|
|
55
|
+
case 'column-reverse': return 2
|
|
56
|
+
case 'row-reverse': return 3
|
|
57
|
+
default: return 0 // column
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Convert flex wrap string to number */
|
|
62
|
+
function flexWrapToNum(wrap: string | undefined): number {
|
|
63
|
+
switch (wrap) {
|
|
64
|
+
case 'wrap': return 1
|
|
65
|
+
case 'wrap-reverse': return 2
|
|
66
|
+
default: return 0 // nowrap
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Convert justify content string to number */
|
|
71
|
+
function justifyToNum(justify: string | undefined): number {
|
|
72
|
+
switch (justify) {
|
|
73
|
+
case 'center': return 1
|
|
74
|
+
case 'flex-end': return 2
|
|
75
|
+
case 'space-between': return 3
|
|
76
|
+
case 'space-around': return 4
|
|
77
|
+
case 'space-evenly': return 5
|
|
78
|
+
default: return 0 // flex-start
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Convert align items string to number */
|
|
83
|
+
function alignToNum(align: string | undefined): number {
|
|
84
|
+
switch (align) {
|
|
85
|
+
case 'flex-start': return 1
|
|
86
|
+
case 'center': return 2
|
|
87
|
+
case 'flex-end': return 3
|
|
88
|
+
case 'baseline': return 4
|
|
89
|
+
default: return 0 // stretch
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Convert overflow string to number */
|
|
94
|
+
function overflowToNum(overflow: string | undefined): number {
|
|
95
|
+
switch (overflow) {
|
|
96
|
+
case 'hidden': return 1
|
|
97
|
+
case 'scroll': return 2
|
|
98
|
+
case 'auto': return 3
|
|
99
|
+
default: return 0 // visible
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Convert align-self string to number (0 = auto) */
|
|
104
|
+
function alignSelfToNum(align: string | undefined): number {
|
|
105
|
+
switch (align) {
|
|
106
|
+
case 'stretch': return 1
|
|
107
|
+
case 'flex-start': return 2
|
|
108
|
+
case 'center': return 3
|
|
109
|
+
case 'flex-end': return 4
|
|
110
|
+
case 'baseline': return 5
|
|
111
|
+
default: return 0 // auto (use parent's alignItems)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a binding for enum props that converts at read time.
|
|
117
|
+
* No derived needed - reads signal directly and converts inline.
|
|
118
|
+
* This creates dependency directly on user's signal, no intermediate objects.
|
|
119
|
+
*/
|
|
120
|
+
function bindEnumProp<T extends string>(
|
|
121
|
+
prop: T | { value: T } | undefined,
|
|
122
|
+
converter: (val: T | undefined) => number
|
|
123
|
+
): ReturnType<typeof bind<number>> {
|
|
124
|
+
// If it's reactive (has .value), create binding that converts at read time
|
|
125
|
+
if (prop !== undefined && typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
126
|
+
const reactiveSource = prop as { value: T }
|
|
127
|
+
return {
|
|
128
|
+
[BINDING_SYMBOL]: true,
|
|
129
|
+
get value(): number {
|
|
130
|
+
return converter(reactiveSource.value)
|
|
131
|
+
},
|
|
132
|
+
set value(_: number) {
|
|
133
|
+
// Enum props are read-only from number side
|
|
134
|
+
},
|
|
135
|
+
} as unknown as ReturnType<typeof bind<number>>
|
|
136
|
+
}
|
|
137
|
+
// Static value - just convert and bind
|
|
138
|
+
return bind(converter(prop as T | undefined))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Get static boolean for visible prop */
|
|
142
|
+
function getStaticBool(prop: unknown, defaultVal: boolean): boolean {
|
|
143
|
+
if (prop === undefined) return defaultVal
|
|
144
|
+
if (typeof prop === 'boolean') return prop
|
|
145
|
+
if (typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
146
|
+
return (prop as { value: boolean }).value
|
|
147
|
+
}
|
|
148
|
+
return defaultVal
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// BOX COMPONENT
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a box container component.
|
|
157
|
+
*
|
|
158
|
+
* Boxes are the building blocks of layouts. They can:
|
|
159
|
+
* - Have borders and backgrounds
|
|
160
|
+
* - Use flexbox for child layout
|
|
161
|
+
* - Contain other components as children
|
|
162
|
+
* - Scroll their content
|
|
163
|
+
*
|
|
164
|
+
* Pass signals directly for reactive props - they stay connected!
|
|
165
|
+
*/
|
|
166
|
+
export function box(props: BoxProps = {}): Cleanup {
|
|
167
|
+
const index = allocateIndex(props.id)
|
|
168
|
+
|
|
169
|
+
// ==========================================================================
|
|
170
|
+
// CORE - Always needed
|
|
171
|
+
// ==========================================================================
|
|
172
|
+
core.componentType[index] = ComponentType.BOX
|
|
173
|
+
core.parentIndex[index] = bind(getCurrentParentIndex())
|
|
174
|
+
|
|
175
|
+
// Visible - only bind if passed (default is visible, handled by TITAN)
|
|
176
|
+
if (props.visible !== undefined) {
|
|
177
|
+
core.visible[index] = bind(props.visible)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
// DIMENSIONS - Only bind what's passed (TITAN uses ?? 0 for undefined)
|
|
182
|
+
// ==========================================================================
|
|
183
|
+
if (props.width !== undefined) dimensions.width[index] = bind(props.width)
|
|
184
|
+
if (props.height !== undefined) dimensions.height[index] = bind(props.height)
|
|
185
|
+
if (props.minWidth !== undefined) dimensions.minWidth[index] = bind(props.minWidth)
|
|
186
|
+
if (props.maxWidth !== undefined) dimensions.maxWidth[index] = bind(props.maxWidth)
|
|
187
|
+
if (props.minHeight !== undefined) dimensions.minHeight[index] = bind(props.minHeight)
|
|
188
|
+
if (props.maxHeight !== undefined) dimensions.maxHeight[index] = bind(props.maxHeight)
|
|
189
|
+
|
|
190
|
+
// ==========================================================================
|
|
191
|
+
// PADDING - Shorthand support: padding sets all 4, individual overrides
|
|
192
|
+
// ==========================================================================
|
|
193
|
+
if (props.padding !== undefined) {
|
|
194
|
+
// Shorthand - set all 4 sides
|
|
195
|
+
spacing.paddingTop[index] = bind(props.paddingTop ?? props.padding)
|
|
196
|
+
spacing.paddingRight[index] = bind(props.paddingRight ?? props.padding)
|
|
197
|
+
spacing.paddingBottom[index] = bind(props.paddingBottom ?? props.padding)
|
|
198
|
+
spacing.paddingLeft[index] = bind(props.paddingLeft ?? props.padding)
|
|
199
|
+
} else {
|
|
200
|
+
// Individual only - bind only what's passed
|
|
201
|
+
if (props.paddingTop !== undefined) spacing.paddingTop[index] = bind(props.paddingTop)
|
|
202
|
+
if (props.paddingRight !== undefined) spacing.paddingRight[index] = bind(props.paddingRight)
|
|
203
|
+
if (props.paddingBottom !== undefined) spacing.paddingBottom[index] = bind(props.paddingBottom)
|
|
204
|
+
if (props.paddingLeft !== undefined) spacing.paddingLeft[index] = bind(props.paddingLeft)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ==========================================================================
|
|
208
|
+
// MARGIN - Shorthand support: margin sets all 4, individual overrides
|
|
209
|
+
// ==========================================================================
|
|
210
|
+
if (props.margin !== undefined) {
|
|
211
|
+
// Shorthand - set all 4 sides
|
|
212
|
+
spacing.marginTop[index] = bind(props.marginTop ?? props.margin)
|
|
213
|
+
spacing.marginRight[index] = bind(props.marginRight ?? props.margin)
|
|
214
|
+
spacing.marginBottom[index] = bind(props.marginBottom ?? props.margin)
|
|
215
|
+
spacing.marginLeft[index] = bind(props.marginLeft ?? props.margin)
|
|
216
|
+
} else {
|
|
217
|
+
// Individual only - bind only what's passed
|
|
218
|
+
if (props.marginTop !== undefined) spacing.marginTop[index] = bind(props.marginTop)
|
|
219
|
+
if (props.marginRight !== undefined) spacing.marginRight[index] = bind(props.marginRight)
|
|
220
|
+
if (props.marginBottom !== undefined) spacing.marginBottom[index] = bind(props.marginBottom)
|
|
221
|
+
if (props.marginLeft !== undefined) spacing.marginLeft[index] = bind(props.marginLeft)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Gap - only bind if passed
|
|
225
|
+
if (props.gap !== undefined) spacing.gap[index] = bind(props.gap)
|
|
226
|
+
|
|
227
|
+
// ==========================================================================
|
|
228
|
+
// LAYOUT - Only bind what's passed (TITAN uses sensible defaults)
|
|
229
|
+
// ==========================================================================
|
|
230
|
+
if (props.flexDirection !== undefined) layout.flexDirection[index] = bindEnumProp(props.flexDirection, flexDirectionToNum)
|
|
231
|
+
if (props.flexWrap !== undefined) layout.flexWrap[index] = bindEnumProp(props.flexWrap, flexWrapToNum)
|
|
232
|
+
if (props.justifyContent !== undefined) layout.justifyContent[index] = bindEnumProp(props.justifyContent, justifyToNum)
|
|
233
|
+
if (props.alignItems !== undefined) layout.alignItems[index] = bindEnumProp(props.alignItems, alignToNum)
|
|
234
|
+
if (props.overflow !== undefined) layout.overflow[index] = bindEnumProp(props.overflow, overflowToNum)
|
|
235
|
+
if (props.grow !== undefined) layout.flexGrow[index] = bind(props.grow)
|
|
236
|
+
if (props.shrink !== undefined) layout.flexShrink[index] = bind(props.shrink)
|
|
237
|
+
if (props.flexBasis !== undefined) layout.flexBasis[index] = bind(props.flexBasis)
|
|
238
|
+
if (props.zIndex !== undefined) layout.zIndex[index] = bind(props.zIndex)
|
|
239
|
+
if (props.alignSelf !== undefined) layout.alignSelf[index] = bindEnumProp(props.alignSelf, alignSelfToNum)
|
|
240
|
+
|
|
241
|
+
// ==========================================================================
|
|
242
|
+
// INTERACTION - Only bind if focusable
|
|
243
|
+
// ==========================================================================
|
|
244
|
+
if (props.focusable) {
|
|
245
|
+
interaction.focusable[index] = bind(1)
|
|
246
|
+
if (props.tabIndex !== undefined) interaction.tabIndex[index] = bind(props.tabIndex)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ==========================================================================
|
|
250
|
+
// VISUAL - Colors and borders (only bind what's passed)
|
|
251
|
+
// ==========================================================================
|
|
252
|
+
if (props.variant && props.variant !== 'default') {
|
|
253
|
+
// Variant colors - inline bindings that read theme at read time (no deriveds!)
|
|
254
|
+
const variant = props.variant
|
|
255
|
+
if (props.fg !== undefined) {
|
|
256
|
+
visual.fgColor[index] = bind(props.fg)
|
|
257
|
+
} else {
|
|
258
|
+
visual.fgColor[index] = {
|
|
259
|
+
[BINDING_SYMBOL]: true,
|
|
260
|
+
get value() { return getVariantStyle(variant).fg },
|
|
261
|
+
set value(_) {},
|
|
262
|
+
} as any
|
|
263
|
+
}
|
|
264
|
+
if (props.bg !== undefined) {
|
|
265
|
+
visual.bgColor[index] = bind(props.bg)
|
|
266
|
+
} else {
|
|
267
|
+
visual.bgColor[index] = {
|
|
268
|
+
[BINDING_SYMBOL]: true,
|
|
269
|
+
get value() { return getVariantStyle(variant).bg },
|
|
270
|
+
set value(_) {},
|
|
271
|
+
} as any
|
|
272
|
+
}
|
|
273
|
+
if (props.borderColor !== undefined) {
|
|
274
|
+
visual.borderColor[index] = bind(props.borderColor)
|
|
275
|
+
} else {
|
|
276
|
+
visual.borderColor[index] = {
|
|
277
|
+
[BINDING_SYMBOL]: true,
|
|
278
|
+
get value() { return getVariantStyle(variant).border },
|
|
279
|
+
set value(_) {},
|
|
280
|
+
} as any
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
// Direct colors - only bind if passed
|
|
284
|
+
if (props.fg !== undefined) visual.fgColor[index] = bind(props.fg)
|
|
285
|
+
if (props.bg !== undefined) visual.bgColor[index] = bind(props.bg)
|
|
286
|
+
if (props.borderColor !== undefined) visual.borderColor[index] = bind(props.borderColor)
|
|
287
|
+
}
|
|
288
|
+
if (props.opacity !== undefined) visual.opacity[index] = bind(props.opacity)
|
|
289
|
+
|
|
290
|
+
// Border style - shorthand and individual
|
|
291
|
+
if (props.border !== undefined) visual.borderStyle[index] = bind(props.border)
|
|
292
|
+
if (props.borderTop !== undefined) visual.borderTop[index] = bind(props.borderTop)
|
|
293
|
+
if (props.borderRight !== undefined) visual.borderRight[index] = bind(props.borderRight)
|
|
294
|
+
if (props.borderBottom !== undefined) visual.borderBottom[index] = bind(props.borderBottom)
|
|
295
|
+
if (props.borderLeft !== undefined) visual.borderLeft[index] = bind(props.borderLeft)
|
|
296
|
+
|
|
297
|
+
// Render children with this box as parent context
|
|
298
|
+
if (props.children) {
|
|
299
|
+
pushParentContext(index)
|
|
300
|
+
try {
|
|
301
|
+
props.children()
|
|
302
|
+
} finally {
|
|
303
|
+
popParentContext()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Cleanup function
|
|
308
|
+
return () => {
|
|
309
|
+
cleanupKeyboardListeners(index) // Remove any focused key handlers
|
|
310
|
+
releaseIndex(index)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Primitives
|
|
3
|
+
*
|
|
4
|
+
* The building blocks for creating terminal UIs.
|
|
5
|
+
* All primitives use bind() for reactive props - no effects needed!
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { box } from './box'
|
|
9
|
+
export { text } from './text'
|
|
10
|
+
|
|
11
|
+
// Types
|
|
12
|
+
export type { BoxProps, TextProps, Cleanup } from './types'
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Text Primitive
|
|
3
|
+
*
|
|
4
|
+
* Display text with styling, alignment, and wrapping.
|
|
5
|
+
*
|
|
6
|
+
* REACTIVITY: Props are passed directly to bind() to preserve reactive links.
|
|
7
|
+
* Don't extract values before binding - that breaks the connection!
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Static text
|
|
12
|
+
* text({ content: 'Hello, World!' })
|
|
13
|
+
*
|
|
14
|
+
* // Reactive text - pass the signal directly!
|
|
15
|
+
* const message = signal('Hello')
|
|
16
|
+
* text({ content: message })
|
|
17
|
+
* message.value = 'Updated!' // UI reacts automatically!
|
|
18
|
+
*
|
|
19
|
+
* // Reactive with derived
|
|
20
|
+
* const count = signal(0)
|
|
21
|
+
* const countText = derived(() => `Count: ${count.value}`)
|
|
22
|
+
* text({ content: countText })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { bind, BINDING_SYMBOL } from '@rlabs-inc/signals'
|
|
27
|
+
import { ComponentType, Attr } from '../types'
|
|
28
|
+
import { allocateIndex, releaseIndex, getCurrentParentIndex } from '../engine/registry'
|
|
29
|
+
import { cleanupIndex as cleanupKeyboardListeners } from '../state/keyboard'
|
|
30
|
+
import { getVariantStyle } from '../state/theme'
|
|
31
|
+
|
|
32
|
+
// Import arrays
|
|
33
|
+
import * as core from '../engine/arrays/core'
|
|
34
|
+
import * as dimensions from '../engine/arrays/dimensions'
|
|
35
|
+
import * as spacing from '../engine/arrays/spacing'
|
|
36
|
+
import * as layout from '../engine/arrays/layout'
|
|
37
|
+
import * as visual from '../engine/arrays/visual'
|
|
38
|
+
import * as textArrays from '../engine/arrays/text'
|
|
39
|
+
|
|
40
|
+
// Import types
|
|
41
|
+
import type { TextProps, Cleanup } from './types'
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// HELPERS - For enum conversions with reactive support!
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/** Convert align string to number */
|
|
48
|
+
function alignToNum(align: string | undefined): number {
|
|
49
|
+
switch (align) {
|
|
50
|
+
case 'center': return 1
|
|
51
|
+
case 'right': return 2
|
|
52
|
+
default: return 0 // left
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Convert wrap string to number */
|
|
57
|
+
function wrapToNum(wrap: string | undefined): number {
|
|
58
|
+
switch (wrap) {
|
|
59
|
+
case 'nowrap': return 0
|
|
60
|
+
case 'truncate': return 2
|
|
61
|
+
default: return 1 // wrap
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a binding for enum props that converts at read time.
|
|
67
|
+
* No derived needed - reads signal directly and converts inline.
|
|
68
|
+
* This creates dependency directly on user's signal, no intermediate objects.
|
|
69
|
+
*/
|
|
70
|
+
function bindEnumProp<T extends string>(
|
|
71
|
+
prop: T | { value: T } | undefined,
|
|
72
|
+
converter: (val: T | undefined) => number
|
|
73
|
+
): ReturnType<typeof bind<number>> {
|
|
74
|
+
// If it's reactive (has .value), create binding that converts at read time
|
|
75
|
+
if (prop !== undefined && typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
76
|
+
const reactiveSource = prop as { value: T }
|
|
77
|
+
return {
|
|
78
|
+
[BINDING_SYMBOL]: true,
|
|
79
|
+
get value(): number {
|
|
80
|
+
return converter(reactiveSource.value)
|
|
81
|
+
},
|
|
82
|
+
set value(_: number) {
|
|
83
|
+
// Enum props are read-only from number side
|
|
84
|
+
},
|
|
85
|
+
} as unknown as ReturnType<typeof bind<number>>
|
|
86
|
+
}
|
|
87
|
+
// Static value - just convert
|
|
88
|
+
return bind(converter(prop as T | undefined))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// TEXT COMPONENT
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a text display component.
|
|
97
|
+
*
|
|
98
|
+
* Pass signals directly for reactive content - they stay connected!
|
|
99
|
+
* The pipeline reads via unwrap() which tracks dependencies.
|
|
100
|
+
*
|
|
101
|
+
* Supports all theme variants:
|
|
102
|
+
* - Core: default, primary, secondary, tertiary, accent
|
|
103
|
+
* - Status: success, warning, error, info
|
|
104
|
+
* - Surface: muted, surface, elevated, ghost, outline
|
|
105
|
+
*/
|
|
106
|
+
export function text(props: TextProps): Cleanup {
|
|
107
|
+
const index = allocateIndex()
|
|
108
|
+
|
|
109
|
+
// ==========================================================================
|
|
110
|
+
// CORE - Always needed
|
|
111
|
+
// ==========================================================================
|
|
112
|
+
core.componentType[index] = ComponentType.TEXT
|
|
113
|
+
core.parentIndex[index] = bind(getCurrentParentIndex())
|
|
114
|
+
|
|
115
|
+
// Visible - only bind if passed
|
|
116
|
+
if (props.visible !== undefined) {
|
|
117
|
+
core.visible[index] = bind(props.visible)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ==========================================================================
|
|
121
|
+
// TEXT CONTENT - Always needed (this is a text component!)
|
|
122
|
+
// ==========================================================================
|
|
123
|
+
textArrays.textContent[index] = bind(props.content)
|
|
124
|
+
|
|
125
|
+
// Text styling - only bind if passed
|
|
126
|
+
if (props.attrs !== undefined) textArrays.textAttrs[index] = bind(props.attrs)
|
|
127
|
+
if (props.align !== undefined) textArrays.textAlign[index] = bindEnumProp(props.align, alignToNum)
|
|
128
|
+
if (props.wrap !== undefined) textArrays.textWrap[index] = bindEnumProp(props.wrap, wrapToNum)
|
|
129
|
+
|
|
130
|
+
// ==========================================================================
|
|
131
|
+
// DIMENSIONS - Only bind what's passed (TITAN uses ?? 0 for undefined)
|
|
132
|
+
// ==========================================================================
|
|
133
|
+
if (props.width !== undefined) dimensions.width[index] = bind(props.width)
|
|
134
|
+
if (props.height !== undefined) dimensions.height[index] = bind(props.height)
|
|
135
|
+
if (props.minWidth !== undefined) dimensions.minWidth[index] = bind(props.minWidth)
|
|
136
|
+
if (props.maxWidth !== undefined) dimensions.maxWidth[index] = bind(props.maxWidth)
|
|
137
|
+
if (props.minHeight !== undefined) dimensions.minHeight[index] = bind(props.minHeight)
|
|
138
|
+
if (props.maxHeight !== undefined) dimensions.maxHeight[index] = bind(props.maxHeight)
|
|
139
|
+
|
|
140
|
+
// ==========================================================================
|
|
141
|
+
// PADDING - Shorthand support
|
|
142
|
+
// ==========================================================================
|
|
143
|
+
if (props.padding !== undefined) {
|
|
144
|
+
spacing.paddingTop[index] = bind(props.paddingTop ?? props.padding)
|
|
145
|
+
spacing.paddingRight[index] = bind(props.paddingRight ?? props.padding)
|
|
146
|
+
spacing.paddingBottom[index] = bind(props.paddingBottom ?? props.padding)
|
|
147
|
+
spacing.paddingLeft[index] = bind(props.paddingLeft ?? props.padding)
|
|
148
|
+
} else {
|
|
149
|
+
if (props.paddingTop !== undefined) spacing.paddingTop[index] = bind(props.paddingTop)
|
|
150
|
+
if (props.paddingRight !== undefined) spacing.paddingRight[index] = bind(props.paddingRight)
|
|
151
|
+
if (props.paddingBottom !== undefined) spacing.paddingBottom[index] = bind(props.paddingBottom)
|
|
152
|
+
if (props.paddingLeft !== undefined) spacing.paddingLeft[index] = bind(props.paddingLeft)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ==========================================================================
|
|
156
|
+
// FLEX ITEM - Only bind if passed (text can be a flex item)
|
|
157
|
+
// ==========================================================================
|
|
158
|
+
if (props.grow !== undefined) layout.flexGrow[index] = bind(props.grow)
|
|
159
|
+
if (props.shrink !== undefined) layout.flexShrink[index] = bind(props.shrink)
|
|
160
|
+
if (props.flexBasis !== undefined) layout.flexBasis[index] = bind(props.flexBasis)
|
|
161
|
+
if (props.alignSelf !== undefined) layout.alignSelf[index] = bind(props.alignSelf)
|
|
162
|
+
|
|
163
|
+
// ==========================================================================
|
|
164
|
+
// VISUAL - Colors with variant support (only bind what's needed)
|
|
165
|
+
// ==========================================================================
|
|
166
|
+
if (props.variant && props.variant !== 'default') {
|
|
167
|
+
// Variant colors - inline bindings that read theme at read time (no deriveds!)
|
|
168
|
+
const variant = props.variant
|
|
169
|
+
if (props.fg !== undefined) {
|
|
170
|
+
visual.fgColor[index] = bind(props.fg)
|
|
171
|
+
} else {
|
|
172
|
+
visual.fgColor[index] = {
|
|
173
|
+
[BINDING_SYMBOL]: true,
|
|
174
|
+
get value() { return getVariantStyle(variant).fg },
|
|
175
|
+
set value(_) {},
|
|
176
|
+
} as any
|
|
177
|
+
}
|
|
178
|
+
if (props.bg !== undefined) {
|
|
179
|
+
visual.bgColor[index] = bind(props.bg)
|
|
180
|
+
} else {
|
|
181
|
+
visual.bgColor[index] = {
|
|
182
|
+
[BINDING_SYMBOL]: true,
|
|
183
|
+
get value() { return getVariantStyle(variant).bg },
|
|
184
|
+
set value(_) {},
|
|
185
|
+
} as any
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// Direct colors - only bind if passed
|
|
189
|
+
if (props.fg !== undefined) visual.fgColor[index] = bind(props.fg)
|
|
190
|
+
if (props.bg !== undefined) visual.bgColor[index] = bind(props.bg)
|
|
191
|
+
}
|
|
192
|
+
if (props.opacity !== undefined) visual.opacity[index] = bind(props.opacity)
|
|
193
|
+
|
|
194
|
+
// Cleanup function
|
|
195
|
+
return () => {
|
|
196
|
+
cleanupKeyboardListeners(index)
|
|
197
|
+
releaseIndex(index)
|
|
198
|
+
}
|
|
199
|
+
}
|