@alcyone-labs/arg-parser 2.13.1 → 2.13.3

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/dist/tui.mjs ADDED
@@ -0,0 +1,1111 @@
1
+ import { useRenderer, createComponent, render } from "@opentui/solid";
2
+ import { createComponent as createComponent2, effect, insert, memo, mergeProps, render as render2, spread, useKeyboard, useRenderer as useRenderer2 } from "@opentui/solid";
3
+ import { onCleanup, createContext, createSignal, useContext, onMount, For, createMemo, Show } from "solid-js";
4
+ const EXIT_GUARD_STATE_KEY = /* @__PURE__ */ Symbol.for(
5
+ "@alcyone-labs/arg-parser/tui/ExitGuardState"
6
+ );
7
+ function getExitGuardState() {
8
+ return process[EXIT_GUARD_STATE_KEY];
9
+ }
10
+ function setExitGuardState(state) {
11
+ if (state) {
12
+ process[EXIT_GUARD_STATE_KEY] = state;
13
+ } else {
14
+ delete process[EXIT_GUARD_STATE_KEY];
15
+ }
16
+ }
17
+ function ExitGuard(props) {
18
+ const renderer = useRenderer();
19
+ let state = getExitGuardState();
20
+ if (!state) {
21
+ const originalExit = process.exit.bind(process);
22
+ state = {
23
+ originalExit,
24
+ renderers: /* @__PURE__ */ new Set(),
25
+ activeCount: 0,
26
+ isExiting: false
27
+ };
28
+ setExitGuardState(state);
29
+ const guardedExit = ((code) => {
30
+ const currentState = getExitGuardState();
31
+ if (!currentState) {
32
+ return originalExit(code);
33
+ }
34
+ if (currentState.isExiting) {
35
+ return currentState.originalExit(code);
36
+ }
37
+ currentState.isExiting = true;
38
+ try {
39
+ if (typeof code === "number") {
40
+ process.exitCode = code;
41
+ }
42
+ for (const r of currentState.renderers) {
43
+ try {
44
+ r.destroy();
45
+ } catch {
46
+ }
47
+ }
48
+ } finally {
49
+ return currentState.originalExit(code);
50
+ }
51
+ });
52
+ process.exit = guardedExit;
53
+ }
54
+ state.activeCount++;
55
+ state.renderers.add(renderer);
56
+ onCleanup(() => {
57
+ const state2 = getExitGuardState();
58
+ if (!state2) {
59
+ return;
60
+ }
61
+ state2.renderers.delete(renderer);
62
+ state2.activeCount = Math.max(0, state2.activeCount - 1);
63
+ if (state2.activeCount === 0) {
64
+ process.exit = state2.originalExit;
65
+ setExitGuardState(void 0);
66
+ }
67
+ });
68
+ return props.children;
69
+ }
70
+ const ShortcutContext = createContext();
71
+ function ShortcutProvider(props) {
72
+ const [registeredBindings, setRegisteredBindings] = createSignal(props.bindings ?? []);
73
+ const [pending, setPending] = createSignal(null);
74
+ const register = (binding) => {
75
+ setRegisteredBindings((prev) => [...prev, binding]);
76
+ return () => {
77
+ setRegisteredBindings((prev) => prev.filter((b) => b !== binding));
78
+ };
79
+ };
80
+ const value = {
81
+ register,
82
+ pending,
83
+ bindings: registeredBindings
84
+ };
85
+ return createComponent(ShortcutContext.Provider, {
86
+ value,
87
+ get children() {
88
+ return props.children;
89
+ }
90
+ });
91
+ }
92
+ function useShortcuts() {
93
+ const context = useContext(ShortcutContext);
94
+ if (!context) {
95
+ throw new Error("useShortcuts must be used within a ShortcutProvider");
96
+ }
97
+ return context;
98
+ }
99
+ const TuiThemes = {
100
+ dark: {
101
+ name: "dark",
102
+ colors: {
103
+ text: "#ffffff",
104
+ muted: "#888888",
105
+ background: "#1a1a1a",
106
+ accent: "#00d4ff",
107
+ success: "#00ff88",
108
+ warning: "#ffaa00",
109
+ error: "#ff4444",
110
+ border: "#444444",
111
+ selection: "#0066cc"
112
+ }
113
+ },
114
+ light: {
115
+ name: "light",
116
+ colors: {
117
+ text: "#000000",
118
+ // Black text for readability
119
+ muted: "#333333",
120
+ // Dark gray muted
121
+ background: "#e8e8e8",
122
+ // Light gray background
123
+ accent: "#0044aa",
124
+ // Deep blue accent
125
+ success: "#005500",
126
+ // Dark green
127
+ warning: "#885500",
128
+ // Dark orange
129
+ error: "#880000",
130
+ // Dark red
131
+ border: "#888888",
132
+ selection: "#0044aa"
133
+ }
134
+ },
135
+ monokai: {
136
+ name: "monokai",
137
+ colors: {
138
+ text: "#f8f8f2",
139
+ muted: "#75715e",
140
+ background: "#272822",
141
+ accent: "#ae81ff",
142
+ success: "#a6e22e",
143
+ warning: "#e6db74",
144
+ error: "#f92672",
145
+ border: "#49483e",
146
+ selection: "#49483e"
147
+ }
148
+ },
149
+ dracula: {
150
+ name: "dracula",
151
+ colors: {
152
+ text: "#f8f8f2",
153
+ muted: "#6272a4",
154
+ background: "#282a36",
155
+ accent: "#bd93f9",
156
+ success: "#50fa7b",
157
+ warning: "#f1fa8c",
158
+ error: "#ff5555",
159
+ border: "#44475a",
160
+ selection: "#44475a"
161
+ }
162
+ },
163
+ nord: {
164
+ name: "nord",
165
+ colors: {
166
+ text: "#eceff4",
167
+ muted: "#4c566a",
168
+ background: "#2e3440",
169
+ accent: "#88c0d0",
170
+ success: "#a3be8c",
171
+ warning: "#ebcb8b",
172
+ error: "#bf616a",
173
+ border: "#3b4252",
174
+ selection: "#4c566a"
175
+ }
176
+ },
177
+ solarized: {
178
+ name: "solarized",
179
+ colors: {
180
+ text: "#839496",
181
+ muted: "#586e75",
182
+ background: "#002b36",
183
+ accent: "#268bd2",
184
+ success: "#859900",
185
+ warning: "#b58900",
186
+ error: "#dc322f",
187
+ border: "#073642",
188
+ selection: "#073642"
189
+ }
190
+ }
191
+ };
192
+ const THEMES = TuiThemes;
193
+ const Theme = {
194
+ /**
195
+ * Start building a theme from an existing base theme.
196
+ */
197
+ from: (base) => ({
198
+ /**
199
+ * Extend the base theme with overrides.
200
+ * Color overrides are shallow-merged with the base colors.
201
+ */
202
+ extend: (overrides) => ({
203
+ name: overrides.name ?? `${base.name}-extended`,
204
+ colors: { ...base.colors, ...overrides.colors }
205
+ })
206
+ }),
207
+ /**
208
+ * Create a new theme from scratch.
209
+ */
210
+ create: (theme) => theme,
211
+ /**
212
+ * Get all available theme names.
213
+ */
214
+ names: () => Object.keys(TuiThemes),
215
+ /**
216
+ * Get a theme by name, with fallback to dark theme.
217
+ */
218
+ get: (name) => TuiThemes[name] ?? TuiThemes["dark"]
219
+ };
220
+ const ThemeContext = createContext();
221
+ function ThemeProvider(props) {
222
+ const themes = { ...TuiThemes };
223
+ const initialTheme = themes[props.initial ?? "dark"] ?? themes["dark"];
224
+ const [current, setCurrent] = createSignal(initialTheme);
225
+ const setTheme = (name) => {
226
+ if (themes[name]) {
227
+ setCurrent(themes[name]);
228
+ }
229
+ };
230
+ const cycle = () => {
231
+ const currentName = current().name;
232
+ const names2 = Object.keys(themes);
233
+ const currentIndex = names2.indexOf(currentName);
234
+ const nextIndex = (currentIndex + 1) % names2.length;
235
+ setCurrent(themes[names2[nextIndex]]);
236
+ };
237
+ const register = (theme) => {
238
+ themes[theme.name] = theme;
239
+ };
240
+ const names = () => Object.keys(themes);
241
+ const value = {
242
+ current,
243
+ setTheme,
244
+ cycle,
245
+ register,
246
+ names
247
+ };
248
+ return createComponent(ThemeContext.Provider, {
249
+ value,
250
+ get children() {
251
+ return props.children;
252
+ }
253
+ });
254
+ }
255
+ function useTheme() {
256
+ const context = useContext(ThemeContext);
257
+ if (!context) {
258
+ throw new Error("useTheme must be used within a ThemeProvider");
259
+ }
260
+ return context;
261
+ }
262
+ const ToastContext = createContext();
263
+ const DEFAULT_DURATION = 3e3;
264
+ function ToastProvider(props) {
265
+ const [state, setState] = createSignal({
266
+ message: "",
267
+ type: "info",
268
+ visible: false
269
+ });
270
+ let hideTimeout = null;
271
+ const show = (message, type, duration) => {
272
+ if (hideTimeout) {
273
+ clearTimeout(hideTimeout);
274
+ }
275
+ setState({ message, type, visible: true });
276
+ hideTimeout = setTimeout(() => {
277
+ setState((prev) => ({ ...prev, visible: false }));
278
+ }, duration);
279
+ };
280
+ const hide = () => {
281
+ if (hideTimeout) {
282
+ clearTimeout(hideTimeout);
283
+ hideTimeout = null;
284
+ }
285
+ setState((prev) => ({ ...prev, visible: false }));
286
+ };
287
+ const value = {
288
+ info: (message, duration = DEFAULT_DURATION) => show(message, "info", duration),
289
+ success: (message, duration = DEFAULT_DURATION) => show(message, "success", duration),
290
+ error: (message, duration = DEFAULT_DURATION) => show(message, "error", duration),
291
+ warning: (message, duration = DEFAULT_DURATION) => show(message, "warning", duration),
292
+ state,
293
+ hide
294
+ };
295
+ return createComponent(ToastContext.Provider, {
296
+ value,
297
+ get children() {
298
+ return props.children;
299
+ }
300
+ });
301
+ }
302
+ function useToast() {
303
+ const context = useContext(ToastContext);
304
+ if (!context) {
305
+ throw new Error("useToast must be used within a ToastProvider");
306
+ }
307
+ return context;
308
+ }
309
+ function createTuiApp(App, config = {}) {
310
+ const { theme = "dark", shortcuts = [], onDestroy } = config;
311
+ return render(
312
+ () => createComponent(ExitGuard, {
313
+ get children() {
314
+ return createComponent(ThemeProvider, {
315
+ get initial() {
316
+ return theme;
317
+ },
318
+ get children() {
319
+ return createComponent(ShortcutProvider, {
320
+ get bindings() {
321
+ return shortcuts;
322
+ },
323
+ get children() {
324
+ return createComponent(ToastProvider, {
325
+ get children() {
326
+ return createComponent(App, {});
327
+ }
328
+ });
329
+ }
330
+ });
331
+ }
332
+ });
333
+ }
334
+ }),
335
+ { onDestroy }
336
+ );
337
+ }
338
+ const TuiContext = createContext();
339
+ function useTui() {
340
+ const context = useContext(TuiContext);
341
+ if (!context) {
342
+ throw new Error("useTui must be used within a TuiProvider");
343
+ }
344
+ return context;
345
+ }
346
+ function TuiProvider(props) {
347
+ const renderer = useRenderer();
348
+ const reservedRows = props.reservedRows ?? 8;
349
+ const scrollSpeed = props.scrollSpeed ?? 3;
350
+ const [viewportHeight, setViewportHeight] = createSignal(
351
+ Math.max(10, renderer.height - reservedRows)
352
+ );
353
+ const [viewportWidth, setViewportWidth] = createSignal(renderer.width);
354
+ const exit = (code = 0) => {
355
+ process.exitCode = code;
356
+ renderer.destroy();
357
+ };
358
+ const handleResize = (width, height) => {
359
+ setViewportHeight(Math.max(10, height - reservedRows));
360
+ setViewportWidth(width);
361
+ };
362
+ const handleMouseScroll = (event) => {
363
+ if (!props.onScroll || !event.scroll) {
364
+ return;
365
+ }
366
+ const sign = event.scroll.direction === "up" ? -1 : event.scroll.direction === "down" ? 1 : 0;
367
+ if (sign === 0) {
368
+ return;
369
+ }
370
+ const delta = event.scroll.delta || 1;
371
+ props.onScroll(sign * delta * scrollSpeed);
372
+ };
373
+ onMount(() => {
374
+ renderer.on("resize", handleResize);
375
+ });
376
+ onCleanup(() => {
377
+ renderer.off("resize", handleResize);
378
+ });
379
+ const contextValue = {
380
+ viewportHeight,
381
+ viewportWidth,
382
+ exit
383
+ };
384
+ const themeName = typeof props.theme === "string" ? props.theme : props.theme?.name ?? "dark";
385
+ return createComponent(ExitGuard, {
386
+ get children() {
387
+ return createComponent(TuiContext.Provider, {
388
+ value: contextValue,
389
+ get children() {
390
+ return createComponent(ThemeProvider, {
391
+ initial: themeName,
392
+ get children() {
393
+ return createComponent(ShortcutProvider, {
394
+ get bindings() {
395
+ return props.shortcuts ?? [];
396
+ },
397
+ get children() {
398
+ return createComponent(ToastProvider, {
399
+ get children() {
400
+ return /* @__PURE__ */ React.createElement(
401
+ "box",
402
+ {
403
+ width: "100%",
404
+ height: "100%",
405
+ onMouseScroll: handleMouseScroll
406
+ },
407
+ props.children
408
+ );
409
+ }
410
+ });
411
+ }
412
+ });
413
+ }
414
+ });
415
+ }
416
+ });
417
+ }
418
+ });
419
+ }
420
+ function Breadcrumb(props) {
421
+ const { current: theme } = useTheme();
422
+ const separator = props.separator ?? "›";
423
+ const accentColor = () => props.accentColor ?? theme().colors.accent;
424
+ const mutedColor = () => props.mutedColor ?? theme().colors.muted;
425
+ return /* @__PURE__ */ React.createElement("box", { height: 1, paddingLeft: 2 }, /* @__PURE__ */ React.createElement(For, { each: props.segments }, (segment, idx) => /* @__PURE__ */ React.createElement(React.Fragment, null, idx() > 0 && /* @__PURE__ */ React.createElement("text", { color: mutedColor() }, " ", separator, " "), /* @__PURE__ */ React.createElement("text", { color: accentColor(), bold: true }, segment))));
426
+ }
427
+ function unwrap(value) {
428
+ return typeof value === "function" ? value() : value;
429
+ }
430
+ function VirtualList(props) {
431
+ const { current: theme } = useTheme();
432
+ const [scrollOffset, setScrollOffset] = createSignal(0);
433
+ const items = () => unwrap(props.items);
434
+ const selectedIndex = () => unwrap(props.selectedIndex);
435
+ const viewportHeight = () => unwrap(props.viewportHeight ?? 20);
436
+ const showIndicator = props.showIndicator ?? true;
437
+ const visibleItems = createMemo(() => {
438
+ const allItems = items();
439
+ const vh = viewportHeight();
440
+ const start = scrollOffset();
441
+ const end = Math.min(start + vh, allItems.length);
442
+ return allItems.slice(start, end).map((item, localIdx) => ({
443
+ item,
444
+ globalIndex: start + localIdx
445
+ }));
446
+ });
447
+ const defaultRenderItem = (item, index, selected) => {
448
+ const label = props.getLabel ? props.getLabel(item) : String(item);
449
+ const t = theme();
450
+ return /* @__PURE__ */ React.createElement(
451
+ "box",
452
+ {
453
+ height: 1,
454
+ backgroundColor: selected ? t.colors.selection : void 0
455
+ },
456
+ /* @__PURE__ */ React.createElement("text", { color: selected ? t.colors.background : t.colors.text }, showIndicator ? selected ? "› " : " " : "", label)
457
+ );
458
+ };
459
+ const renderItem = props.renderItem ?? defaultRenderItem;
460
+ return /* @__PURE__ */ React.createElement("box", { flexDirection: "column", flexGrow: 1 }, /* @__PURE__ */ React.createElement(Show, { when: props.title }, /* @__PURE__ */ React.createElement("text", { bold: true, color: theme().colors.text, marginBottom: 1 }, props.title, " (", selectedIndex() + 1, "/", items().length, ")")), /* @__PURE__ */ React.createElement(For, { each: visibleItems() }, ({ item, globalIndex }) => renderItem(item, globalIndex, globalIndex === selectedIndex())));
461
+ }
462
+ function createVirtualListController(items, selectedIndex, setSelectedIndex, viewportHeight) {
463
+ const [scrollOffset, setScrollOffset] = createSignal(0);
464
+ const adjustScroll = (newIdx) => {
465
+ const vh = viewportHeight();
466
+ const currentOffset = scrollOffset();
467
+ if (newIdx < currentOffset) {
468
+ setScrollOffset(newIdx);
469
+ } else if (newIdx >= currentOffset + vh) {
470
+ setScrollOffset(newIdx - vh + 1);
471
+ }
472
+ };
473
+ const scrollBy = (delta) => {
474
+ const maxOffset = Math.max(0, items().length - viewportHeight());
475
+ setScrollOffset((o) => Math.max(0, Math.min(maxOffset, o + delta)));
476
+ };
477
+ const selectPrevious = () => {
478
+ const newIdx = Math.max(0, selectedIndex() - 1);
479
+ setSelectedIndex(newIdx);
480
+ adjustScroll(newIdx);
481
+ };
482
+ const selectNext = () => {
483
+ const newIdx = Math.min(items().length - 1, selectedIndex() + 1);
484
+ setSelectedIndex(newIdx);
485
+ adjustScroll(newIdx);
486
+ };
487
+ return { scrollOffset, adjustScroll, scrollBy, selectPrevious, selectNext };
488
+ }
489
+ function MasterDetail(props) {
490
+ const { current: theme } = useTheme();
491
+ const masterWidth = props.masterWidth ?? "35%";
492
+ return /* @__PURE__ */ React.createElement(
493
+ "box",
494
+ {
495
+ width: "100%",
496
+ height: "100%",
497
+ flexDirection: "column",
498
+ backgroundColor: theme().colors.background
499
+ },
500
+ /* @__PURE__ */ React.createElement(
501
+ "box",
502
+ {
503
+ height: 3,
504
+ borderStyle: "single",
505
+ borderColor: theme().colors.accent,
506
+ justifyContent: "center",
507
+ alignItems: "center"
508
+ },
509
+ /* @__PURE__ */ React.createElement("text", { bold: true, color: theme().colors.accent }, props.headerIcon ? ` ${props.headerIcon} ` : " ", props.header, " ")
510
+ ),
511
+ /* @__PURE__ */ React.createElement(Show, { when: props.breadcrumb && props.breadcrumb.length > 0 }, /* @__PURE__ */ React.createElement(Breadcrumb, { segments: props.breadcrumb })),
512
+ /* @__PURE__ */ React.createElement("box", { flexGrow: 1, flexDirection: "row" }, /* @__PURE__ */ React.createElement(
513
+ "box",
514
+ {
515
+ width: masterWidth,
516
+ borderStyle: "single",
517
+ borderColor: theme().colors.border,
518
+ flexDirection: "column",
519
+ padding: 1
520
+ },
521
+ props.master
522
+ ), /* @__PURE__ */ React.createElement(
523
+ "box",
524
+ {
525
+ flexGrow: 1,
526
+ borderStyle: "single",
527
+ borderColor: theme().colors.border,
528
+ flexDirection: "column",
529
+ padding: 2
530
+ },
531
+ props.detail
532
+ )),
533
+ /* @__PURE__ */ React.createElement(Show, { when: props.footer }, /* @__PURE__ */ React.createElement("box", { height: 1, backgroundColor: theme().colors.background }, /* @__PURE__ */ React.createElement("text", { color: theme().colors.muted }, " ", props.footer)))
534
+ );
535
+ }
536
+ function parseWidth(width) {
537
+ if (width === void 0) return "30%";
538
+ if (typeof width === "number") return width;
539
+ if (width.endsWith("%")) {
540
+ const percent = parseInt(width.replace("%", ""), 10);
541
+ return `${percent}%`;
542
+ }
543
+ return width;
544
+ }
545
+ function MasterDetailLayout$1(props) {
546
+ const masterWidth = parseWidth(props.masterWidth);
547
+ const gap = props.gap ?? 1;
548
+ const showDivider = props.showDivider ?? true;
549
+ return {
550
+ type: "box",
551
+ props: {
552
+ flexDirection: "row",
553
+ width: "100%",
554
+ height: "100%"
555
+ },
556
+ children: [
557
+ // Master panel
558
+ {
559
+ type: "box",
560
+ props: {
561
+ flexBasis: masterWidth,
562
+ flexShrink: 0,
563
+ overflow: "scroll"
564
+ },
565
+ children: props.master
566
+ },
567
+ // Divider (optional)
568
+ ...showDivider ? [
569
+ {
570
+ type: "box",
571
+ props: {
572
+ width: 1,
573
+ marginLeft: Math.floor(gap / 2),
574
+ marginRight: Math.ceil(gap / 2)
575
+ },
576
+ children: {
577
+ type: "text",
578
+ props: {
579
+ style: { fg: "#444444" }
580
+ },
581
+ children: "│".repeat(100)
582
+ // Repeating for height
583
+ }
584
+ }
585
+ ] : [],
586
+ // Detail panel
587
+ {
588
+ type: "box",
589
+ props: {
590
+ flexGrow: 1,
591
+ overflow: "scroll"
592
+ },
593
+ children: props.detail
594
+ }
595
+ ]
596
+ };
597
+ }
598
+ function DrillDownNavigator(props) {
599
+ const [stack, setStack] = createSignal([]);
600
+ const push = (view) => {
601
+ setStack((prev) => [...prev, view]);
602
+ props.onNavigate?.(stack().length + 1);
603
+ };
604
+ const pop = () => {
605
+ if (stack().length > 0) {
606
+ setStack((prev) => prev.slice(0, -1));
607
+ props.onNavigate?.(stack().length - 1);
608
+ }
609
+ };
610
+ const replace = (view) => {
611
+ if (stack().length > 0) {
612
+ setStack((prev) => [...prev.slice(0, -1), view]);
613
+ } else {
614
+ push(view);
615
+ }
616
+ };
617
+ const reset = () => {
618
+ setStack([]);
619
+ props.onNavigate?.(0);
620
+ };
621
+ const depth = () => stack().length;
622
+ const canGoBack = () => stack().length > 0;
623
+ const api = {
624
+ push,
625
+ pop,
626
+ replace,
627
+ reset,
628
+ depth,
629
+ canGoBack
630
+ };
631
+ const currentView = () => {
632
+ const currentStack = stack();
633
+ if (currentStack.length > 0) {
634
+ return currentStack[currentStack.length - 1]();
635
+ }
636
+ return props.children(api);
637
+ };
638
+ return {
639
+ type: "box",
640
+ props: {
641
+ width: "100%",
642
+ height: "100%",
643
+ onKeyDown: (event) => {
644
+ if (event.defaultPrevented) return;
645
+ if ((event.key === "Escape" || event.key === "ArrowLeft") && canGoBack()) {
646
+ pop();
647
+ }
648
+ }
649
+ },
650
+ children: currentView()
651
+ };
652
+ }
653
+ function Card(props) {
654
+ const borderStyle = props.borderStyle ?? "single";
655
+ const padding = props.padding ?? 1;
656
+ let borderColor = props.borderColor;
657
+ if (!borderColor) {
658
+ try {
659
+ const { current } = useTheme();
660
+ borderColor = current().colors.border;
661
+ } catch {
662
+ borderColor = "#444444";
663
+ }
664
+ }
665
+ return {
666
+ type: "box",
667
+ props: {
668
+ flexDirection: "column",
669
+ width: props.width,
670
+ height: props.height,
671
+ ...props.onClick && { onMouseDown: props.onClick },
672
+ style: {
673
+ border: borderStyle !== "none" ? borderStyle : void 0,
674
+ borderColor,
675
+ padding
676
+ }
677
+ },
678
+ children: [
679
+ // Title row (if provided)
680
+ ...props.title ? [
681
+ {
682
+ type: "text",
683
+ props: {
684
+ style: { bold: true }
685
+ },
686
+ children: ` ${props.title} `
687
+ }
688
+ ] : [],
689
+ // Content
690
+ props.children
691
+ ]
692
+ };
693
+ }
694
+ function formatValue(value, format, currency) {
695
+ if (typeof value === "string") return value;
696
+ switch (format) {
697
+ case "compact":
698
+ if (value >= 1e9)
699
+ return `${(value / 1e9).toFixed(1)}B`;
700
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
701
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
702
+ return value.toString();
703
+ case "percent":
704
+ return `${(value * 100).toFixed(1)}%`;
705
+ case "currency":
706
+ return new Intl.NumberFormat("en-US", {
707
+ style: "currency",
708
+ currency: currency ?? "USD",
709
+ minimumFractionDigits: 0,
710
+ maximumFractionDigits: 2
711
+ }).format(value);
712
+ case "number":
713
+ default:
714
+ return new Intl.NumberFormat("en-US").format(value);
715
+ }
716
+ }
717
+ function getTrendIndicator(trend) {
718
+ switch (trend) {
719
+ case "up":
720
+ return { symbol: "▲", color: "#00ff88" };
721
+ case "down":
722
+ return { symbol: "▼", color: "#ff4444" };
723
+ case "neutral":
724
+ default:
725
+ return { symbol: "─", color: "#888888" };
726
+ }
727
+ }
728
+ function StatCard(props) {
729
+ const formattedValue = formatValue(
730
+ props.value,
731
+ props.format ?? "number",
732
+ props.currency
733
+ );
734
+ const trendIndicator = props.trend ? getTrendIndicator(props.trend) : null;
735
+ return {
736
+ type: "box",
737
+ props: {
738
+ flexDirection: "column",
739
+ width: props.width,
740
+ padding: 1,
741
+ style: {
742
+ border: "single"
743
+ },
744
+ ...props.onClick && { onMouseDown: props.onClick }
745
+ },
746
+ children: [
747
+ // Label
748
+ {
749
+ type: "text",
750
+ props: {
751
+ style: { fg: "#888888" }
752
+ },
753
+ children: props.label
754
+ },
755
+ // Value with optional trend
756
+ {
757
+ type: "box",
758
+ props: {
759
+ flexDirection: "row",
760
+ gap: 1
761
+ },
762
+ children: [
763
+ {
764
+ type: "text",
765
+ props: {
766
+ style: { bold: true, fg: "#ffffff" }
767
+ },
768
+ children: formattedValue
769
+ },
770
+ ...trendIndicator ? [
771
+ {
772
+ type: "text",
773
+ props: {
774
+ style: { fg: trendIndicator.color }
775
+ },
776
+ children: trendIndicator.symbol
777
+ }
778
+ ] : []
779
+ ]
780
+ }
781
+ ]
782
+ };
783
+ }
784
+ function MarkdownBlock(props) {
785
+ const { current } = useTheme();
786
+ return {
787
+ type: "box",
788
+ props: {
789
+ flexDirection: "column",
790
+ width: props.width ?? "100%",
791
+ height: props.height,
792
+ padding: props.padding ?? 0,
793
+ overflow: "scroll"
794
+ },
795
+ children: [
796
+ {
797
+ type: "text",
798
+ props: {
799
+ style: {
800
+ fg: current().colors.text
801
+ },
802
+ text: props.content
803
+ }
804
+ }
805
+ ]
806
+ };
807
+ }
808
+ function Button(props) {
809
+ const { current } = useTheme();
810
+ const [isHovered, setIsHovered] = createSignal(false);
811
+ const [isPressed, setIsPressed] = createSignal(false);
812
+ const getBackgroundColor = () => {
813
+ if (props.disabled) return current().colors.muted;
814
+ switch (props.variant) {
815
+ case "danger":
816
+ return isPressed() ? "#bd2c00" : isHovered() ? "#c82829" : current().colors.error;
817
+ case "primary":
818
+ default:
819
+ return isPressed() ? current().colors.selection : isHovered() ? current().colors.accent : current().colors.accent;
820
+ }
821
+ };
822
+ const getTextColor = () => {
823
+ if (props.disabled) return current().colors.background;
824
+ return props.variant === "primary" || props.variant === "danger" ? "#ffffff" : current().colors.text;
825
+ };
826
+ return {
827
+ type: "box",
828
+ props: {
829
+ width: props.width,
830
+ height: 3,
831
+ // Standard button height with border
832
+ flexDirection: "row",
833
+ alignItems: "center",
834
+ justifyContent: "center",
835
+ style: {
836
+ border: "single",
837
+ borderColor: isHovered() && !props.disabled ? current().colors.text : current().colors.border,
838
+ bg: getBackgroundColor()
839
+ },
840
+ // Event handlers
841
+ onMouseOver: () => !props.disabled && setIsHovered(true),
842
+ onMouseOut: () => {
843
+ setIsHovered(false);
844
+ setIsPressed(false);
845
+ },
846
+ onMouseDown: () => !props.disabled && setIsPressed(true),
847
+ onMouseUp: () => {
848
+ if (!props.disabled && isPressed()) {
849
+ setIsPressed(false);
850
+ props.onClick?.();
851
+ }
852
+ }
853
+ },
854
+ children: [
855
+ {
856
+ type: "text",
857
+ props: {
858
+ style: {
859
+ fg: getTextColor(),
860
+ bold: true
861
+ },
862
+ text: ` ${props.label} `
863
+ }
864
+ }
865
+ ]
866
+ };
867
+ }
868
+ const LAYOUT_THEMES = {
869
+ dark: {
870
+ bg: "#0d0d0d",
871
+ fg: "#f5f5f5",
872
+ accent: "#00d4ff",
873
+ muted: "#999999",
874
+ error: "#ff4444",
875
+ success: "#44ff44",
876
+ border: "#444444",
877
+ selection: "#00d4ff",
878
+ selectionFg: "#000000"
879
+ },
880
+ light: {
881
+ bg: "#e8e8e8",
882
+ fg: "#000000",
883
+ accent: "#0044aa",
884
+ muted: "#333333",
885
+ error: "#880000",
886
+ success: "#005500",
887
+ border: "#888888",
888
+ selection: "#0044aa",
889
+ selectionFg: "#ffffff"
890
+ },
891
+ monokai: {
892
+ bg: "#272822",
893
+ fg: "#f8f8f2",
894
+ accent: "#a6e22e",
895
+ muted: "#75715e",
896
+ error: "#f92672",
897
+ success: "#a6e22e",
898
+ border: "#49483e",
899
+ selection: "#a6e22e",
900
+ selectionFg: "#272822"
901
+ }
902
+ };
903
+ function resolveTheme(theme) {
904
+ if (!theme) return LAYOUT_THEMES.dark;
905
+ if (typeof theme === "string")
906
+ return LAYOUT_THEMES[theme] ?? LAYOUT_THEMES.dark;
907
+ return theme;
908
+ }
909
+ function MasterDetailLayout(props) {
910
+ const t = resolveTheme(props.theme);
911
+ props.masterWidth ?? "35%";
912
+ return /* @__PURE__ */ React.createElement(
913
+ "box",
914
+ {
915
+ width: "100%",
916
+ height: "100%",
917
+ flexDirection: "column",
918
+ backgroundColor: t.bg
919
+ },
920
+ /* @__PURE__ */ React.createElement(
921
+ "box",
922
+ {
923
+ height: 3,
924
+ borderStyle: "single",
925
+ borderColor: t.accent,
926
+ justifyContent: "center",
927
+ alignItems: "center"
928
+ },
929
+ /* @__PURE__ */ React.createElement("text", { bold: true, color: t.accent }, " ", props.header, " ")
930
+ ),
931
+ props.breadcrumb && props.breadcrumb.length > 0 && /* @__PURE__ */ React.createElement("box", { height: 1, paddingLeft: 2, backgroundColor: t.bg }, props.breadcrumb.map((segment, idx) => /* @__PURE__ */ React.createElement(React.Fragment, null, idx > 0 && /* @__PURE__ */ React.createElement("text", { color: t.muted }, " › "), /* @__PURE__ */ React.createElement("text", { color: t.accent, bold: true }, segment)))),
932
+ /* @__PURE__ */ React.createElement("box", { flexGrow: 1, flexDirection: "row" }, props.children),
933
+ props.footer && /* @__PURE__ */ React.createElement("box", { height: 1, backgroundColor: t.bg }, /* @__PURE__ */ React.createElement("text", { color: t.muted }, " ", props.footer))
934
+ );
935
+ }
936
+ function MasterPanel(props) {
937
+ const t = resolveTheme(props.theme);
938
+ return /* @__PURE__ */ React.createElement(
939
+ "box",
940
+ {
941
+ width: props.width ?? "35%",
942
+ borderStyle: "single",
943
+ borderColor: t.border,
944
+ flexDirection: "column",
945
+ padding: 1
946
+ },
947
+ props.children
948
+ );
949
+ }
950
+ function DetailPanel(props) {
951
+ const t = resolveTheme(props.theme);
952
+ return /* @__PURE__ */ React.createElement(
953
+ "box",
954
+ {
955
+ flexGrow: 1,
956
+ borderStyle: "single",
957
+ borderColor: t.border,
958
+ flexDirection: "column",
959
+ padding: 2
960
+ },
961
+ props.children
962
+ );
963
+ }
964
+ function ListItem(props) {
965
+ const t = resolveTheme(props.theme);
966
+ return /* @__PURE__ */ React.createElement("box", { height: 1, backgroundColor: props.selected ? t.selection : void 0 }, /* @__PURE__ */ React.createElement("text", { color: props.selected ? t.selectionFg : t.fg }, props.selected ? "› " : " ", props.label));
967
+ }
968
+ function useVirtualScroll(items, _selectedIdx, viewportHeight) {
969
+ const [scrollOffset, setScrollOffset] = createSignal(0);
970
+ const adjustScroll = (newIdx) => {
971
+ const vh = viewportHeight();
972
+ const currentOffset = scrollOffset();
973
+ if (newIdx < currentOffset) {
974
+ setScrollOffset(newIdx);
975
+ } else if (newIdx >= currentOffset + vh) {
976
+ setScrollOffset(newIdx - vh + 1);
977
+ }
978
+ };
979
+ const visibleItems = createMemo(() => {
980
+ const allItems = items();
981
+ const vh = viewportHeight();
982
+ const start = scrollOffset();
983
+ const end = Math.min(start + vh, allItems.length);
984
+ return allItems.slice(start, end).map((item, localIdx) => ({
985
+ item,
986
+ globalIndex: start + localIdx
987
+ }));
988
+ });
989
+ const scrollBy = (delta) => {
990
+ const maxOffset = Math.max(0, items().length - viewportHeight());
991
+ setScrollOffset((o) => Math.max(0, Math.min(maxOffset, o + delta)));
992
+ };
993
+ return { visibleItems, adjustScroll, scrollOffset, scrollBy };
994
+ }
995
+ function getViewportHeight(reservedRows = 8, minHeight = 10) {
996
+ return Math.max(minHeight, (process.stdout.rows || 24) - reservedRows);
997
+ }
998
+ function enableMouseReporting() {
999
+ process.stdout.write("\x1B[?1000h");
1000
+ process.stdout.write("\x1B[?1006h");
1001
+ }
1002
+ function disableMouseReporting() {
1003
+ process.stdout.write("\x1B[?1000l");
1004
+ process.stdout.write("\x1B[?1002l");
1005
+ process.stdout.write("\x1B[?1003l");
1006
+ process.stdout.write("\x1B[?1006l");
1007
+ process.stdout.write("\x1B[?1015l");
1008
+ }
1009
+ function clearScreen() {
1010
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
1011
+ }
1012
+ function resetAttributes() {
1013
+ process.stdout.write("\x1B[0m");
1014
+ }
1015
+ function restoreStdin() {
1016
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
1017
+ try {
1018
+ process.stdin.setRawMode(false);
1019
+ } catch {
1020
+ }
1021
+ }
1022
+ }
1023
+ function switchToMainScreen() {
1024
+ process.stdout.write("\x1B[?1049l");
1025
+ }
1026
+ function cleanupTerminal() {
1027
+ switchToMainScreen();
1028
+ disableMouseReporting();
1029
+ clearScreen();
1030
+ resetAttributes();
1031
+ restoreStdin();
1032
+ }
1033
+ function parseMouseScroll(data) {
1034
+ const str = data.toString();
1035
+ const sgrMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
1036
+ if (sgrMatch) {
1037
+ const button = parseInt(sgrMatch[1], 10);
1038
+ if (button === 64) return -1;
1039
+ if (button === 65) return 1;
1040
+ }
1041
+ return 0;
1042
+ }
1043
+ function useMouse(options = {}) {
1044
+ const { onScroll, scrollSpeed = 3 } = options;
1045
+ const handleInput = (data) => {
1046
+ const scrollDir = parseMouseScroll(data);
1047
+ if (scrollDir !== 0 && onScroll) {
1048
+ onScroll(scrollDir * scrollSpeed);
1049
+ }
1050
+ };
1051
+ onMount(() => {
1052
+ enableMouseReporting();
1053
+ if (process.stdin.isTTY) {
1054
+ process.stdin.setRawMode(true);
1055
+ }
1056
+ process.stdin.on("data", handleInput);
1057
+ });
1058
+ onCleanup(() => {
1059
+ disableMouseReporting();
1060
+ process.stdin.off("data", handleInput);
1061
+ });
1062
+ }
1063
+ export {
1064
+ Breadcrumb,
1065
+ Button,
1066
+ Card,
1067
+ DetailPanel,
1068
+ DrillDownNavigator,
1069
+ LAYOUT_THEMES,
1070
+ ListItem,
1071
+ MarkdownBlock,
1072
+ MasterDetail,
1073
+ MasterDetailLayout$1 as MasterDetailLayout,
1074
+ MasterDetailLayout as MasterDetailTemplate,
1075
+ MasterPanel,
1076
+ ShortcutProvider,
1077
+ StatCard,
1078
+ THEMES,
1079
+ Theme,
1080
+ ThemeProvider,
1081
+ ToastProvider,
1082
+ TuiProvider,
1083
+ TuiThemes,
1084
+ VirtualList,
1085
+ cleanupTerminal,
1086
+ clearScreen,
1087
+ createComponent2 as createComponent,
1088
+ createTuiApp,
1089
+ createVirtualListController,
1090
+ disableMouseReporting,
1091
+ effect,
1092
+ enableMouseReporting,
1093
+ getViewportHeight,
1094
+ insert,
1095
+ memo,
1096
+ mergeProps,
1097
+ parseMouseScroll,
1098
+ render2 as render,
1099
+ resetAttributes,
1100
+ restoreStdin,
1101
+ spread,
1102
+ useKeyboard,
1103
+ useMouse,
1104
+ useRenderer2 as useRenderer,
1105
+ useShortcuts,
1106
+ useTheme,
1107
+ useToast,
1108
+ useTui,
1109
+ useVirtualScroll
1110
+ };
1111
+ //# sourceMappingURL=tui.mjs.map