@pablozaiden/terminatui 0.2.0 → 0.3.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 (181) hide show
  1. package/README.md +64 -43
  2. package/package.json +11 -8
  3. package/src/__tests__/application.test.ts +87 -68
  4. package/src/__tests__/buildCliCommand.test.ts +99 -119
  5. package/src/__tests__/builtins.test.ts +27 -75
  6. package/src/__tests__/command.test.ts +100 -131
  7. package/src/__tests__/configOnChange.test.ts +63 -0
  8. package/src/__tests__/context.test.ts +1 -26
  9. package/src/__tests__/helpCore.test.ts +227 -0
  10. package/src/__tests__/parser.test.ts +98 -244
  11. package/src/__tests__/registry.test.ts +33 -160
  12. package/src/__tests__/schemaToFields.test.ts +75 -158
  13. package/src/builtins/help.ts +12 -4
  14. package/src/builtins/settings.ts +18 -32
  15. package/src/builtins/version.ts +3 -3
  16. package/src/cli/output/colors.ts +1 -1
  17. package/src/cli/parser.ts +26 -95
  18. package/src/core/application.ts +192 -110
  19. package/src/core/command.ts +26 -9
  20. package/src/core/context.ts +31 -20
  21. package/src/core/help.ts +24 -18
  22. package/src/core/knownCommands.ts +13 -0
  23. package/src/core/logger.ts +39 -42
  24. package/src/core/registry.ts +5 -12
  25. package/src/index.ts +22 -137
  26. package/src/tui/TuiApplication.tsx +63 -120
  27. package/src/tui/TuiRoot.tsx +135 -0
  28. package/src/tui/adapters/factory.ts +19 -0
  29. package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
  30. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  31. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  32. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  33. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  34. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  35. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  36. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  37. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  38. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  39. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  40. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  41. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  42. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  43. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  44. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  45. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  46. package/src/tui/adapters/ink/keyboard.ts +97 -0
  47. package/src/tui/adapters/ink/utils.ts +16 -0
  48. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
  49. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  50. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  51. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  52. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  53. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  54. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  55. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  56. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  57. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  58. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  59. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  60. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  61. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  62. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  63. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  64. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  65. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  66. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  67. package/src/tui/adapters/types.ts +71 -0
  68. package/src/tui/components/ActionButton.tsx +0 -36
  69. package/src/tui/components/CommandSelector.tsx +45 -92
  70. package/src/tui/components/ConfigForm.tsx +68 -42
  71. package/src/tui/components/FieldRow.tsx +0 -30
  72. package/src/tui/components/Header.tsx +14 -13
  73. package/src/tui/components/JsonHighlight.tsx +10 -17
  74. package/src/tui/components/ModalBase.tsx +38 -0
  75. package/src/tui/components/ResultsPanel.tsx +27 -36
  76. package/src/tui/components/StatusBar.tsx +24 -39
  77. package/src/tui/components/logColors.ts +12 -0
  78. package/src/tui/context/ClipboardContext.tsx +87 -0
  79. package/src/tui/context/ExecutorContext.tsx +139 -0
  80. package/src/tui/context/KeyboardContext.tsx +85 -71
  81. package/src/tui/context/LogsContext.tsx +35 -0
  82. package/src/tui/context/NavigationContext.tsx +194 -0
  83. package/src/tui/context/RendererContext.tsx +20 -0
  84. package/src/tui/context/TuiAppContext.tsx +58 -0
  85. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  86. package/src/tui/hooks/useBackHandler.ts +34 -0
  87. package/src/tui/hooks/useClipboard.ts +40 -25
  88. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  89. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  90. package/src/tui/modals/CliModal.tsx +82 -0
  91. package/src/tui/modals/EditorModal.tsx +207 -0
  92. package/src/tui/modals/LogsModal.tsx +98 -0
  93. package/src/tui/registry.ts +102 -0
  94. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  95. package/src/tui/screens/ConfigScreen.tsx +165 -0
  96. package/src/tui/screens/ErrorScreen.tsx +58 -0
  97. package/src/tui/screens/ResultsScreen.tsx +68 -0
  98. package/src/tui/screens/RunningScreen.tsx +72 -0
  99. package/src/tui/screens/ScreenBase.ts +6 -0
  100. package/src/tui/semantic/Button.tsx +7 -0
  101. package/src/tui/semantic/Code.tsx +7 -0
  102. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  103. package/src/tui/semantic/Container.tsx +7 -0
  104. package/src/tui/semantic/Field.tsx +7 -0
  105. package/src/tui/semantic/Label.tsx +7 -0
  106. package/src/tui/semantic/MenuButton.tsx +7 -0
  107. package/src/tui/semantic/MenuItem.tsx +7 -0
  108. package/src/tui/semantic/Overlay.tsx +7 -0
  109. package/src/tui/semantic/Panel.tsx +7 -0
  110. package/src/tui/semantic/ScrollView.tsx +9 -0
  111. package/src/tui/semantic/Select.tsx +7 -0
  112. package/src/tui/semantic/Spacer.tsx +7 -0
  113. package/src/tui/semantic/Spinner.tsx +7 -0
  114. package/src/tui/semantic/TextInput.tsx +7 -0
  115. package/src/tui/semantic/Value.tsx +7 -0
  116. package/src/tui/semantic/types.ts +195 -0
  117. package/src/tui/theme.ts +25 -14
  118. package/src/tui/utils/buildCliCommand.ts +1 -0
  119. package/src/tui/utils/getEnumKeys.ts +3 -0
  120. package/src/tui/utils/parameterPersistence.ts +1 -0
  121. package/src/types/command.ts +0 -60
  122. package/.devcontainer/devcontainer.json +0 -19
  123. package/.devcontainer/install-prerequisites.sh +0 -49
  124. package/.github/workflows/copilot-setup-steps.yml +0 -32
  125. package/.github/workflows/pull-request.yml +0 -27
  126. package/.github/workflows/release-npm-package.yml +0 -81
  127. package/AGENTS.md +0 -31
  128. package/bun.lock +0 -236
  129. package/examples/tui-app/commands/config/app/get.ts +0 -66
  130. package/examples/tui-app/commands/config/app/index.ts +0 -27
  131. package/examples/tui-app/commands/config/app/set.ts +0 -86
  132. package/examples/tui-app/commands/config/index.ts +0 -32
  133. package/examples/tui-app/commands/config/user/get.ts +0 -65
  134. package/examples/tui-app/commands/config/user/index.ts +0 -27
  135. package/examples/tui-app/commands/config/user/set.ts +0 -61
  136. package/examples/tui-app/commands/greet.ts +0 -76
  137. package/examples/tui-app/commands/index.ts +0 -4
  138. package/examples/tui-app/commands/math.ts +0 -115
  139. package/examples/tui-app/commands/status.ts +0 -77
  140. package/examples/tui-app/index.ts +0 -35
  141. package/guides/01-hello-world.md +0 -96
  142. package/guides/02-adding-options.md +0 -103
  143. package/guides/03-multiple-commands.md +0 -163
  144. package/guides/04-subcommands.md +0 -206
  145. package/guides/05-interactive-tui.md +0 -194
  146. package/guides/06-config-validation.md +0 -264
  147. package/guides/07-async-cancellation.md +0 -336
  148. package/guides/08-complete-application.md +0 -537
  149. package/guides/README.md +0 -74
  150. package/src/__tests__/colors.test.ts +0 -127
  151. package/src/__tests__/commandClass.test.ts +0 -130
  152. package/src/__tests__/help.test.ts +0 -412
  153. package/src/__tests__/registryNew.test.ts +0 -160
  154. package/src/__tests__/table.test.ts +0 -146
  155. package/src/__tests__/tui.test.ts +0 -26
  156. package/src/builtins/index.ts +0 -4
  157. package/src/cli/help.ts +0 -174
  158. package/src/cli/index.ts +0 -3
  159. package/src/cli/output/index.ts +0 -2
  160. package/src/cli/output/table.ts +0 -141
  161. package/src/commands/help.ts +0 -50
  162. package/src/commands/index.ts +0 -1
  163. package/src/components/index.ts +0 -147
  164. package/src/core/index.ts +0 -15
  165. package/src/hooks/index.ts +0 -131
  166. package/src/registry/commandRegistry.ts +0 -77
  167. package/src/registry/index.ts +0 -1
  168. package/src/tui/TuiApp.tsx +0 -619
  169. package/src/tui/app.ts +0 -29
  170. package/src/tui/components/CliModal.tsx +0 -81
  171. package/src/tui/components/EditorModal.tsx +0 -177
  172. package/src/tui/components/LogsPanel.tsx +0 -86
  173. package/src/tui/components/index.ts +0 -13
  174. package/src/tui/context/index.ts +0 -7
  175. package/src/tui/hooks/index.ts +0 -35
  176. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  177. package/src/tui/hooks/useLogStream.ts +0 -96
  178. package/src/tui/index.ts +0 -65
  179. package/src/tui/utils/index.ts +0 -13
  180. package/src/types/index.ts +0 -1
  181. package/tsconfig.json +0 -25
