@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.
Files changed (44) hide show
  1. package/README.md +141 -0
  2. package/index.ts +45 -0
  3. package/package.json +59 -0
  4. package/src/api/index.ts +7 -0
  5. package/src/api/mount.ts +230 -0
  6. package/src/engine/arrays/core.ts +60 -0
  7. package/src/engine/arrays/dimensions.ts +68 -0
  8. package/src/engine/arrays/index.ts +166 -0
  9. package/src/engine/arrays/interaction.ts +112 -0
  10. package/src/engine/arrays/layout.ts +175 -0
  11. package/src/engine/arrays/spacing.ts +100 -0
  12. package/src/engine/arrays/text.ts +55 -0
  13. package/src/engine/arrays/visual.ts +140 -0
  14. package/src/engine/index.ts +25 -0
  15. package/src/engine/inheritance.ts +138 -0
  16. package/src/engine/registry.ts +180 -0
  17. package/src/pipeline/frameBuffer.ts +473 -0
  18. package/src/pipeline/layout/index.ts +105 -0
  19. package/src/pipeline/layout/titan-engine.ts +798 -0
  20. package/src/pipeline/layout/types.ts +194 -0
  21. package/src/pipeline/layout/utils/hierarchy.ts +202 -0
  22. package/src/pipeline/layout/utils/math.ts +134 -0
  23. package/src/pipeline/layout/utils/text-measure.ts +160 -0
  24. package/src/pipeline/layout.ts +30 -0
  25. package/src/primitives/box.ts +312 -0
  26. package/src/primitives/index.ts +12 -0
  27. package/src/primitives/text.ts +199 -0
  28. package/src/primitives/types.ts +222 -0
  29. package/src/primitives/utils.ts +37 -0
  30. package/src/renderer/ansi.ts +625 -0
  31. package/src/renderer/buffer.ts +667 -0
  32. package/src/renderer/index.ts +40 -0
  33. package/src/renderer/input.ts +518 -0
  34. package/src/renderer/output.ts +451 -0
  35. package/src/state/cursor.ts +176 -0
  36. package/src/state/focus.ts +241 -0
  37. package/src/state/index.ts +43 -0
  38. package/src/state/keyboard.ts +771 -0
  39. package/src/state/mouse.ts +524 -0
  40. package/src/state/scroll.ts +341 -0
  41. package/src/state/theme.ts +687 -0
  42. package/src/types/color.ts +401 -0
  43. package/src/types/index.ts +316 -0
  44. 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
+ }