@hauktui/registry 0.0.1
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/components/accordion/accordion.tsx +146 -0
- package/components/accordion/index.ts +2 -0
- package/components/alert/alert.tsx +69 -0
- package/components/alert/index.ts +2 -0
- package/components/alert-dialog/alert-dialog.tsx +185 -0
- package/components/alert-dialog/index.ts +2 -0
- package/components/avatar/avatar.tsx +57 -0
- package/components/avatar/index.ts +2 -0
- package/components/avatar-group/avatar-group.tsx +144 -0
- package/components/avatar-group/index.ts +2 -0
- package/components/badge/badge.tsx +52 -0
- package/components/badge/index.ts +2 -0
- package/components/banner/banner.tsx +407 -0
- package/components/banner/index.ts +2 -0
- package/components/breadcrumb/breadcrumb.tsx +58 -0
- package/components/breadcrumb/index.ts +2 -0
- package/components/button/button.tsx +114 -0
- package/components/button/index.ts +2 -0
- package/components/calendar/calendar.tsx +250 -0
- package/components/calendar/index.ts +2 -0
- package/components/card/card.tsx +88 -0
- package/components/card/index.ts +2 -0
- package/components/carousel/carousel.tsx +185 -0
- package/components/carousel/index.ts +2 -0
- package/components/chart/chart.tsx +189 -0
- package/components/chart/index.ts +2 -0
- package/components/checkbox/checkbox.tsx +98 -0
- package/components/checkbox/index.ts +2 -0
- package/components/code-block/code-block.tsx +214 -0
- package/components/code-block/index.ts +2 -0
- package/components/collapsible/collapsible.tsx +123 -0
- package/components/collapsible/index.ts +2 -0
- package/components/color-picker/color-picker.tsx +211 -0
- package/components/color-picker/index.ts +2 -0
- package/components/combobox/combobox.tsx +275 -0
- package/components/combobox/index.ts +2 -0
- package/components/command/command.tsx +304 -0
- package/components/command/index.ts +2 -0
- package/components/confirm-dialog/confirm-dialog.tsx +140 -0
- package/components/confirm-dialog/index.ts +2 -0
- package/components/context-menu/context-menu.tsx +188 -0
- package/components/context-menu/index.ts +2 -0
- package/components/countdown/countdown.tsx +165 -0
- package/components/countdown/index.ts +2 -0
- package/components/data-table/data-table.tsx +256 -0
- package/components/data-table/index.ts +2 -0
- package/components/date-picker/date-picker.tsx +280 -0
- package/components/date-picker/index.ts +2 -0
- package/components/dialog/dialog.tsx +84 -0
- package/components/dialog/index.ts +2 -0
- package/components/drawer/drawer.tsx +141 -0
- package/components/drawer/index.ts +2 -0
- package/components/dropdown-menu/dropdown-menu.tsx +188 -0
- package/components/dropdown-menu/index.ts +2 -0
- package/components/empty/empty.tsx +107 -0
- package/components/empty/index.ts +2 -0
- package/components/field/field.tsx +83 -0
- package/components/field/index.ts +2 -0
- package/components/form/form.tsx +202 -0
- package/components/form/index.ts +8 -0
- package/components/hover-card/hover-card.tsx +72 -0
- package/components/hover-card/index.ts +2 -0
- package/components/input-otp/index.ts +2 -0
- package/components/input-otp/input-otp.tsx +176 -0
- package/components/kbd/index.ts +2 -0
- package/components/kbd/kbd.tsx +30 -0
- package/components/label/index.ts +2 -0
- package/components/label/label.tsx +56 -0
- package/components/list/index.ts +2 -0
- package/components/list/list.tsx +247 -0
- package/components/menubar/index.ts +2 -0
- package/components/menubar/menubar.tsx +220 -0
- package/components/navigation-menu/index.ts +6 -0
- package/components/navigation-menu/navigation-menu.tsx +216 -0
- package/components/pagination/index.ts +2 -0
- package/components/pagination/pagination.tsx +158 -0
- package/components/password-input/index.ts +2 -0
- package/components/password-input/password-input.tsx +198 -0
- package/components/popover/index.ts +2 -0
- package/components/popover/popover.tsx +102 -0
- package/components/progress/index.ts +2 -0
- package/components/progress/progress.tsx +73 -0
- package/components/radio-group/index.ts +2 -0
- package/components/radio-group/radio-group.tsx +167 -0
- package/components/resizable/index.ts +2 -0
- package/components/resizable/resizable.tsx +141 -0
- package/components/scroll-area/index.ts +2 -0
- package/components/scroll-area/scroll-area.tsx +133 -0
- package/components/select/index.ts +2 -0
- package/components/select/select.tsx +185 -0
- package/components/separator/index.ts +2 -0
- package/components/separator/separator.tsx +63 -0
- package/components/sheet/index.ts +2 -0
- package/components/sheet/sheet.tsx +137 -0
- package/components/sidebar/index.ts +2 -0
- package/components/sidebar/sidebar.tsx +225 -0
- package/components/skeleton/index.ts +2 -0
- package/components/skeleton/skeleton.tsx +64 -0
- package/components/slider/index.ts +2 -0
- package/components/slider/slider.tsx +128 -0
- package/components/spinner/index.ts +2 -0
- package/components/spinner/spinner.tsx +57 -0
- package/components/stat/index.ts +2 -0
- package/components/stat/stat.tsx +138 -0
- package/components/stepper/index.ts +2 -0
- package/components/stepper/stepper.tsx +219 -0
- package/components/switch/index.ts +2 -0
- package/components/switch/switch.tsx +102 -0
- package/components/table/index.ts +2 -0
- package/components/table/table.tsx +242 -0
- package/components/tabs/index.ts +2 -0
- package/components/tabs/tabs.tsx +240 -0
- package/components/tag-input/index.ts +2 -0
- package/components/tag-input/tag-input.tsx +180 -0
- package/components/terminal/index.ts +2 -0
- package/components/terminal/terminal.tsx +162 -0
- package/components/text-input/index.ts +2 -0
- package/components/text-input/text-input.tsx +179 -0
- package/components/textarea/index.ts +2 -0
- package/components/textarea/textarea.tsx +206 -0
- package/components/timeline/index.ts +2 -0
- package/components/timeline/timeline.tsx +167 -0
- package/components/toast/index.ts +2 -0
- package/components/toast/toast.tsx +93 -0
- package/components/toggle/index.ts +2 -0
- package/components/toggle/toggle.tsx +114 -0
- package/components/toggle-group/index.ts +2 -0
- package/components/toggle-group/toggle-group.tsx +176 -0
- package/components/tooltip/index.ts +2 -0
- package/components/tooltip/tooltip.tsx +65 -0
- package/components/tree-view/index.ts +2 -0
- package/components/tree-view/tree-view.tsx +245 -0
- package/components/typography/index.ts +12 -0
- package/components/typography/typography.tsx +154 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +938 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/registry.json +923 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import type { Tokens } from "@hauktui/tokens";
|
|
4
|
+
import { useTokens, useFocusable } from "@hauktui/primitives-ink";
|
|
5
|
+
import { stableId, clamp } from "@hauktui/core";
|
|
6
|
+
|
|
7
|
+
export interface Tab {
|
|
8
|
+
/** Tab key/identifier */
|
|
9
|
+
key: string;
|
|
10
|
+
/** Tab label */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Whether this tab is disabled */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TabsProps {
|
|
17
|
+
/** Available tabs */
|
|
18
|
+
tabs: Tab[];
|
|
19
|
+
/** Controlled active tab key */
|
|
20
|
+
activeKey?: string;
|
|
21
|
+
/** Default active tab for uncontrolled mode */
|
|
22
|
+
defaultActiveKey?: string;
|
|
23
|
+
/** Callback when tab changes */
|
|
24
|
+
onChange?: (key: string) => void;
|
|
25
|
+
/** Whether the tabs component is disabled */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Custom tokens override */
|
|
28
|
+
tokens?: Tokens;
|
|
29
|
+
/** Focus ID for focus management */
|
|
30
|
+
focusId?: string;
|
|
31
|
+
/** Tab orientation */
|
|
32
|
+
orientation?: "horizontal" | "vertical";
|
|
33
|
+
/** Tab style variant */
|
|
34
|
+
variant?: "default" | "boxed" | "underline";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Tabs({
|
|
38
|
+
tabs,
|
|
39
|
+
activeKey: controlledActiveKey,
|
|
40
|
+
defaultActiveKey,
|
|
41
|
+
onChange,
|
|
42
|
+
disabled = false,
|
|
43
|
+
tokens: propTokens,
|
|
44
|
+
focusId,
|
|
45
|
+
orientation = "horizontal",
|
|
46
|
+
variant = "default",
|
|
47
|
+
}: TabsProps): React.ReactElement {
|
|
48
|
+
const contextTokens = useTokens();
|
|
49
|
+
const tokens = propTokens ?? contextTokens;
|
|
50
|
+
const id = focusId ?? stableId("tabs");
|
|
51
|
+
const { isFocused } = useFocusable(id);
|
|
52
|
+
|
|
53
|
+
// Internal state for uncontrolled mode
|
|
54
|
+
const defaultKey = defaultActiveKey ?? tabs[0]?.key;
|
|
55
|
+
const [internalActiveKey, setInternalActiveKey] = useState<string | undefined>(defaultKey);
|
|
56
|
+
const isControlled = controlledActiveKey !== undefined;
|
|
57
|
+
const currentActiveKey = isControlled ? controlledActiveKey : internalActiveKey;
|
|
58
|
+
|
|
59
|
+
const activeIndex = tabs.findIndex((t) => t.key === currentActiveKey);
|
|
60
|
+
|
|
61
|
+
const selectTab = useCallback(
|
|
62
|
+
(key: string) => {
|
|
63
|
+
const tab = tabs.find((t) => t.key === key);
|
|
64
|
+
if (!tab || tab.disabled) return;
|
|
65
|
+
|
|
66
|
+
if (!isControlled) {
|
|
67
|
+
setInternalActiveKey(key);
|
|
68
|
+
}
|
|
69
|
+
onChange?.(key);
|
|
70
|
+
},
|
|
71
|
+
[tabs, isControlled, onChange]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const navigateTo = useCallback(
|
|
75
|
+
(direction: 1 | -1) => {
|
|
76
|
+
let nextIndex = activeIndex + direction;
|
|
77
|
+
|
|
78
|
+
// Skip disabled tabs
|
|
79
|
+
while (
|
|
80
|
+
nextIndex >= 0 &&
|
|
81
|
+
nextIndex < tabs.length &&
|
|
82
|
+
tabs[nextIndex]?.disabled
|
|
83
|
+
) {
|
|
84
|
+
nextIndex += direction;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
nextIndex = clamp(nextIndex, 0, tabs.length - 1);
|
|
88
|
+
const nextTab = tabs[nextIndex];
|
|
89
|
+
if (nextTab && !nextTab.disabled) {
|
|
90
|
+
selectTab(nextTab.key);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[activeIndex, tabs, selectTab]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Handle keyboard navigation
|
|
97
|
+
useInput(
|
|
98
|
+
(input, key) => {
|
|
99
|
+
if (!isFocused || disabled) return;
|
|
100
|
+
|
|
101
|
+
const isHorizontal = orientation === "horizontal";
|
|
102
|
+
|
|
103
|
+
if (isHorizontal) {
|
|
104
|
+
if (key.leftArrow || input === "h") {
|
|
105
|
+
navigateTo(-1);
|
|
106
|
+
} else if (key.rightArrow || input === "l") {
|
|
107
|
+
navigateTo(1);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
if (key.upArrow || input === "k") {
|
|
111
|
+
navigateTo(-1);
|
|
112
|
+
} else if (key.downArrow || input === "j") {
|
|
113
|
+
navigateTo(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Number keys for quick access (1-9)
|
|
118
|
+
const num = parseInt(input, 10);
|
|
119
|
+
if (num >= 1 && num <= 9 && num <= tabs.length) {
|
|
120
|
+
const tab = tabs[num - 1];
|
|
121
|
+
if (tab && !tab.disabled) {
|
|
122
|
+
selectTab(tab.key);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Home/End
|
|
127
|
+
if (input === "g" && key.shift === false) {
|
|
128
|
+
// Go to first tab
|
|
129
|
+
const firstEnabled = tabs.find((t) => !t.disabled);
|
|
130
|
+
if (firstEnabled) selectTab(firstEnabled.key);
|
|
131
|
+
} else if (input === "G") {
|
|
132
|
+
// Go to last tab
|
|
133
|
+
const lastEnabled = [...tabs].reverse().find((t) => !t.disabled);
|
|
134
|
+
if (lastEnabled) selectTab(lastEnabled.key);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{ isActive: isFocused }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Render a single tab
|
|
141
|
+
const renderTab = (tab: Tab, index: number) => {
|
|
142
|
+
const isActive = tab.key === currentActiveKey;
|
|
143
|
+
const isDisabled = tab.disabled;
|
|
144
|
+
|
|
145
|
+
let color = tokens.colors.fg;
|
|
146
|
+
if (isDisabled) {
|
|
147
|
+
color = tokens.colors.disabled;
|
|
148
|
+
} else if (isActive) {
|
|
149
|
+
color = tokens.colors.accent;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Different styles based on variant
|
|
153
|
+
let prefix = "";
|
|
154
|
+
let suffix = "";
|
|
155
|
+
|
|
156
|
+
if (variant === "boxed") {
|
|
157
|
+
prefix = isActive ? "[" : " ";
|
|
158
|
+
suffix = isActive ? "]" : " ";
|
|
159
|
+
} else if (variant === "underline") {
|
|
160
|
+
suffix = isActive ? "─" : " ";
|
|
161
|
+
} else {
|
|
162
|
+
prefix = isActive ? "● " : "○ ";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tabNumber = index + 1 <= 9 ? `${index + 1}.` : " ";
|
|
166
|
+
|
|
167
|
+
return React.createElement(
|
|
168
|
+
Box,
|
|
169
|
+
{
|
|
170
|
+
key: tab.key,
|
|
171
|
+
paddingX: orientation === "horizontal" ? 1 : 0,
|
|
172
|
+
paddingY: orientation === "vertical" ? 0 : 0,
|
|
173
|
+
},
|
|
174
|
+
React.createElement(
|
|
175
|
+
Text,
|
|
176
|
+
{
|
|
177
|
+
color,
|
|
178
|
+
bold: isActive && isFocused,
|
|
179
|
+
dimColor: isDisabled,
|
|
180
|
+
inverse: isActive && isFocused && variant !== "underline",
|
|
181
|
+
},
|
|
182
|
+
prefix,
|
|
183
|
+
React.createElement(Text, { color: tokens.colors.muted }, tabNumber, " "),
|
|
184
|
+
tab.label,
|
|
185
|
+
suffix
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Render underline for underline variant
|
|
191
|
+
const renderUnderline = () => {
|
|
192
|
+
if (variant !== "underline" || orientation !== "horizontal") return null;
|
|
193
|
+
|
|
194
|
+
return React.createElement(
|
|
195
|
+
Box,
|
|
196
|
+
null,
|
|
197
|
+
tabs.map((tab) => {
|
|
198
|
+
const isActive = tab.key === currentActiveKey;
|
|
199
|
+
const width = tab.label.length + 5; // account for number prefix and padding
|
|
200
|
+
|
|
201
|
+
return React.createElement(
|
|
202
|
+
Box,
|
|
203
|
+
{ key: tab.key, paddingX: 1 },
|
|
204
|
+
React.createElement(
|
|
205
|
+
Text,
|
|
206
|
+
{ color: isActive ? tokens.colors.accent : tokens.colors.border },
|
|
207
|
+
isActive ? "─".repeat(width) : " ".repeat(width)
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return React.createElement(
|
|
215
|
+
Box,
|
|
216
|
+
{
|
|
217
|
+
flexDirection: "column",
|
|
218
|
+
borderStyle: isFocused ? "round" : "single",
|
|
219
|
+
borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
|
|
220
|
+
paddingX: 1,
|
|
221
|
+
},
|
|
222
|
+
React.createElement(
|
|
223
|
+
Box,
|
|
224
|
+
{
|
|
225
|
+
flexDirection: orientation === "horizontal" ? "row" : "column",
|
|
226
|
+
gap: orientation === "horizontal" ? 0 : 0,
|
|
227
|
+
},
|
|
228
|
+
tabs.map(renderTab)
|
|
229
|
+
),
|
|
230
|
+
renderUnderline(),
|
|
231
|
+
// Hint
|
|
232
|
+
isFocused
|
|
233
|
+
? React.createElement(
|
|
234
|
+
Text,
|
|
235
|
+
{ color: tokens.colors.muted, dimColor: true },
|
|
236
|
+
orientation === "horizontal" ? "←/→ or 1-9" : "↑/↓ or 1-9"
|
|
237
|
+
)
|
|
238
|
+
: null
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import type { Tokens } from "@hauktui/tokens";
|
|
4
|
+
import { useTokens, useFocusable } from "@hauktui/primitives-ink";
|
|
5
|
+
import { stableId } from "@hauktui/core";
|
|
6
|
+
|
|
7
|
+
export interface TagInputProps {
|
|
8
|
+
/** Current tags */
|
|
9
|
+
value: string[];
|
|
10
|
+
/** Callback when tags change */
|
|
11
|
+
onChange?: (tags: string[]) => void;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Maximum number of tags */
|
|
15
|
+
maxTags?: number;
|
|
16
|
+
/** Allowed tag separator characters */
|
|
17
|
+
separators?: string[];
|
|
18
|
+
/** Custom tokens override */
|
|
19
|
+
tokens?: Tokens;
|
|
20
|
+
/** Focus ID for focus management */
|
|
21
|
+
focusId?: string;
|
|
22
|
+
/** Tag variant */
|
|
23
|
+
variant?: "default" | "outline";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TagInput({
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
placeholder = "Add tag...",
|
|
30
|
+
maxTags,
|
|
31
|
+
separators = [",", " ", "Enter"],
|
|
32
|
+
tokens: propTokens,
|
|
33
|
+
focusId,
|
|
34
|
+
variant = "default",
|
|
35
|
+
}: TagInputProps): React.ReactElement {
|
|
36
|
+
const contextTokens = useTokens();
|
|
37
|
+
const tokens = propTokens ?? contextTokens;
|
|
38
|
+
const id = focusId ?? stableId("tag-input");
|
|
39
|
+
const { isFocused } = useFocusable(id);
|
|
40
|
+
|
|
41
|
+
const [inputValue, setInputValue] = useState("");
|
|
42
|
+
const [focusedTagIndex, setFocusedTagIndex] = useState(-1); // -1 means input is focused
|
|
43
|
+
|
|
44
|
+
// Add a tag
|
|
45
|
+
const addTag = (tag: string) => {
|
|
46
|
+
const trimmed = tag.trim();
|
|
47
|
+
if (!trimmed) return;
|
|
48
|
+
if (value.includes(trimmed)) return;
|
|
49
|
+
if (maxTags && value.length >= maxTags) return;
|
|
50
|
+
|
|
51
|
+
onChange?.([...value, trimmed]);
|
|
52
|
+
setInputValue("");
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Remove a tag
|
|
56
|
+
const removeTag = (index: number) => {
|
|
57
|
+
const newTags = [...value];
|
|
58
|
+
newTags.splice(index, 1);
|
|
59
|
+
onChange?.(newTags);
|
|
60
|
+
setFocusedTagIndex(-1);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Handle keyboard input
|
|
64
|
+
useInput(
|
|
65
|
+
(input, key) => {
|
|
66
|
+
if (!isFocused) return;
|
|
67
|
+
|
|
68
|
+
// If we're focused on a tag
|
|
69
|
+
if (focusedTagIndex >= 0) {
|
|
70
|
+
if (key.leftArrow || input === "h") {
|
|
71
|
+
setFocusedTagIndex((prev) => Math.max(0, prev - 1));
|
|
72
|
+
} else if (key.rightArrow || input === "l") {
|
|
73
|
+
if (focusedTagIndex < value.length - 1) {
|
|
74
|
+
setFocusedTagIndex((prev) => prev + 1);
|
|
75
|
+
} else {
|
|
76
|
+
setFocusedTagIndex(-1); // Go to input
|
|
77
|
+
}
|
|
78
|
+
} else if (key.backspace || key.delete || input === "x") {
|
|
79
|
+
removeTag(focusedTagIndex);
|
|
80
|
+
} else if (key.escape) {
|
|
81
|
+
setFocusedTagIndex(-1);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Input mode
|
|
87
|
+
if (key.backspace) {
|
|
88
|
+
if (inputValue === "" && value.length > 0) {
|
|
89
|
+
setFocusedTagIndex(value.length - 1);
|
|
90
|
+
} else {
|
|
91
|
+
setInputValue((prev) => prev.slice(0, -1));
|
|
92
|
+
}
|
|
93
|
+
} else if (key.return || (separators.includes(input) && inputValue)) {
|
|
94
|
+
addTag(inputValue);
|
|
95
|
+
} else if (key.leftArrow && inputValue === "" && value.length > 0) {
|
|
96
|
+
setFocusedTagIndex(value.length - 1);
|
|
97
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
98
|
+
if (!separators.includes(input)) {
|
|
99
|
+
setInputValue((prev) => prev + input);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{ isActive: isFocused }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Render a tag
|
|
107
|
+
const renderTag = (tag: string, index: number) => {
|
|
108
|
+
const isFocusedTag = focusedTagIndex === index;
|
|
109
|
+
|
|
110
|
+
return React.createElement(
|
|
111
|
+
Box,
|
|
112
|
+
{
|
|
113
|
+
key: index,
|
|
114
|
+
borderStyle: variant === "outline" ? "round" : undefined,
|
|
115
|
+
borderColor: isFocusedTag ? tokens.colors.danger : tokens.colors.accent,
|
|
116
|
+
paddingX: variant === "outline" ? 0 : undefined,
|
|
117
|
+
},
|
|
118
|
+
React.createElement(
|
|
119
|
+
Text,
|
|
120
|
+
{
|
|
121
|
+
backgroundColor:
|
|
122
|
+
variant === "default"
|
|
123
|
+
? isFocusedTag
|
|
124
|
+
? tokens.colors.danger
|
|
125
|
+
: tokens.colors.accent
|
|
126
|
+
: undefined,
|
|
127
|
+
color:
|
|
128
|
+
variant === "default"
|
|
129
|
+
? tokens.colors.bg
|
|
130
|
+
: isFocusedTag
|
|
131
|
+
? tokens.colors.danger
|
|
132
|
+
: tokens.colors.accent,
|
|
133
|
+
},
|
|
134
|
+
` ${tag} `
|
|
135
|
+
),
|
|
136
|
+
isFocusedTag &&
|
|
137
|
+
React.createElement(Text, { color: tokens.colors.danger }, "×")
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return React.createElement(
|
|
142
|
+
Box,
|
|
143
|
+
{
|
|
144
|
+
flexDirection: "row",
|
|
145
|
+
flexWrap: "wrap",
|
|
146
|
+
gap: 1,
|
|
147
|
+
borderStyle: isFocused ? "round" : "single",
|
|
148
|
+
borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
|
|
149
|
+
paddingX: 1,
|
|
150
|
+
paddingY: 0,
|
|
151
|
+
},
|
|
152
|
+
// Existing tags
|
|
153
|
+
...value.map(renderTag),
|
|
154
|
+
// Input
|
|
155
|
+
React.createElement(
|
|
156
|
+
Box,
|
|
157
|
+
{},
|
|
158
|
+
React.createElement(
|
|
159
|
+
Text,
|
|
160
|
+
{
|
|
161
|
+
color:
|
|
162
|
+
inputValue || focusedTagIndex === -1
|
|
163
|
+
? tokens.colors.fg
|
|
164
|
+
: tokens.colors.muted,
|
|
165
|
+
},
|
|
166
|
+
inputValue || (focusedTagIndex === -1 ? placeholder : "")
|
|
167
|
+
),
|
|
168
|
+
isFocused &&
|
|
169
|
+
focusedTagIndex === -1 &&
|
|
170
|
+
React.createElement(Text, { color: tokens.colors.accent }, "█")
|
|
171
|
+
),
|
|
172
|
+
// Max tags indicator
|
|
173
|
+
maxTags &&
|
|
174
|
+
React.createElement(
|
|
175
|
+
Text,
|
|
176
|
+
{ color: tokens.colors.muted, dimColor: true },
|
|
177
|
+
` (${value.length}/${maxTags})`
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { Tokens } from "@hauktui/tokens";
|
|
4
|
+
import { useTokens } from "@hauktui/primitives-ink";
|
|
5
|
+
|
|
6
|
+
export interface TerminalLine {
|
|
7
|
+
/** Line content */
|
|
8
|
+
content: string;
|
|
9
|
+
/** Line type */
|
|
10
|
+
type?: "command" | "output" | "error" | "success" | "info" | "warning";
|
|
11
|
+
/** Optional prefix/prompt */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
/** Timestamp */
|
|
14
|
+
timestamp?: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TerminalProps {
|
|
18
|
+
/** Terminal lines */
|
|
19
|
+
lines: TerminalLine[];
|
|
20
|
+
/** Terminal title */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** Default prompt prefix */
|
|
23
|
+
prompt?: string;
|
|
24
|
+
/** Show timestamps */
|
|
25
|
+
showTimestamps?: boolean;
|
|
26
|
+
/** Maximum lines to display (scrollable) */
|
|
27
|
+
maxLines?: number;
|
|
28
|
+
/** Custom tokens override */
|
|
29
|
+
tokens?: Tokens;
|
|
30
|
+
/** Terminal width */
|
|
31
|
+
width?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Terminal({
|
|
35
|
+
lines,
|
|
36
|
+
title = "Terminal",
|
|
37
|
+
prompt = "$ ",
|
|
38
|
+
showTimestamps = false,
|
|
39
|
+
maxLines,
|
|
40
|
+
tokens: propTokens,
|
|
41
|
+
width,
|
|
42
|
+
}: TerminalProps): React.ReactElement {
|
|
43
|
+
const contextTokens = useTokens();
|
|
44
|
+
const tokens = propTokens ?? contextTokens;
|
|
45
|
+
|
|
46
|
+
// Limit lines if maxLines is set
|
|
47
|
+
const displayLines = maxLines ? lines.slice(-maxLines) : lines;
|
|
48
|
+
|
|
49
|
+
// Get color for line type
|
|
50
|
+
const getLineColor = (type: TerminalLine["type"]): string => {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case "command":
|
|
53
|
+
return tokens.colors.accent;
|
|
54
|
+
case "error":
|
|
55
|
+
return tokens.colors.danger;
|
|
56
|
+
case "success":
|
|
57
|
+
return tokens.colors.success;
|
|
58
|
+
case "info":
|
|
59
|
+
return tokens.colors.info;
|
|
60
|
+
case "warning":
|
|
61
|
+
return tokens.colors.warning;
|
|
62
|
+
case "output":
|
|
63
|
+
default:
|
|
64
|
+
return tokens.colors.fg;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Format timestamp
|
|
69
|
+
const formatTimestamp = (date: Date): string => {
|
|
70
|
+
return date.toLocaleTimeString("en-US", {
|
|
71
|
+
hour12: false,
|
|
72
|
+
hour: "2-digit",
|
|
73
|
+
minute: "2-digit",
|
|
74
|
+
second: "2-digit",
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return React.createElement(
|
|
79
|
+
Box,
|
|
80
|
+
{
|
|
81
|
+
flexDirection: "column",
|
|
82
|
+
borderStyle: "round",
|
|
83
|
+
borderColor: tokens.colors.border,
|
|
84
|
+
...(width ? { width } : {}),
|
|
85
|
+
},
|
|
86
|
+
// Title bar
|
|
87
|
+
React.createElement(
|
|
88
|
+
Box,
|
|
89
|
+
{
|
|
90
|
+
paddingX: 1,
|
|
91
|
+
justifyContent: "space-between",
|
|
92
|
+
borderStyle: "single",
|
|
93
|
+
borderTop: false,
|
|
94
|
+
borderLeft: false,
|
|
95
|
+
borderRight: false,
|
|
96
|
+
borderColor: tokens.colors.border,
|
|
97
|
+
},
|
|
98
|
+
React.createElement(
|
|
99
|
+
Box,
|
|
100
|
+
{ gap: 1 },
|
|
101
|
+
React.createElement(Text, { color: tokens.colors.danger }, "●"),
|
|
102
|
+
React.createElement(Text, { color: tokens.colors.warning }, "●"),
|
|
103
|
+
React.createElement(Text, { color: tokens.colors.success }, "●")
|
|
104
|
+
),
|
|
105
|
+
React.createElement(
|
|
106
|
+
Text,
|
|
107
|
+
{ color: tokens.colors.muted, bold: true },
|
|
108
|
+
title
|
|
109
|
+
),
|
|
110
|
+
React.createElement(Text, {}, " ") // Spacer for symmetry
|
|
111
|
+
),
|
|
112
|
+
// Terminal content
|
|
113
|
+
React.createElement(
|
|
114
|
+
Box,
|
|
115
|
+
{ flexDirection: "column", paddingX: 1, paddingY: 1 },
|
|
116
|
+
displayLines.length === 0
|
|
117
|
+
? React.createElement(
|
|
118
|
+
Text,
|
|
119
|
+
{ color: tokens.colors.muted },
|
|
120
|
+
prompt + "█"
|
|
121
|
+
)
|
|
122
|
+
: displayLines.map((line, index) => {
|
|
123
|
+
const prefix =
|
|
124
|
+
line.type === "command"
|
|
125
|
+
? (line.prefix ?? prompt)
|
|
126
|
+
: (line.prefix ?? "");
|
|
127
|
+
|
|
128
|
+
return React.createElement(
|
|
129
|
+
Box,
|
|
130
|
+
{ key: index, gap: 1 },
|
|
131
|
+
// Timestamp
|
|
132
|
+
showTimestamps &&
|
|
133
|
+
line.timestamp &&
|
|
134
|
+
React.createElement(
|
|
135
|
+
Text,
|
|
136
|
+
{ color: tokens.colors.muted },
|
|
137
|
+
"[" + formatTimestamp(line.timestamp) + "]"
|
|
138
|
+
),
|
|
139
|
+
// Prefix/prompt
|
|
140
|
+
prefix &&
|
|
141
|
+
React.createElement(
|
|
142
|
+
Text,
|
|
143
|
+
{
|
|
144
|
+
color:
|
|
145
|
+
line.type === "command"
|
|
146
|
+
? tokens.colors.success
|
|
147
|
+
: tokens.colors.muted,
|
|
148
|
+
bold: line.type === "command",
|
|
149
|
+
},
|
|
150
|
+
prefix
|
|
151
|
+
),
|
|
152
|
+
// Content
|
|
153
|
+
React.createElement(
|
|
154
|
+
Text,
|
|
155
|
+
{ color: getLineColor(line.type) },
|
|
156
|
+
line.content
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|