@@ -0,0 +1,5 @@
1
+ import type { OverlayProps } from "../../../semantic/types.ts";
2
+
3
+ export function Overlay({ children }: OverlayProps) {
4
+ return <>{children}</>;
5
+ }
@@ -0,0 +1,15 @@
1
+ import { Text } from "ink";
2
+ import type { PanelProps } from "../../../semantic/types.ts";
3
+
4
+ export function Panel({ title, children }: PanelProps) {
5
+ return (
6
+ <>
7
+ {title ? (
8
+ <Text bold>
9
+ {title}
10
+ </Text>
11
+ ) : null}
12
+ {children}
13
+ </>
14
+ );
15
+ }
@@ -0,0 +1,5 @@
1
+ import type { ScrollViewProps } from "../../../semantic/types.ts";
2
+
3
+ export function ScrollView({ children }: ScrollViewProps) {
4
+ return <>{children}</>;
5
+ }
@@ -0,0 +1,44 @@
1
+ import InkSelectInput from "ink-select-input";
2
+ import { Text } from "ink";
3
+ import { useMemo } from "react";
4
+ import type { SelectProps } from "../../../semantic/types.ts";
5
+
6
+ type Item = { label: string; value: string };
7
+
8
+ type ItemComponentProps = {
9
+ isSelected?: boolean;
10
+ label: string;
11
+ };
12
+
13
+ function ItemComponent({ label }: ItemComponentProps) {
14
+ // ink-select-input already provides its own selection marker.
15
+ // Keep this as plain text to avoid double-marking.
16
+ return <Text>{label}</Text>;
17
+ }
18
+
19
+ export function Select({ options, value, focused, onChange, onSubmit }: SelectProps) {
20
+ const items = useMemo(
21
+ () => options.map((o) => ({ label: o.label, value: o.value }) as Item),
22
+ [options]
23
+ );
24
+
25
+ const initialIndex = Math.max(
26
+ 0,
27
+ options.findIndex((o) => o.value === value)
28
+ );
29
+
30
+ // Force remount so ink-select-input respects updated initialIndex.
31
+ const key = `${value}:${options.length}`;
32
+
33
+ return (
34
+ <InkSelectInput
35
+ key={key}
36
+ items={items}
37
+ isFocused={focused}
38
+ initialIndex={initialIndex}
39
+ itemComponent={ItemComponent}
40
+ onHighlight={(item) => onChange(item.value)}
41
+ onSelect={() => onSubmit?.()}
42
+ />
43
+ );
44
+ }
@@ -0,0 +1,15 @@
1
+ import { Text } from "ink";
2
+ import type { SpacerProps } from "../../../semantic/types.ts";
3
+
4
+ export function Spacer({ size, axis }: SpacerProps) {
5
+ if (axis === "horizontal") {
6
+ return <Text>{" ".repeat(size)}</Text>;
7
+ }
8
+ return (
9
+ <>
10
+ {Array.from({ length: size }).map((_, idx) => (
11
+ <Text key={idx} />
12
+ ))}
13
+ </>
14
+ );
15
+ }
@@ -0,0 +1,5 @@
1
+ import type { SpinnerProps } from "../../../semantic/types.ts";
2
+
3
+ export function Spinner(_: SpinnerProps) {
4
+ return null;
5
+ }
@@ -0,0 +1,22 @@
1
+ import InkTextInput from "ink-text-input";
2
+ import { Text } from "ink";
3
+ import type { TextInputProps } from "../../../semantic/types.ts";
4
+
5
+ export function TextInput({ value, placeholder, focused, onChange, onSubmit }: TextInputProps) {
6
+ // ink-text-input renders nothing if you pass empty placeholder; provide a minimal hint.
7
+ const hint = placeholder ?? "";
8
+
9
+ return (
10
+ <Text>
11
+ <InkTextInput
12
+ value={value}
13
+ placeholder={hint}
14
+ focus={focused}
15
+ onChange={onChange}
16
+ onSubmit={() => {
17
+ onSubmit?.();
18
+ }}
19
+ />
20
+ </Text>
21
+ );
22
+ }
@@ -0,0 +1,7 @@
1
+ import { Text } from "ink";
2
+ import type { ValueProps } from "../../../semantic/types.ts";
3
+ import { toPlainText } from "../utils.ts";
4
+
5
+ export function Value({ children }: ValueProps) {
6
+ return <Text color="magenta">{toPlainText(children)}</Text>;
7
+ }
@@ -0,0 +1,97 @@
1
+ import { useInput, type Key } from "ink";
2
+ import { useCallback, useMemo, useRef } from "react";
3
+ import type { KeyboardAdapter, KeyboardEvent, KeyHandler } from "../types.ts";
4
+
5
+ function normalizeKeyName(input: string, key: Key): KeyboardEvent {
6
+ const event: KeyboardEvent = {
7
+ name: input,
8
+ sequence: input,
9
+ ctrl: Boolean(key.ctrl),
10
+ shift: Boolean(key.shift),
11
+ meta: Boolean(key.meta),
12
+ };
13
+
14
+ if (key.return) {
15
+ event.name = "return";
16
+ } else if (key.escape) {
17
+ event.name = "escape";
18
+ } else if (key.backspace) {
19
+ event.name = "backspace";
20
+ } else if (key.delete) {
21
+ // Terminals often send escape sequences for “Delete” that some libraries
22
+ // expose as `delete`, others as `del`. Keep it normalized.
23
+ event.name = "delete";
24
+ } else if (key.tab) {
25
+ event.name = "tab";
26
+ } else if (key.upArrow) {
27
+ event.name = "up";
28
+ } else if (key.downArrow) {
29
+ event.name = "down";
30
+ } else if (key.leftArrow) {
31
+ event.name = "left";
32
+ } else if (key.rightArrow) {
33
+ event.name = "right";
34
+ } else if (key.pageUp) {
35
+ event.name = "pageup";
36
+ } else if (key.pageDown) {
37
+ event.name = "pagedown";
38
+ } else if (key.home) {
39
+ event.name = "home";
40
+ } else if (key.end) {
41
+ event.name = "end";
42
+ }
43
+
44
+ // Normalize enter -> return (some code checks either)
45
+ if (event.name === "enter") {
46
+ event.name = "return";
47
+ }
48
+
49
+ return event;
50
+ }
51
+
52
+ export function useInkKeyboardAdapter(): KeyboardAdapter {
53
+ const handlerStackRef = useRef<{ id: string; handler: KeyHandler }[]>([]);
54
+ const globalHandlerRef = useRef<KeyHandler | null>(null);
55
+
56
+ const setActiveHandler = useCallback((id: string, handler: KeyHandler) => {
57
+ handlerStackRef.current = handlerStackRef.current.filter((h) => h.id !== id);
58
+ handlerStackRef.current.push({ id, handler });
59
+
60
+ return () => {
61
+ handlerStackRef.current = handlerStackRef.current.filter((h) => h.id !== id);
62
+ };
63
+ }, []);
64
+
65
+ const setGlobalHandler = useCallback((handler: KeyHandler) => {
66
+ const previous = globalHandlerRef.current;
67
+ globalHandlerRef.current = handler;
68
+
69
+ return () => {
70
+ globalHandlerRef.current = previous;
71
+ };
72
+ }, []);
73
+
74
+ useInput((input, key) => {
75
+ const event = normalizeKeyName(input, key);
76
+
77
+ if (globalHandlerRef.current) {
78
+ const handled = globalHandlerRef.current(event);
79
+ if (handled) {
80
+ return;
81
+ }
82
+ }
83
+
84
+ const activeHandler = handlerStackRef.current[handlerStackRef.current.length - 1];
85
+ if (activeHandler) {
86
+ activeHandler.handler(event);
87
+ }
88
+ });
89
+
90
+ return useMemo(
91
+ () => ({
92
+ setActiveHandler,
93
+ setGlobalHandler,
94
+ }),
95
+ [setActiveHandler, setGlobalHandler]
96
+ );
97
+ }
@@ -0,0 +1,16 @@
1
+ export function toPlainText(node: unknown): string {
2
+ if (node === null || node === undefined || typeof node === "boolean") {
3
+ return "";
4
+ }
5
+ if (typeof node === "string" || typeof node === "number") {
6
+ return String(node);
7
+ }
8
+ if (Array.isArray(node)) {
9
+ return node.map(toPlainText).join("");
10
+ }
11
+ if (typeof node === "object" && node && "props" in node) {
12
+ const anyNode = node as any;
13
+ return toPlainText(anyNode.props?.children);
14
+ }
15
+ return "";
16
+ }
@@ -0,0 +1,119 @@
1
+ import { createCliRenderer, type CliRenderer } from "@opentui/core";
2
+ import { createRoot, type Root } from "@opentui/react";
3
+ import { useLayoutEffect, type ReactNode } from "react";
4
+ import { SemanticColors } from "../../theme.ts";
5
+ import type { Renderer, RendererConfig } from "../types.ts";
6
+ import { useOpenTuiKeyboardAdapter } from "./keyboard.ts";
7
+ import { Button } from "./components/Button.tsx";
8
+ import { Code } from "./components/Code.tsx";
9
+ import { CodeHighlight } from "./components/CodeHighlight.tsx";
10
+ import { Container } from "./components/Container.tsx";
11
+ import { Field } from "./components/Field.tsx";
12
+ import { Label } from "./components/Label.tsx";
13
+ import { MenuButton } from "./components/MenuButton.tsx";
14
+ import { MenuItem } from "./components/MenuItem.tsx";
15
+ import { Overlay } from "./components/Overlay.tsx";
16
+ import { Spacer } from "./components/Spacer.tsx";
17
+ import { Spinner } from "./components/Spinner.tsx";
18
+ import { Panel } from "./components/Panel.tsx";
19
+ import { ScrollView as OpenTuiScrollView } from "./components/ScrollView.tsx";
20
+ import { Select } from "./components/Select.tsx";
21
+ import { TextInput } from "./components/TextInput.tsx";
22
+ import { Value } from "./components/Value.tsx";
23
+
24
+ export class OpenTuiRenderer implements Renderer {
25
+ private renderer: CliRenderer | null = null;
26
+ private root: Root | null = null;
27
+
28
+ private activeKeyboardAdapter: Renderer["keyboard"] | null = null;
29
+
30
+ public supportCustomRendering(): boolean {
31
+ return true;
32
+ }
33
+
34
+ public keyboard: Renderer["keyboard"] = {
35
+ setActiveHandler: (id, handler) => {
36
+ return this.activeKeyboardAdapter?.setActiveHandler(id, handler) ?? (() => {});
37
+ },
38
+ setGlobalHandler: (handler) => {
39
+ return this.activeKeyboardAdapter?.setGlobalHandler(handler) ?? (() => {});
40
+ },
41
+ };
42
+
43
+ public components: Renderer["components"] = {
44
+ Field,
45
+ Button,
46
+ MenuButton,
47
+ MenuItem,
48
+ Container,
49
+ Panel,
50
+ ScrollView: OpenTuiScrollView,
51
+
52
+ Overlay,
53
+ Spacer,
54
+ Spinner,
55
+ Label,
56
+ Value,
57
+ Code,
58
+ CodeHighlight,
59
+
60
+ Select,
61
+ TextInput,
62
+ };
63
+
64
+ constructor(private readonly config: RendererConfig) {}
65
+
66
+ async initialize(): Promise<void> {
67
+ this.renderer = await createCliRenderer({
68
+ useAlternateScreen: this.config.useAlternateScreen ?? true,
69
+ useConsole: false,
70
+ exitOnCtrlC: true,
71
+ backgroundColor: SemanticColors.background,
72
+ useMouse: true,
73
+ enableMouseMovement: true,
74
+ openConsoleOnError: false,
75
+ });
76
+
77
+ this.root = createRoot(this.renderer);
78
+ }
79
+
80
+ render(node: ReactNode): void {
81
+ if (!this.root) {
82
+ throw new Error("OpenTuiRenderer not initialized");
83
+ }
84
+
85
+ this.root.render(
86
+ <KeyboardBridge
87
+ onReady={(keyboard) => {
88
+ this.activeKeyboardAdapter = keyboard;
89
+ }}
90
+ >
91
+ {node}
92
+ </KeyboardBridge>
93
+ );
94
+
95
+ this.renderer?.start();
96
+ }
97
+
98
+ destroy(): void {
99
+ this.renderer?.destroy();
100
+ this.renderer = null;
101
+ this.root = null;
102
+ }
103
+ }
104
+
105
+ function KeyboardBridge({
106
+ children,
107
+ onReady,
108
+ }: {
109
+ children: ReactNode;
110
+ onReady: (keyboard: ReturnType<typeof useOpenTuiKeyboardAdapter>) => void;
111
+ }) {
112
+ const keyboard = useOpenTuiKeyboardAdapter();
113
+
114
+ useLayoutEffect(() => {
115
+ onReady(keyboard);
116
+ }, [onReady, keyboard]);
117
+
118
+ return <>{children}</>;
119
+ }
@@ -0,0 +1,13 @@
1
+ import type { ButtonProps } from "../../../semantic/types.ts";
2
+ import { SemanticColors } from "../../../theme.ts";
3
+
4
+ export function Button({ label, selected, onActivate }: ButtonProps) {
5
+ const fg = selected ? SemanticColors.selectionText : SemanticColors.text;
6
+ const bg = selected ? SemanticColors.selectionBackground : undefined;
7
+
8
+ return (
9
+ <text fg={fg} bg={bg} {...({ onClick: onActivate })}>
10
+ {label}
11
+ </text>
12
+ );
13
+ }
@@ -0,0 +1,12 @@
1
+ import type { CodeProps } from "../../../semantic/types.ts";
2
+ import { SemanticColors } from "../../../theme.ts";
3
+
4
+ export function Code({ color = "code", children }: CodeProps) {
5
+ const fg = SemanticColors[color] ?? SemanticColors.code;
6
+
7
+ return (
8
+ <text fg={fg}>
9
+ {children}
10
+ </text>
11
+ );
12
+ }
@@ -0,0 +1,24 @@
1
+ import type { CodeHighlightProps, CodeTokenType } from "../../../semantic/types.ts";
2
+ import { SemanticColors } from "../../../theme.ts";
3
+
4
+ const TOKEN_COLORS: Record<CodeTokenType, string> = {
5
+ key: SemanticColors.primary,
6
+ string: SemanticColors.success,
7
+ number: "#d19a66",
8
+ boolean: "#c678dd",
9
+ null: "#c678dd",
10
+ punctuation: SemanticColors.mutedText,
11
+ unknown: SemanticColors.text,
12
+ };
13
+
14
+ export function CodeHighlight({ tokens }: CodeHighlightProps) {
15
+ return (
16
+ <text>
17
+ {tokens.map((token, tokenIdx) => (
18
+ <span key={tokenIdx} fg={TOKEN_COLORS[token.type] ?? SemanticColors.text}>
19
+ {token.value}
20
+ </span>
21
+ ))}
22
+ </text>
23
+ );
24
+ }
@@ -0,0 +1,56 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ContainerProps, Spacing } from "../../../semantic/types.ts";
3
+
4
+ function normalizePadding(padding: number | Spacing | undefined):
5
+ | { padding?: number; paddingTop?: number; paddingRight?: number; paddingBottom?: number; paddingLeft?: number }
6
+ | undefined {
7
+ if (padding === undefined) {
8
+ return undefined;
9
+ }
10
+
11
+ if (typeof padding === "number") {
12
+ return { padding };
13
+ }
14
+
15
+ return {
16
+ paddingTop: padding.top ?? 0,
17
+ paddingRight: padding.right ?? 0,
18
+ paddingBottom: padding.bottom ?? 0,
19
+ paddingLeft: padding.left ?? 0,
20
+ };
21
+ }
22
+
23
+ export function Container({
24
+ children,
25
+ flex,
26
+ width,
27
+ height,
28
+ flexDirection,
29
+ alignItems,
30
+ justifyContent,
31
+ gap,
32
+ padding,
33
+ noShrink,
34
+ }: ContainerProps & { children?: ReactNode }) {
35
+ const resolvedPadding = normalizePadding(padding);
36
+
37
+ return (
38
+ <box
39
+ flexGrow={flex}
40
+ flexShrink={noShrink ? 0 : flex === undefined ? undefined : 1}
41
+ width={width as any}
42
+ height={height as any}
43
+ flexDirection={flexDirection as any}
44
+ alignItems={alignItems as any}
45
+ justifyContent={justifyContent as any}
46
+ gap={gap}
47
+ padding={resolvedPadding?.padding}
48
+ paddingTop={resolvedPadding?.paddingTop}
49
+ paddingRight={resolvedPadding?.paddingRight}
50
+ paddingBottom={resolvedPadding?.paddingBottom}
51
+ paddingLeft={resolvedPadding?.paddingLeft}
52
+ >
53
+ {children}
54
+ </box>
55
+ );
56
+ }
@@ -0,0 +1,18 @@
1
+ import type { FieldProps } from "../../../semantic/types.ts";
2
+ import { SemanticColors } from "../../../theme.ts";
3
+
4
+ export function Field({ label, value, selected, onActivate }: FieldProps) {
5
+ const prefix = selected ? "► " : " ";
6
+ const labelColor = selected ? SemanticColors.focusBorder : SemanticColors.mutedText;
7
+ const valueColor = selected ? SemanticColors.value : SemanticColors.text;
8
+
9
+ return (
10
+ <box flexDirection="row" gap={1} {...({ onClick: onActivate })}>
11
+ <text fg={labelColor}>
12
+ {prefix}
13
+ {label}:
14
+ </text>
15
+ <text fg={valueColor}>{value}</text>
16
+ </box>
17
+ );
18
+ }
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from "react";
2
+ import type { LabelProps } from "../../../semantic/types.ts";
3
+ import { SemanticColors } from "../../../theme.ts";
4
+
5
+ export function Label({ color = "text", bold, italic, wrap, children }: LabelProps & { children: ReactNode }) {
6
+ const fg = SemanticColors[color] ?? SemanticColors.text;
7
+
8
+ const content = bold ? <strong>{children}</strong> : children;
9
+
10
+ return (
11
+ <text fg={fg} {...({ wrap } as any)}>
12
+ {italic ? <em>{content}</em> : content}
13
+ </text>
14
+ );
15
+ }
@@ -0,0 +1,14 @@
1
+ import type { MenuButtonProps } from "../../../semantic/types.ts";
2
+ import { MenuItem } from "./MenuItem.tsx";
3
+
4
+ export function MenuButton({ label, selected, onActivate }: MenuButtonProps) {
5
+ return (
6
+ <box marginTop={1}>
7
+ <MenuItem
8
+ label={`[ ${label} ]`}
9
+ selected={selected}
10
+ onActivate={onActivate}
11
+ />
12
+ </box>
13
+ );
14
+ }
@@ -0,0 +1,29 @@
1
+ import type { MenuItemProps } from "../../../semantic/types.ts";
2
+ import { SemanticColors } from "../../../theme.ts";
3
+
4
+ export function MenuItem({
5
+ label,
6
+ description,
7
+ suffix,
8
+ selected,
9
+ onActivate,
10
+ }: MenuItemProps) {
11
+ const prefix = selected ? "► " : " ";
12
+ const displayLabel = suffix ? `${label} ${suffix}` : label;
13
+
14
+ const fg = selected ? SemanticColors.selectionText : SemanticColors.text;
15
+ const bg = selected ? SemanticColors.selectionBackground : undefined;
16
+
17
+ return (
18
+ <box flexDirection="column">
19
+ <text fg={fg} bg={bg} {...({ onClick: onActivate })}>
20
+ {prefix}{displayLabel}
21
+ </text>
22
+ {description ? (
23
+ <text fg={selected ? SemanticColors.text : SemanticColors.mutedText}>
24
+ {" "}{description}
25
+ </text>
26
+ ) : null}
27
+ </box>
28
+ );
29
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+ import type { OverlayProps } from "../../../semantic/types.ts";
3
+
4
+ export function Overlay({
5
+ zIndex = 10,
6
+ top,
7
+ left,
8
+ right,
9
+ bottom,
10
+ width,
11
+ height,
12
+ children,
13
+ }: OverlayProps & { children?: ReactNode }) {
14
+ return (
15
+ <box position="absolute" top={0} left={0} right={0} bottom={0} zIndex={zIndex}>
16
+ <box position="absolute" top={top as any} left={left as any} right={right as any} bottom={bottom as any} width={width as any} height={height as any}>
17
+ {children}
18
+ </box>
19
+ </box>
20
+ );
21
+ }
@@ -0,0 +1,78 @@
1
+ import type { ReactNode } from "react";
2
+ import type { PanelProps, Spacing } from "../../../semantic/types.ts";
3
+ import { SemanticColors } from "../../../theme.ts";
4
+
5
+ function normalizePadding(
6
+ padding: number | Spacing | undefined,
7
+ opts: { dense: boolean }
8
+ ): { padding?: number; paddingTop?: number; paddingRight?: number; paddingBottom?: number; paddingLeft?: number } {
9
+ if (padding === undefined) {
10
+ return opts.dense
11
+ ? {
12
+ padding: 0,
13
+ paddingLeft: 1,
14
+ paddingRight: 1,
15
+ }
16
+ : { padding: 1 };
17
+ }
18
+
19
+ if (typeof padding === "number") {
20
+ return { padding };
21
+ }
22
+
23
+ return {
24
+ paddingTop: padding.top ?? 0,
25
+ paddingRight: padding.right ?? 0,
26
+ paddingBottom: padding.bottom ?? 0,
27
+ paddingLeft: padding.left ?? 0,
28
+ };
29
+ }
30
+
31
+ export function Panel({
32
+ title,
33
+ focused,
34
+ border = true,
35
+ surface = "panel",
36
+ dense = false,
37
+ children,
38
+ flex,
39
+ width,
40
+ height,
41
+ flexDirection,
42
+ alignItems,
43
+ justifyContent,
44
+ gap,
45
+ padding,
46
+ noShrink,
47
+ }: PanelProps & { children?: ReactNode }) {
48
+ const backgroundColor = surface === "overlay" ? SemanticColors.overlay : SemanticColors.panelBackground;
49
+
50
+ const borderColor = surface === "overlay" ? SemanticColors.warning : focused ? SemanticColors.focusBorder : SemanticColors.border;
51
+
52
+ const resolvedPadding = normalizePadding(padding, { dense });
53
+
54
+ return (
55
+ <box
56
+ border={border}
57
+ borderStyle={border ? "rounded" : undefined}
58
+ borderColor={borderColor}
59
+ title={title}
60
+ padding={resolvedPadding.padding}
61
+ paddingTop={resolvedPadding.paddingTop}
62
+ paddingRight={resolvedPadding.paddingRight}
63
+ paddingBottom={resolvedPadding.paddingBottom}
64
+ paddingLeft={resolvedPadding.paddingLeft}
65
+ flexGrow={flex}
66
+ flexShrink={noShrink ? 0 : flex === undefined ? undefined : 1}
67
+ width={width as any}
68
+ height={height as any}
69
+ flexDirection={flexDirection as any}
70
+ alignItems={alignItems as any}
71
+ justifyContent={justifyContent as any}
72
+ gap={gap}
73
+ backgroundColor={backgroundColor}
74
+ >
75
+ {children}
76
+ </box>
77
+ );
78
+ }