@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,687 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Reactive Theme System
|
|
3
|
+
*
|
|
4
|
+
* Semantic colors with OKLCH support and terminal default fallbacks.
|
|
5
|
+
* The theme is reactive - changing colors triggers re-render.
|
|
6
|
+
*
|
|
7
|
+
* Color values:
|
|
8
|
+
* - null/undefined: Use terminal's default color (respects user's terminal theme)
|
|
9
|
+
* - 0-15: ANSI color index (respects terminal's color palette)
|
|
10
|
+
* - 16-255: Extended 256-color palette
|
|
11
|
+
* - 0x100+: RGB color (0xRRGGBB format)
|
|
12
|
+
* - string: CSS color parsed via parseColor()
|
|
13
|
+
*
|
|
14
|
+
* The DEFAULT theme uses terminal colors so the UI matches the user's terminal.
|
|
15
|
+
* Custom themes (Dracula, Nord, etc.) override with specific RGB values.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { state, derived } from '@rlabs-inc/signals'
|
|
19
|
+
import type { RGBA } from '../types'
|
|
20
|
+
import { parseColor, TERMINAL_DEFAULT, ansiColor } from '../types/color'
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// THEME COLOR TYPE
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Theme color can be:
|
|
28
|
+
* - null: Terminal default
|
|
29
|
+
* - number 0-15: ANSI color index
|
|
30
|
+
* - number 16-255: Extended palette
|
|
31
|
+
* - number > 255: RGB (0xRRGGBB)
|
|
32
|
+
* - string: CSS color (hex, rgb, oklch, etc.)
|
|
33
|
+
*/
|
|
34
|
+
export type ThemeColor = null | number | string
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// REACTIVE THEME STATE
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The theme object - all properties are reactive.
|
|
42
|
+
* Changing any color triggers dependent deriveds/effects.
|
|
43
|
+
*
|
|
44
|
+
* Default values use ANSI indices to respect terminal color schemes.
|
|
45
|
+
*/
|
|
46
|
+
export const theme = state({
|
|
47
|
+
// =========================================================================
|
|
48
|
+
// MAIN PALETTE - Using ANSI colors that respect terminal theme
|
|
49
|
+
// =========================================================================
|
|
50
|
+
|
|
51
|
+
/** Primary brand color - ANSI bright blue (12) */
|
|
52
|
+
primary: 12 as ThemeColor,
|
|
53
|
+
|
|
54
|
+
/** Secondary accent - ANSI bright magenta (13) */
|
|
55
|
+
secondary: 13 as ThemeColor,
|
|
56
|
+
|
|
57
|
+
/** Tertiary color - ANSI bright cyan (14) */
|
|
58
|
+
tertiary: 14 as ThemeColor,
|
|
59
|
+
|
|
60
|
+
/** Accent for highlights - ANSI bright yellow (11) */
|
|
61
|
+
accent: 11 as ThemeColor,
|
|
62
|
+
|
|
63
|
+
// =========================================================================
|
|
64
|
+
// SEMANTIC COLORS
|
|
65
|
+
// =========================================================================
|
|
66
|
+
|
|
67
|
+
/** Success/positive - ANSI green (2) */
|
|
68
|
+
success: 2 as ThemeColor,
|
|
69
|
+
|
|
70
|
+
/** Warning/caution - ANSI yellow (3) */
|
|
71
|
+
warning: 3 as ThemeColor,
|
|
72
|
+
|
|
73
|
+
/** Error/danger - ANSI red (1) */
|
|
74
|
+
error: 1 as ThemeColor,
|
|
75
|
+
|
|
76
|
+
/** Informational - ANSI cyan (6) */
|
|
77
|
+
info: 6 as ThemeColor,
|
|
78
|
+
|
|
79
|
+
// =========================================================================
|
|
80
|
+
// TEXT COLORS
|
|
81
|
+
// =========================================================================
|
|
82
|
+
|
|
83
|
+
/** Primary text - terminal default */
|
|
84
|
+
text: null as ThemeColor,
|
|
85
|
+
|
|
86
|
+
/** Muted text - ANSI bright black/gray (8) */
|
|
87
|
+
textMuted: 8 as ThemeColor,
|
|
88
|
+
|
|
89
|
+
/** Dimmed text - same as muted */
|
|
90
|
+
textDim: 8 as ThemeColor,
|
|
91
|
+
|
|
92
|
+
/** Disabled text */
|
|
93
|
+
textDisabled: 8 as ThemeColor,
|
|
94
|
+
|
|
95
|
+
/** Bright/emphasized text - ANSI bright white (15) */
|
|
96
|
+
textBright: 15 as ThemeColor,
|
|
97
|
+
|
|
98
|
+
// =========================================================================
|
|
99
|
+
// BACKGROUND COLORS
|
|
100
|
+
// =========================================================================
|
|
101
|
+
|
|
102
|
+
/** Primary background - terminal default */
|
|
103
|
+
background: null as ThemeColor,
|
|
104
|
+
|
|
105
|
+
/** Muted background - terminal default (let terminal handle it) */
|
|
106
|
+
backgroundMuted: null as ThemeColor,
|
|
107
|
+
|
|
108
|
+
/** Surface (cards, panels) - terminal default */
|
|
109
|
+
surface: null as ThemeColor,
|
|
110
|
+
|
|
111
|
+
/** Overlay (modals) - terminal default */
|
|
112
|
+
overlay: null as ThemeColor,
|
|
113
|
+
|
|
114
|
+
// =========================================================================
|
|
115
|
+
// BORDER COLORS
|
|
116
|
+
// =========================================================================
|
|
117
|
+
|
|
118
|
+
/** Default border - ANSI white (7) */
|
|
119
|
+
border: 7 as ThemeColor,
|
|
120
|
+
|
|
121
|
+
/** Focused border - primary color */
|
|
122
|
+
borderFocus: 12 as ThemeColor,
|
|
123
|
+
|
|
124
|
+
// =========================================================================
|
|
125
|
+
// METADATA
|
|
126
|
+
// =========================================================================
|
|
127
|
+
|
|
128
|
+
name: 'terminal',
|
|
129
|
+
description: 'Uses terminal default colors',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// =============================================================================
|
|
133
|
+
// THEME PRESETS
|
|
134
|
+
// =============================================================================
|
|
135
|
+
|
|
136
|
+
export const themes = {
|
|
137
|
+
/**
|
|
138
|
+
* Terminal default - uses ANSI colors to respect user's terminal theme.
|
|
139
|
+
* This should be the default for most applications.
|
|
140
|
+
*/
|
|
141
|
+
terminal: {
|
|
142
|
+
name: 'terminal',
|
|
143
|
+
description: 'Uses terminal default colors',
|
|
144
|
+
primary: 12, // bright blue
|
|
145
|
+
secondary: 13, // bright magenta
|
|
146
|
+
tertiary: 14, // bright cyan
|
|
147
|
+
accent: 11, // bright yellow
|
|
148
|
+
success: 2, // green
|
|
149
|
+
warning: 3, // yellow
|
|
150
|
+
error: 1, // red
|
|
151
|
+
info: 6, // cyan
|
|
152
|
+
text: null,
|
|
153
|
+
textMuted: 8,
|
|
154
|
+
textDim: 8,
|
|
155
|
+
textDisabled: 8,
|
|
156
|
+
textBright: 15,
|
|
157
|
+
background: null,
|
|
158
|
+
backgroundMuted: null,
|
|
159
|
+
surface: null,
|
|
160
|
+
overlay: null,
|
|
161
|
+
border: 7,
|
|
162
|
+
borderFocus: 12,
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Dracula - dark theme with vivid colors.
|
|
167
|
+
* Uses OKLCH for perceptually uniform colors.
|
|
168
|
+
*/
|
|
169
|
+
dracula: {
|
|
170
|
+
name: 'dracula',
|
|
171
|
+
description: 'Dracula dark theme',
|
|
172
|
+
primary: 'oklch(0.75 0.15 300)', // purple
|
|
173
|
+
secondary: 'oklch(0.75 0.2 340)', // pink
|
|
174
|
+
tertiary: 'oklch(0.85 0.12 200)', // cyan
|
|
175
|
+
accent: 'oklch(0.9 0.15 100)', // yellow
|
|
176
|
+
success: 'oklch(0.8 0.2 140)', // green
|
|
177
|
+
warning: 'oklch(0.9 0.15 100)', // yellow
|
|
178
|
+
error: 'oklch(0.7 0.25 25)', // red
|
|
179
|
+
info: 'oklch(0.85 0.12 200)', // cyan
|
|
180
|
+
text: 0xf8f8f2,
|
|
181
|
+
textMuted: 0x6272a4,
|
|
182
|
+
textDim: 0x6272a4,
|
|
183
|
+
textDisabled: 0x44475a,
|
|
184
|
+
textBright: 0xffffff,
|
|
185
|
+
background: 0x282a36,
|
|
186
|
+
backgroundMuted: 0x343746,
|
|
187
|
+
surface: 0x44475a,
|
|
188
|
+
overlay: 0x21222c,
|
|
189
|
+
border: 0x6272a4,
|
|
190
|
+
borderFocus: 0xbd93f9,
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Nord - arctic, bluish colors.
|
|
195
|
+
*/
|
|
196
|
+
nord: {
|
|
197
|
+
name: 'nord',
|
|
198
|
+
description: 'Nord arctic theme',
|
|
199
|
+
primary: 'oklch(0.8 0.08 210)', // frost cyan
|
|
200
|
+
secondary: 'oklch(0.7 0.08 230)', // frost blue
|
|
201
|
+
tertiary: 'oklch(0.6 0.1 250)', // frost dark blue
|
|
202
|
+
accent: 'oklch(0.7 0.12 50)', // aurora orange
|
|
203
|
+
success: 'oklch(0.75 0.1 130)', // aurora green
|
|
204
|
+
warning: 'oklch(0.85 0.1 90)', // aurora yellow
|
|
205
|
+
error: 'oklch(0.65 0.15 20)', // aurora red
|
|
206
|
+
info: 'oklch(0.8 0.08 210)', // frost cyan
|
|
207
|
+
text: 0xd8dee9,
|
|
208
|
+
textMuted: 0x4c566a,
|
|
209
|
+
textDim: 0x4c566a,
|
|
210
|
+
textDisabled: 0x3b4252,
|
|
211
|
+
textBright: 0xeceff4,
|
|
212
|
+
background: 0x2e3440,
|
|
213
|
+
backgroundMuted: 0x3b4252,
|
|
214
|
+
surface: 0x434c5e,
|
|
215
|
+
overlay: 0x2e3440,
|
|
216
|
+
border: 0x4c566a,
|
|
217
|
+
borderFocus: 0x88c0d0,
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Monokai - vibrant syntax-highlighting inspired theme.
|
|
222
|
+
*/
|
|
223
|
+
monokai: {
|
|
224
|
+
name: 'monokai',
|
|
225
|
+
description: 'Monokai vibrant theme',
|
|
226
|
+
primary: 'oklch(0.65 0.25 350)', // pink
|
|
227
|
+
secondary: 'oklch(0.85 0.25 125)', // green
|
|
228
|
+
tertiary: 'oklch(0.7 0.2 300)', // purple
|
|
229
|
+
accent: 'oklch(0.75 0.18 60)', // orange
|
|
230
|
+
success: 'oklch(0.85 0.25 125)', // green
|
|
231
|
+
warning: 'oklch(0.75 0.18 60)', // orange
|
|
232
|
+
error: 'oklch(0.65 0.25 350)', // pink
|
|
233
|
+
info: 'oklch(0.8 0.12 220)', // blue
|
|
234
|
+
text: 0xf8f8f2,
|
|
235
|
+
textMuted: 0x75715e,
|
|
236
|
+
textDim: 0x75715e,
|
|
237
|
+
textDisabled: 0x49483e,
|
|
238
|
+
textBright: 0xffffff,
|
|
239
|
+
background: 0x272822,
|
|
240
|
+
backgroundMuted: 0x3e3d32,
|
|
241
|
+
surface: 0x49483e,
|
|
242
|
+
overlay: 0x1e1f1c,
|
|
243
|
+
border: 0x75715e,
|
|
244
|
+
borderFocus: 0xf92672,
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Solarized Dark - precision color scheme.
|
|
249
|
+
*/
|
|
250
|
+
solarized: {
|
|
251
|
+
name: 'solarized',
|
|
252
|
+
description: 'Solarized Dark theme',
|
|
253
|
+
primary: 0x268bd2, // blue
|
|
254
|
+
secondary: 0x2aa198, // cyan
|
|
255
|
+
tertiary: 0x859900, // green
|
|
256
|
+
accent: 0xcb4b16, // orange
|
|
257
|
+
success: 0x859900, // green
|
|
258
|
+
warning: 0xb58900, // yellow
|
|
259
|
+
error: 0xdc322f, // red
|
|
260
|
+
info: 0x268bd2, // blue
|
|
261
|
+
text: 0x839496, // base0
|
|
262
|
+
textMuted: 0x586e75, // base01
|
|
263
|
+
textDim: 0x586e75,
|
|
264
|
+
textDisabled: 0x073642, // base02
|
|
265
|
+
textBright: 0x93a1a1, // base1
|
|
266
|
+
background: 0x002b36, // base03
|
|
267
|
+
backgroundMuted: 0x073642, // base02
|
|
268
|
+
surface: 0x073642,
|
|
269
|
+
overlay: 0x002b36,
|
|
270
|
+
border: 0x586e75,
|
|
271
|
+
borderFocus: 0x268bd2,
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// =============================================================================
|
|
276
|
+
// THEME FUNCTIONS
|
|
277
|
+
// =============================================================================
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Apply a theme preset or custom theme object.
|
|
281
|
+
*/
|
|
282
|
+
export function setTheme(
|
|
283
|
+
themeNameOrObject: keyof typeof themes | Partial<typeof theme>
|
|
284
|
+
): void {
|
|
285
|
+
if (typeof themeNameOrObject === 'string') {
|
|
286
|
+
const preset = themes[themeNameOrObject]
|
|
287
|
+
if (!preset) {
|
|
288
|
+
console.error(`Theme '${themeNameOrObject}' not found`)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
Object.assign(theme, preset)
|
|
292
|
+
} else {
|
|
293
|
+
Object.assign(theme, themeNameOrObject)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get list of available theme names.
|
|
299
|
+
*/
|
|
300
|
+
export function getThemeNames(): string[] {
|
|
301
|
+
return Object.keys(themes)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// COLOR RESOLUTION
|
|
306
|
+
// =============================================================================
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolve a theme color to RGBA.
|
|
310
|
+
*
|
|
311
|
+
* Handles:
|
|
312
|
+
* - null → TERMINAL_DEFAULT
|
|
313
|
+
* - 0-255 → ANSI color marker (respects terminal palette!)
|
|
314
|
+
* - > 255 → RGB (0xRRGGBB)
|
|
315
|
+
* - string → CSS color parsing (including OKLCH)
|
|
316
|
+
*/
|
|
317
|
+
export function resolveColor(color: ThemeColor): RGBA {
|
|
318
|
+
if (color === null || color === undefined) {
|
|
319
|
+
return TERMINAL_DEFAULT
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (typeof color === 'string') {
|
|
323
|
+
return parseColor(color)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ANSI colors (0-255) - return marker so renderer uses terminal's palette!
|
|
327
|
+
if (color >= 0 && color <= 255) {
|
|
328
|
+
return ansiColor(color)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// RGB value (0xRRGGBB)
|
|
332
|
+
return {
|
|
333
|
+
r: (color >> 16) & 0xff,
|
|
334
|
+
g: (color >> 8) & 0xff,
|
|
335
|
+
b: color & 0xff,
|
|
336
|
+
a: 255,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Convert ANSI 16-color index to approximate RGBA.
|
|
342
|
+
* These are approximations - actual colors depend on terminal theme.
|
|
343
|
+
*/
|
|
344
|
+
function ansi16ToRgba(ansi: number): RGBA {
|
|
345
|
+
const colors: RGBA[] = [
|
|
346
|
+
{ r: 0, g: 0, b: 0, a: 255 }, // 0: black
|
|
347
|
+
{ r: 170, g: 0, b: 0, a: 255 }, // 1: red
|
|
348
|
+
{ r: 0, g: 170, b: 0, a: 255 }, // 2: green
|
|
349
|
+
{ r: 170, g: 85, b: 0, a: 255 }, // 3: yellow
|
|
350
|
+
{ r: 0, g: 0, b: 170, a: 255 }, // 4: blue
|
|
351
|
+
{ r: 170, g: 0, b: 170, a: 255 }, // 5: magenta
|
|
352
|
+
{ r: 0, g: 170, b: 170, a: 255 }, // 6: cyan
|
|
353
|
+
{ r: 170, g: 170, b: 170, a: 255 }, // 7: white
|
|
354
|
+
{ r: 85, g: 85, b: 85, a: 255 }, // 8: bright black
|
|
355
|
+
{ r: 255, g: 85, b: 85, a: 255 }, // 9: bright red
|
|
356
|
+
{ r: 85, g: 255, b: 85, a: 255 }, // 10: bright green
|
|
357
|
+
{ r: 255, g: 255, b: 85, a: 255 }, // 11: bright yellow
|
|
358
|
+
{ r: 85, g: 85, b: 255, a: 255 }, // 12: bright blue
|
|
359
|
+
{ r: 255, g: 85, b: 255, a: 255 }, // 13: bright magenta
|
|
360
|
+
{ r: 85, g: 255, b: 255, a: 255 }, // 14: bright cyan
|
|
361
|
+
{ r: 255, g: 255, b: 255, a: 255 }, // 15: bright white
|
|
362
|
+
]
|
|
363
|
+
return colors[ansi] || TERMINAL_DEFAULT
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Convert ANSI 256-color index to RGBA.
|
|
368
|
+
*/
|
|
369
|
+
function ansi256ToRgba(index: number): RGBA {
|
|
370
|
+
// 0-15: Standard ANSI colors
|
|
371
|
+
if (index < 16) {
|
|
372
|
+
return ansi16ToRgba(index)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 16-231: 6x6x6 color cube
|
|
376
|
+
if (index < 232) {
|
|
377
|
+
const n = index - 16
|
|
378
|
+
const b = n % 6
|
|
379
|
+
const g = Math.floor(n / 6) % 6
|
|
380
|
+
const r = Math.floor(n / 36)
|
|
381
|
+
return {
|
|
382
|
+
r: r === 0 ? 0 : 55 + r * 40,
|
|
383
|
+
g: g === 0 ? 0 : 55 + g * 40,
|
|
384
|
+
b: b === 0 ? 0 : 55 + b * 40,
|
|
385
|
+
a: 255,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 232-255: Grayscale (24 shades)
|
|
390
|
+
const gray = (index - 232) * 10 + 8
|
|
391
|
+
return { r: gray, g: gray, b: gray, a: 255 }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// =============================================================================
|
|
395
|
+
// ANSI CODE GENERATION
|
|
396
|
+
// =============================================================================
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get ANSI escape code for a theme color (foreground).
|
|
400
|
+
*/
|
|
401
|
+
export function toAnsiFg(color: ThemeColor): string {
|
|
402
|
+
if (color === null || color === undefined) {
|
|
403
|
+
return '\x1b[39m' // Reset to terminal default
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (typeof color === 'string') {
|
|
407
|
+
const rgba = parseColor(color)
|
|
408
|
+
if (rgba.r === -1) return '\x1b[39m' // TERMINAL_DEFAULT
|
|
409
|
+
return `\x1b[38;2;${rgba.r};${rgba.g};${rgba.b}m`
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ANSI 16 colors
|
|
413
|
+
if (color >= 0 && color <= 7) {
|
|
414
|
+
return `\x1b[${30 + color}m`
|
|
415
|
+
}
|
|
416
|
+
if (color >= 8 && color <= 15) {
|
|
417
|
+
return `\x1b[${90 + (color - 8)}m`
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ANSI 256 colors
|
|
421
|
+
if (color >= 16 && color <= 255) {
|
|
422
|
+
return `\x1b[38;5;${color}m`
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// RGB (0xRRGGBB)
|
|
426
|
+
const r = (color >> 16) & 0xff
|
|
427
|
+
const g = (color >> 8) & 0xff
|
|
428
|
+
const b = color & 0xff
|
|
429
|
+
return `\x1b[38;2;${r};${g};${b}m`
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get ANSI escape code for a theme color (background).
|
|
434
|
+
*/
|
|
435
|
+
export function toAnsiBg(color: ThemeColor): string {
|
|
436
|
+
if (color === null || color === undefined) {
|
|
437
|
+
return '\x1b[49m' // Reset to terminal default
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (typeof color === 'string') {
|
|
441
|
+
const rgba = parseColor(color)
|
|
442
|
+
if (rgba.r === -1) return '\x1b[49m' // TERMINAL_DEFAULT
|
|
443
|
+
return `\x1b[48;2;${rgba.r};${rgba.g};${rgba.b}m`
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ANSI 16 colors
|
|
447
|
+
if (color >= 0 && color <= 7) {
|
|
448
|
+
return `\x1b[${40 + color}m`
|
|
449
|
+
}
|
|
450
|
+
if (color >= 8 && color <= 15) {
|
|
451
|
+
return `\x1b[${100 + (color - 8)}m`
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ANSI 256 colors
|
|
455
|
+
if (color >= 16 && color <= 255) {
|
|
456
|
+
return `\x1b[48;5;${color}m`
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// RGB (0xRRGGBB)
|
|
460
|
+
const r = (color >> 16) & 0xff
|
|
461
|
+
const g = (color >> 8) & 0xff
|
|
462
|
+
const b = color & 0xff
|
|
463
|
+
return `\x1b[48;2;${r};${g};${b}m`
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// =============================================================================
|
|
467
|
+
// RESOLVED THEME (derived)
|
|
468
|
+
// =============================================================================
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get current theme colors resolved to RGBA.
|
|
472
|
+
* This is a derived that updates when theme changes.
|
|
473
|
+
* Use this when you need RGBA values (e.g., for blending).
|
|
474
|
+
*/
|
|
475
|
+
export const resolvedTheme = derived(() => ({
|
|
476
|
+
primary: resolveColor(theme.primary),
|
|
477
|
+
secondary: resolveColor(theme.secondary),
|
|
478
|
+
tertiary: resolveColor(theme.tertiary),
|
|
479
|
+
accent: resolveColor(theme.accent),
|
|
480
|
+
success: resolveColor(theme.success),
|
|
481
|
+
warning: resolveColor(theme.warning),
|
|
482
|
+
error: resolveColor(theme.error),
|
|
483
|
+
info: resolveColor(theme.info),
|
|
484
|
+
text: resolveColor(theme.text),
|
|
485
|
+
textMuted: resolveColor(theme.textMuted),
|
|
486
|
+
textDim: resolveColor(theme.textDim),
|
|
487
|
+
textDisabled: resolveColor(theme.textDisabled),
|
|
488
|
+
textBright: resolveColor(theme.textBright),
|
|
489
|
+
background: resolveColor(theme.background),
|
|
490
|
+
backgroundMuted: resolveColor(theme.backgroundMuted),
|
|
491
|
+
surface: resolveColor(theme.surface),
|
|
492
|
+
overlay: resolveColor(theme.overlay),
|
|
493
|
+
border: resolveColor(theme.border),
|
|
494
|
+
borderFocus: resolveColor(theme.borderFocus),
|
|
495
|
+
}))
|
|
496
|
+
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// EASY THEME ACCESS - `t.primary` instead of `resolvedTheme.value.primary`
|
|
499
|
+
// =============================================================================
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Easy access to theme colors as reactive deriveds.
|
|
503
|
+
*
|
|
504
|
+
* Usage:
|
|
505
|
+
* ```ts
|
|
506
|
+
* import { t } from 'tui/theme'
|
|
507
|
+
*
|
|
508
|
+
* box({
|
|
509
|
+
* borderColor: t.primary, // Reactive! Updates when theme changes
|
|
510
|
+
* fg: t.text,
|
|
511
|
+
* bg: t.surface,
|
|
512
|
+
* })
|
|
513
|
+
* ```
|
|
514
|
+
*
|
|
515
|
+
* Each property is a derived that resolves the theme color to RGBA.
|
|
516
|
+
* Pass directly to component props - bind() handles the rest.
|
|
517
|
+
*/
|
|
518
|
+
export const t = {
|
|
519
|
+
// Main palette
|
|
520
|
+
primary: derived(() => resolveColor(theme.primary)),
|
|
521
|
+
secondary: derived(() => resolveColor(theme.secondary)),
|
|
522
|
+
tertiary: derived(() => resolveColor(theme.tertiary)),
|
|
523
|
+
accent: derived(() => resolveColor(theme.accent)),
|
|
524
|
+
|
|
525
|
+
// Semantic
|
|
526
|
+
success: derived(() => resolveColor(theme.success)),
|
|
527
|
+
warning: derived(() => resolveColor(theme.warning)),
|
|
528
|
+
error: derived(() => resolveColor(theme.error)),
|
|
529
|
+
info: derived(() => resolveColor(theme.info)),
|
|
530
|
+
|
|
531
|
+
// Text
|
|
532
|
+
text: derived(() => resolveColor(theme.text)),
|
|
533
|
+
textMuted: derived(() => resolveColor(theme.textMuted)),
|
|
534
|
+
textDim: derived(() => resolveColor(theme.textDim)),
|
|
535
|
+
textDisabled: derived(() => resolveColor(theme.textDisabled)),
|
|
536
|
+
textBright: derived(() => resolveColor(theme.textBright)),
|
|
537
|
+
|
|
538
|
+
// Backgrounds
|
|
539
|
+
bg: derived(() => resolveColor(theme.background)),
|
|
540
|
+
bgMuted: derived(() => resolveColor(theme.backgroundMuted)),
|
|
541
|
+
surface: derived(() => resolveColor(theme.surface)),
|
|
542
|
+
overlay: derived(() => resolveColor(theme.overlay)),
|
|
543
|
+
|
|
544
|
+
// Borders
|
|
545
|
+
border: derived(() => resolveColor(theme.border)),
|
|
546
|
+
borderFocus: derived(() => resolveColor(theme.borderFocus)),
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// =============================================================================
|
|
550
|
+
// VARIANT DEFINITIONS
|
|
551
|
+
// =============================================================================
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Variant style definitions.
|
|
555
|
+
* Each variant defines colors for different component states.
|
|
556
|
+
*/
|
|
557
|
+
export type Variant =
|
|
558
|
+
| 'default'
|
|
559
|
+
| 'primary' | 'secondary' | 'tertiary' | 'accent'
|
|
560
|
+
| 'success' | 'warning' | 'error' | 'info'
|
|
561
|
+
| 'muted' | 'surface' | 'elevated'
|
|
562
|
+
| 'ghost' | 'outline'
|
|
563
|
+
|
|
564
|
+
export interface VariantStyle {
|
|
565
|
+
fg: RGBA
|
|
566
|
+
bg: RGBA
|
|
567
|
+
border: RGBA
|
|
568
|
+
borderFocus: RGBA
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Get variant styles resolved to RGBA.
|
|
573
|
+
* Returns colors based on variant name and current theme.
|
|
574
|
+
*/
|
|
575
|
+
export function getVariantStyle(variant: Variant): VariantStyle {
|
|
576
|
+
const resolved = resolvedTheme.value
|
|
577
|
+
|
|
578
|
+
switch (variant) {
|
|
579
|
+
case 'primary':
|
|
580
|
+
return {
|
|
581
|
+
fg: resolved.textBright,
|
|
582
|
+
bg: resolved.primary,
|
|
583
|
+
border: resolved.primary,
|
|
584
|
+
borderFocus: resolved.accent,
|
|
585
|
+
}
|
|
586
|
+
case 'secondary':
|
|
587
|
+
return {
|
|
588
|
+
fg: resolved.textBright,
|
|
589
|
+
bg: resolved.secondary,
|
|
590
|
+
border: resolved.secondary,
|
|
591
|
+
borderFocus: resolved.accent,
|
|
592
|
+
}
|
|
593
|
+
case 'tertiary':
|
|
594
|
+
return {
|
|
595
|
+
fg: resolved.textBright,
|
|
596
|
+
bg: resolved.tertiary,
|
|
597
|
+
border: resolved.tertiary,
|
|
598
|
+
borderFocus: resolved.accent,
|
|
599
|
+
}
|
|
600
|
+
case 'accent':
|
|
601
|
+
return {
|
|
602
|
+
fg: { r: 0, g: 0, b: 0, a: 255 }, // Dark text on accent (usually bright)
|
|
603
|
+
bg: resolved.accent,
|
|
604
|
+
border: resolved.accent,
|
|
605
|
+
borderFocus: resolved.primary,
|
|
606
|
+
}
|
|
607
|
+
case 'success':
|
|
608
|
+
return {
|
|
609
|
+
fg: resolved.textBright,
|
|
610
|
+
bg: resolved.success,
|
|
611
|
+
border: resolved.success,
|
|
612
|
+
borderFocus: resolved.accent,
|
|
613
|
+
}
|
|
614
|
+
case 'warning':
|
|
615
|
+
return {
|
|
616
|
+
fg: { r: 0, g: 0, b: 0, a: 255 }, // Dark text on warning
|
|
617
|
+
bg: resolved.warning,
|
|
618
|
+
border: resolved.warning,
|
|
619
|
+
borderFocus: resolved.accent,
|
|
620
|
+
}
|
|
621
|
+
case 'error':
|
|
622
|
+
return {
|
|
623
|
+
fg: resolved.textBright,
|
|
624
|
+
bg: resolved.error,
|
|
625
|
+
border: resolved.error,
|
|
626
|
+
borderFocus: resolved.accent,
|
|
627
|
+
}
|
|
628
|
+
case 'info':
|
|
629
|
+
return {
|
|
630
|
+
fg: resolved.textBright,
|
|
631
|
+
bg: resolved.info,
|
|
632
|
+
border: resolved.info,
|
|
633
|
+
borderFocus: resolved.accent,
|
|
634
|
+
}
|
|
635
|
+
case 'muted':
|
|
636
|
+
return {
|
|
637
|
+
fg: resolved.textMuted,
|
|
638
|
+
bg: resolved.surface,
|
|
639
|
+
border: resolved.border,
|
|
640
|
+
borderFocus: resolved.borderFocus,
|
|
641
|
+
}
|
|
642
|
+
case 'surface':
|
|
643
|
+
return {
|
|
644
|
+
fg: resolved.text,
|
|
645
|
+
bg: resolved.surface,
|
|
646
|
+
border: resolved.border,
|
|
647
|
+
borderFocus: resolved.borderFocus,
|
|
648
|
+
}
|
|
649
|
+
case 'elevated':
|
|
650
|
+
return {
|
|
651
|
+
fg: resolved.textBright,
|
|
652
|
+
bg: resolved.surface,
|
|
653
|
+
border: resolved.primary,
|
|
654
|
+
borderFocus: resolved.borderFocus,
|
|
655
|
+
}
|
|
656
|
+
case 'ghost':
|
|
657
|
+
return {
|
|
658
|
+
fg: resolved.text,
|
|
659
|
+
bg: TERMINAL_DEFAULT,
|
|
660
|
+
border: TERMINAL_DEFAULT,
|
|
661
|
+
borderFocus: resolved.borderFocus,
|
|
662
|
+
}
|
|
663
|
+
case 'outline':
|
|
664
|
+
return {
|
|
665
|
+
fg: resolved.primary,
|
|
666
|
+
bg: TERMINAL_DEFAULT,
|
|
667
|
+
border: resolved.primary,
|
|
668
|
+
borderFocus: resolved.borderFocus,
|
|
669
|
+
}
|
|
670
|
+
case 'default':
|
|
671
|
+
default:
|
|
672
|
+
return {
|
|
673
|
+
fg: resolved.text,
|
|
674
|
+
bg: resolved.background,
|
|
675
|
+
border: resolved.border,
|
|
676
|
+
borderFocus: resolved.borderFocus,
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Reactive variant style derived.
|
|
683
|
+
* Use when you need styles to update with theme changes.
|
|
684
|
+
*/
|
|
685
|
+
export function variantStyle(variant: Variant) {
|
|
686
|
+
return derived(() => getVariantStyle(variant))
|
|
687
|
+
}
|