@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.
Files changed (139) hide show
  1. package/components/accordion/accordion.tsx +146 -0
  2. package/components/accordion/index.ts +2 -0
  3. package/components/alert/alert.tsx +69 -0
  4. package/components/alert/index.ts +2 -0
  5. package/components/alert-dialog/alert-dialog.tsx +185 -0
  6. package/components/alert-dialog/index.ts +2 -0
  7. package/components/avatar/avatar.tsx +57 -0
  8. package/components/avatar/index.ts +2 -0
  9. package/components/avatar-group/avatar-group.tsx +144 -0
  10. package/components/avatar-group/index.ts +2 -0
  11. package/components/badge/badge.tsx +52 -0
  12. package/components/badge/index.ts +2 -0
  13. package/components/banner/banner.tsx +407 -0
  14. package/components/banner/index.ts +2 -0
  15. package/components/breadcrumb/breadcrumb.tsx +58 -0
  16. package/components/breadcrumb/index.ts +2 -0
  17. package/components/button/button.tsx +114 -0
  18. package/components/button/index.ts +2 -0
  19. package/components/calendar/calendar.tsx +250 -0
  20. package/components/calendar/index.ts +2 -0
  21. package/components/card/card.tsx +88 -0
  22. package/components/card/index.ts +2 -0
  23. package/components/carousel/carousel.tsx +185 -0
  24. package/components/carousel/index.ts +2 -0
  25. package/components/chart/chart.tsx +189 -0
  26. package/components/chart/index.ts +2 -0
  27. package/components/checkbox/checkbox.tsx +98 -0
  28. package/components/checkbox/index.ts +2 -0
  29. package/components/code-block/code-block.tsx +214 -0
  30. package/components/code-block/index.ts +2 -0
  31. package/components/collapsible/collapsible.tsx +123 -0
  32. package/components/collapsible/index.ts +2 -0
  33. package/components/color-picker/color-picker.tsx +211 -0
  34. package/components/color-picker/index.ts +2 -0
  35. package/components/combobox/combobox.tsx +275 -0
  36. package/components/combobox/index.ts +2 -0
  37. package/components/command/command.tsx +304 -0
  38. package/components/command/index.ts +2 -0
  39. package/components/confirm-dialog/confirm-dialog.tsx +140 -0
  40. package/components/confirm-dialog/index.ts +2 -0
  41. package/components/context-menu/context-menu.tsx +188 -0
  42. package/components/context-menu/index.ts +2 -0
  43. package/components/countdown/countdown.tsx +165 -0
  44. package/components/countdown/index.ts +2 -0
  45. package/components/data-table/data-table.tsx +256 -0
  46. package/components/data-table/index.ts +2 -0
  47. package/components/date-picker/date-picker.tsx +280 -0
  48. package/components/date-picker/index.ts +2 -0
  49. package/components/dialog/dialog.tsx +84 -0
  50. package/components/dialog/index.ts +2 -0
  51. package/components/drawer/drawer.tsx +141 -0
  52. package/components/drawer/index.ts +2 -0
  53. package/components/dropdown-menu/dropdown-menu.tsx +188 -0
  54. package/components/dropdown-menu/index.ts +2 -0
  55. package/components/empty/empty.tsx +107 -0
  56. package/components/empty/index.ts +2 -0
  57. package/components/field/field.tsx +83 -0
  58. package/components/field/index.ts +2 -0
  59. package/components/form/form.tsx +202 -0
  60. package/components/form/index.ts +8 -0
  61. package/components/hover-card/hover-card.tsx +72 -0
  62. package/components/hover-card/index.ts +2 -0
  63. package/components/input-otp/index.ts +2 -0
  64. package/components/input-otp/input-otp.tsx +176 -0
  65. package/components/kbd/index.ts +2 -0
  66. package/components/kbd/kbd.tsx +30 -0
  67. package/components/label/index.ts +2 -0
  68. package/components/label/label.tsx +56 -0
  69. package/components/list/index.ts +2 -0
  70. package/components/list/list.tsx +247 -0
  71. package/components/menubar/index.ts +2 -0
  72. package/components/menubar/menubar.tsx +220 -0
  73. package/components/navigation-menu/index.ts +6 -0
  74. package/components/navigation-menu/navigation-menu.tsx +216 -0
  75. package/components/pagination/index.ts +2 -0
  76. package/components/pagination/pagination.tsx +158 -0
  77. package/components/password-input/index.ts +2 -0
  78. package/components/password-input/password-input.tsx +198 -0
  79. package/components/popover/index.ts +2 -0
  80. package/components/popover/popover.tsx +102 -0
  81. package/components/progress/index.ts +2 -0
  82. package/components/progress/progress.tsx +73 -0
  83. package/components/radio-group/index.ts +2 -0
  84. package/components/radio-group/radio-group.tsx +167 -0
  85. package/components/resizable/index.ts +2 -0
  86. package/components/resizable/resizable.tsx +141 -0
  87. package/components/scroll-area/index.ts +2 -0
  88. package/components/scroll-area/scroll-area.tsx +133 -0
  89. package/components/select/index.ts +2 -0
  90. package/components/select/select.tsx +185 -0
  91. package/components/separator/index.ts +2 -0
  92. package/components/separator/separator.tsx +63 -0
  93. package/components/sheet/index.ts +2 -0
  94. package/components/sheet/sheet.tsx +137 -0
  95. package/components/sidebar/index.ts +2 -0
  96. package/components/sidebar/sidebar.tsx +225 -0
  97. package/components/skeleton/index.ts +2 -0
  98. package/components/skeleton/skeleton.tsx +64 -0
  99. package/components/slider/index.ts +2 -0
  100. package/components/slider/slider.tsx +128 -0
  101. package/components/spinner/index.ts +2 -0
  102. package/components/spinner/spinner.tsx +57 -0
  103. package/components/stat/index.ts +2 -0
  104. package/components/stat/stat.tsx +138 -0
  105. package/components/stepper/index.ts +2 -0
  106. package/components/stepper/stepper.tsx +219 -0
  107. package/components/switch/index.ts +2 -0
  108. package/components/switch/switch.tsx +102 -0
  109. package/components/table/index.ts +2 -0
  110. package/components/table/table.tsx +242 -0
  111. package/components/tabs/index.ts +2 -0
  112. package/components/tabs/tabs.tsx +240 -0
  113. package/components/tag-input/index.ts +2 -0
  114. package/components/tag-input/tag-input.tsx +180 -0
  115. package/components/terminal/index.ts +2 -0
  116. package/components/terminal/terminal.tsx +162 -0
  117. package/components/text-input/index.ts +2 -0
  118. package/components/text-input/text-input.tsx +179 -0
  119. package/components/textarea/index.ts +2 -0
  120. package/components/textarea/textarea.tsx +206 -0
  121. package/components/timeline/index.ts +2 -0
  122. package/components/timeline/timeline.tsx +167 -0
  123. package/components/toast/index.ts +2 -0
  124. package/components/toast/toast.tsx +93 -0
  125. package/components/toggle/index.ts +2 -0
  126. package/components/toggle/toggle.tsx +114 -0
  127. package/components/toggle-group/index.ts +2 -0
  128. package/components/toggle-group/toggle-group.tsx +176 -0
  129. package/components/tooltip/index.ts +2 -0
  130. package/components/tooltip/tooltip.tsx +65 -0
  131. package/components/tree-view/index.ts +2 -0
  132. package/components/tree-view/tree-view.tsx +245 -0
  133. package/components/typography/index.ts +12 -0
  134. package/components/typography/typography.tsx +154 -0
  135. package/dist/index.d.ts +102 -0
  136. package/dist/index.js +938 -0
  137. package/dist/index.js.map +1 -0
  138. package/package.json +41 -0
  139. package/registry.json +923 -0
@@ -0,0 +1,304 @@
1
+ import React, { useState, useCallback, useMemo } 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 CommandItem {
8
+ /** Unique identifier */
9
+ id: string;
10
+ /** Display label */
11
+ label: string;
12
+ /** Optional description */
13
+ description?: string;
14
+ /** Optional keyboard shortcut */
15
+ shortcut?: string;
16
+ /** Optional icon/prefix */
17
+ icon?: string;
18
+ /** Optional group/category */
19
+ group?: string;
20
+ /** Whether this item is disabled */
21
+ disabled?: boolean;
22
+ /** Keywords for search */
23
+ keywords?: string[];
24
+ }
25
+
26
+ export interface CommandProps {
27
+ /** Available command items */
28
+ items: CommandItem[];
29
+ /** Placeholder text for search input */
30
+ placeholder?: string;
31
+ /** Callback when an item is selected */
32
+ onSelect: (item: CommandItem) => void;
33
+ /** Callback when command palette should close */
34
+ onClose?: () => void;
35
+ /** Whether the command palette is open */
36
+ open?: boolean;
37
+ /** Custom tokens override */
38
+ tokens?: Tokens;
39
+ /** Focus ID for focus management */
40
+ focusId?: string;
41
+ /** Maximum visible items */
42
+ maxItems?: number;
43
+ /** Title for the command palette */
44
+ title?: string;
45
+ /** Empty state message */
46
+ emptyMessage?: string;
47
+ }
48
+
49
+ export function Command({
50
+ items,
51
+ placeholder = "Type to search...",
52
+ onSelect,
53
+ onClose,
54
+ open = true,
55
+ tokens: propTokens,
56
+ focusId,
57
+ maxItems = 8,
58
+ title,
59
+ emptyMessage = "No results found.",
60
+ }: CommandProps): React.ReactElement | null {
61
+ const contextTokens = useTokens();
62
+ const tokens = propTokens ?? contextTokens;
63
+ const id = focusId ?? stableId("command");
64
+ const { isFocused } = useFocusable(id);
65
+
66
+ const [search, setSearch] = useState("");
67
+ const [selectedIndex, setSelectedIndex] = useState(0);
68
+
69
+ // Filter items based on search
70
+ const filteredItems = useMemo(() => {
71
+ if (!search.trim()) return items;
72
+
73
+ const lowerSearch = search.toLowerCase();
74
+ return items.filter((item) => {
75
+ const labelMatch = item.label.toLowerCase().includes(lowerSearch);
76
+ const descMatch = item.description?.toLowerCase().includes(lowerSearch);
77
+ const keywordMatch = item.keywords?.some((k) =>
78
+ k.toLowerCase().includes(lowerSearch)
79
+ );
80
+ return labelMatch || descMatch || keywordMatch;
81
+ });
82
+ }, [items, search]);
83
+
84
+ // Group items
85
+ const groupedItems = useMemo(() => {
86
+ const groups: Record<string, CommandItem[]> = {};
87
+ const ungrouped: CommandItem[] = [];
88
+
89
+ for (const item of filteredItems) {
90
+ if (item.group) {
91
+ if (!groups[item.group]) {
92
+ groups[item.group] = [];
93
+ }
94
+ groups[item.group]!.push(item);
95
+ } else {
96
+ ungrouped.push(item);
97
+ }
98
+ }
99
+
100
+ return { groups, ungrouped };
101
+ }, [filteredItems]);
102
+
103
+ const selectItem = useCallback(
104
+ (item: CommandItem) => {
105
+ if (item.disabled) return;
106
+ onSelect(item);
107
+ },
108
+ [onSelect]
109
+ );
110
+
111
+ useInput(
112
+ (input, key) => {
113
+ if (!isFocused) return;
114
+
115
+ if (key.escape) {
116
+ onClose?.();
117
+ return;
118
+ }
119
+
120
+ if (key.upArrow) {
121
+ setSelectedIndex((prev) =>
122
+ clamp(prev - 1, 0, filteredItems.length - 1)
123
+ );
124
+ } else if (key.downArrow) {
125
+ setSelectedIndex((prev) =>
126
+ clamp(prev + 1, 0, filteredItems.length - 1)
127
+ );
128
+ } else if (key.return) {
129
+ const selectedItem = filteredItems[selectedIndex];
130
+ if (selectedItem) {
131
+ selectItem(selectedItem);
132
+ }
133
+ } else if (key.backspace || key.delete) {
134
+ setSearch((prev) => prev.slice(0, -1));
135
+ setSelectedIndex(0);
136
+ } else if (input && !key.ctrl && !key.meta) {
137
+ setSearch((prev) => prev + input);
138
+ setSelectedIndex(0);
139
+ }
140
+ },
141
+ { isActive: isFocused }
142
+ );
143
+
144
+ if (!open) return null;
145
+
146
+ // Flatten for display
147
+ const visibleItems = filteredItems.slice(0, maxItems);
148
+
149
+ return React.createElement(
150
+ Box,
151
+ {
152
+ flexDirection: "column",
153
+ borderStyle: "round",
154
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
155
+ width: 50,
156
+ },
157
+ // Title
158
+ title
159
+ ? React.createElement(
160
+ Box,
161
+ { paddingX: 1 },
162
+ React.createElement(
163
+ Text,
164
+ { color: tokens.colors.fg, bold: true },
165
+ title
166
+ )
167
+ )
168
+ : null,
169
+ // Search input
170
+ React.createElement(
171
+ Box,
172
+ {
173
+ paddingX: 1,
174
+ borderStyle: "single",
175
+ borderColor: tokens.colors.border,
176
+ borderTop: false,
177
+ borderLeft: false,
178
+ borderRight: false,
179
+ },
180
+ React.createElement(Text, { color: tokens.colors.muted }, "> "),
181
+ React.createElement(
182
+ Text,
183
+ { color: search ? tokens.colors.fg : tokens.colors.muted },
184
+ search || placeholder
185
+ ),
186
+ React.createElement(Text, { inverse: true }, " ")
187
+ ),
188
+ // Results
189
+ filteredItems.length === 0
190
+ ? React.createElement(
191
+ Box,
192
+ { paddingX: 1, paddingY: 1 },
193
+ React.createElement(
194
+ Text,
195
+ { color: tokens.colors.muted },
196
+ emptyMessage
197
+ )
198
+ )
199
+ : React.createElement(
200
+ Box,
201
+ { flexDirection: "column", paddingX: 1 },
202
+ // Render grouped items
203
+ ...(
204
+ Object.entries(groupedItems.groups) as [string, CommandItem[]][]
205
+ ).flatMap(([group, groupItems]) => [
206
+ React.createElement(
207
+ Box,
208
+ { key: `group-${group}`, marginTop: 1 },
209
+ React.createElement(
210
+ Text,
211
+ { color: tokens.colors.muted, dimColor: true },
212
+ group
213
+ )
214
+ ),
215
+ ...groupItems
216
+ .filter((item) => visibleItems.includes(item))
217
+ .map((item) => renderItem(item, filteredItems.indexOf(item))),
218
+ ]),
219
+ // Render ungrouped items
220
+ ...groupedItems.ungrouped
221
+ .filter((item) => visibleItems.includes(item))
222
+ .map((item) => renderItem(item, filteredItems.indexOf(item)))
223
+ ),
224
+ // Hint
225
+ React.createElement(
226
+ Box,
227
+ {
228
+ paddingX: 1,
229
+ borderStyle: "single",
230
+ borderColor: tokens.colors.border,
231
+ borderBottom: false,
232
+ borderLeft: false,
233
+ borderRight: false,
234
+ },
235
+ React.createElement(
236
+ Text,
237
+ { color: tokens.colors.muted, dimColor: true },
238
+ "↑↓ navigate • Enter select • Esc close"
239
+ )
240
+ )
241
+ );
242
+
243
+ function renderItem(item: CommandItem, index: number): React.ReactElement {
244
+ const isSelected = index === selectedIndex;
245
+ const isDisabled = item.disabled;
246
+
247
+ return React.createElement(
248
+ Box,
249
+ {
250
+ key: item.id,
251
+ paddingX: 1,
252
+ gap: 2,
253
+ },
254
+ // Selection indicator
255
+ React.createElement(
256
+ Text,
257
+ { color: isSelected ? tokens.colors.accent : tokens.colors.fg },
258
+ isSelected ? "›" : " "
259
+ ),
260
+ // Icon
261
+ item.icon
262
+ ? React.createElement(Text, { color: tokens.colors.muted }, item.icon)
263
+ : null,
264
+ // Label and description
265
+ React.createElement(
266
+ Box,
267
+ { flexGrow: 1, gap: 1 },
268
+ React.createElement(
269
+ Text,
270
+ {
271
+ color: isDisabled
272
+ ? tokens.colors.disabled
273
+ : isSelected
274
+ ? tokens.colors.accent
275
+ : tokens.colors.fg,
276
+ bold: isSelected,
277
+ inverse: isSelected && isFocused,
278
+ },
279
+ item.label
280
+ ),
281
+ item.description
282
+ ? React.createElement(
283
+ Text,
284
+ { color: tokens.colors.muted, dimColor: true },
285
+ item.description
286
+ )
287
+ : null
288
+ ),
289
+ // Shortcut
290
+ item.shortcut
291
+ ? React.createElement(
292
+ Text,
293
+ {
294
+ backgroundColor: tokens.colors.border,
295
+ color: tokens.colors.fg,
296
+ },
297
+ " ",
298
+ item.shortcut,
299
+ " "
300
+ )
301
+ : null
302
+ );
303
+ }
304
+ }
@@ -0,0 +1,2 @@
1
+ export { Command } from "./command.js";
2
+ export type { CommandProps, CommandItem } from "./command.js";
@@ -0,0 +1,140 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens } from "@hauktui/primitives-ink";
5
+
6
+ export interface ConfirmDialogProps {
7
+ /** Dialog title */
8
+ title?: string;
9
+ /** Dialog message */
10
+ message: string;
11
+ /** Callback when confirmed */
12
+ onConfirm: () => void;
13
+ /** Callback when cancelled */
14
+ onCancel: () => void;
15
+ /** Confirm button label */
16
+ confirmLabel?: string;
17
+ /** Cancel button label */
18
+ cancelLabel?: string;
19
+ /** Whether confirm is destructive (shows in danger color) */
20
+ destructive?: boolean;
21
+ /** Custom tokens override */
22
+ tokens?: Tokens;
23
+ /** Whether to show the dialog */
24
+ isOpen?: boolean;
25
+ }
26
+
27
+ export function ConfirmDialog({
28
+ title,
29
+ message,
30
+ onConfirm,
31
+ onCancel,
32
+ confirmLabel = "Confirm",
33
+ cancelLabel = "Cancel",
34
+ destructive = false,
35
+ tokens: propTokens,
36
+ isOpen = true,
37
+ }: ConfirmDialogProps): React.ReactElement | null {
38
+ const contextTokens = useTokens();
39
+ const tokens = propTokens ?? contextTokens;
40
+ const [selectedIndex, setSelectedIndex] = useState(0);
41
+
42
+ useInput(
43
+ (input, key) => {
44
+ if (!isOpen) return;
45
+
46
+ if (key.leftArrow || key.rightArrow || key.tab) {
47
+ setSelectedIndex((prev) => (prev === 0 ? 1 : 0));
48
+ } else if (key.return) {
49
+ if (selectedIndex === 0) {
50
+ onConfirm();
51
+ } else {
52
+ onCancel();
53
+ }
54
+ } else if (key.escape) {
55
+ onCancel();
56
+ } else if (input === "y" || input === "Y") {
57
+ onConfirm();
58
+ } else if (input === "n" || input === "N") {
59
+ onCancel();
60
+ }
61
+ },
62
+ { isActive: isOpen }
63
+ );
64
+
65
+ if (!isOpen) return null;
66
+
67
+ const confirmColor = destructive ? tokens.colors.danger : tokens.colors.accent;
68
+
69
+ return React.createElement(
70
+ Box,
71
+ {
72
+ flexDirection: "column",
73
+ borderStyle: "round",
74
+ borderColor: tokens.colors.border,
75
+ paddingX: 2,
76
+ paddingY: 1,
77
+ },
78
+ title
79
+ ? React.createElement(
80
+ Box,
81
+ { marginBottom: 1 },
82
+ React.createElement(
83
+ Text,
84
+ { bold: true, color: tokens.colors.fg },
85
+ title
86
+ )
87
+ )
88
+ : null,
89
+ React.createElement(
90
+ Box,
91
+ { marginBottom: 1 },
92
+ React.createElement(Text, { color: tokens.colors.muted }, message)
93
+ ),
94
+ React.createElement(
95
+ Box,
96
+ { gap: 2 },
97
+ React.createElement(
98
+ Box,
99
+ {
100
+ borderStyle: selectedIndex === 0 ? "round" : undefined,
101
+ borderColor: selectedIndex === 0 ? confirmColor : undefined,
102
+ paddingX: 1,
103
+ },
104
+ React.createElement(
105
+ Text,
106
+ {
107
+ color: confirmColor,
108
+ bold: selectedIndex === 0,
109
+ },
110
+ confirmLabel
111
+ )
112
+ ),
113
+ React.createElement(
114
+ Box,
115
+ {
116
+ borderStyle: selectedIndex === 1 ? "round" : undefined,
117
+ borderColor: selectedIndex === 1 ? tokens.colors.focus : undefined,
118
+ paddingX: 1,
119
+ },
120
+ React.createElement(
121
+ Text,
122
+ {
123
+ color: tokens.colors.muted,
124
+ bold: selectedIndex === 1,
125
+ },
126
+ cancelLabel
127
+ )
128
+ )
129
+ ),
130
+ React.createElement(
131
+ Box,
132
+ { marginTop: 1 },
133
+ React.createElement(
134
+ Text,
135
+ { color: tokens.colors.muted, dimColor: true },
136
+ "Press Y/N or Enter to select"
137
+ )
138
+ )
139
+ );
140
+ }
@@ -0,0 +1,2 @@
1
+ export { ConfirmDialog } from "./confirm-dialog.js";
2
+ export type { ConfirmDialogProps } from "./confirm-dialog.js";
@@ -0,0 +1,188 @@
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 ContextMenuItem {
8
+ /** Unique identifier */
9
+ id: string;
10
+ /** Display label */
11
+ label: string;
12
+ /** Optional keyboard shortcut */
13
+ shortcut?: string;
14
+ /** Whether this item is disabled */
15
+ disabled?: boolean;
16
+ /** Whether this is a separator */
17
+ separator?: boolean;
18
+ /** Optional icon/prefix */
19
+ icon?: string;
20
+ /** Danger/destructive action */
21
+ danger?: boolean;
22
+ }
23
+
24
+ export interface ContextMenuProps {
25
+ /** Menu items */
26
+ items: ContextMenuItem[];
27
+ /** Whether the menu is open */
28
+ open: boolean;
29
+ /** Callback when menu should close */
30
+ onClose: () => void;
31
+ /** Callback when an item is selected */
32
+ onSelect: (item: ContextMenuItem) => void;
33
+ /** Custom tokens override */
34
+ tokens?: Tokens;
35
+ /** Focus ID for focus management */
36
+ focusId?: string;
37
+ /** Menu width */
38
+ width?: number;
39
+ /** Menu title */
40
+ title?: string;
41
+ }
42
+
43
+ export function ContextMenu({
44
+ items,
45
+ open,
46
+ onClose,
47
+ onSelect,
48
+ tokens: propTokens,
49
+ focusId,
50
+ width = 25,
51
+ title,
52
+ }: ContextMenuProps): React.ReactElement | null {
53
+ const contextTokens = useTokens();
54
+ const tokens = propTokens ?? contextTokens;
55
+ const id = focusId ?? stableId("context-menu");
56
+ const { isFocused } = useFocusable(id);
57
+
58
+ const [selectedIndex, setSelectedIndex] = useState(0);
59
+
60
+ const selectableItems = items.filter(
61
+ (item) => !item.separator && !item.disabled
62
+ );
63
+
64
+ const handleSelect = useCallback(() => {
65
+ const item = selectableItems[selectedIndex];
66
+ if (item && !item.disabled) {
67
+ onSelect(item);
68
+ onClose();
69
+ }
70
+ }, [selectableItems, selectedIndex, onSelect, onClose]);
71
+
72
+ useInput(
73
+ (input, key) => {
74
+ if (!open || !isFocused) return;
75
+
76
+ if (key.escape || input === "q") {
77
+ onClose();
78
+ return;
79
+ }
80
+
81
+ if (key.upArrow || input === "k") {
82
+ setSelectedIndex((prev) =>
83
+ clamp(prev - 1, 0, selectableItems.length - 1)
84
+ );
85
+ } else if (key.downArrow || input === "j") {
86
+ setSelectedIndex((prev) =>
87
+ clamp(prev + 1, 0, selectableItems.length - 1)
88
+ );
89
+ } else if (key.return) {
90
+ handleSelect();
91
+ }
92
+ },
93
+ { isActive: open && isFocused }
94
+ );
95
+
96
+ if (!open) return null;
97
+
98
+ const menuItems = items.map((item, index) => {
99
+ if (item.separator) {
100
+ return React.createElement(
101
+ Box,
102
+ { key: `sep-${index}`, paddingX: 1 },
103
+ React.createElement(
104
+ Text,
105
+ { color: tokens.colors.border },
106
+ "─".repeat(width - 4)
107
+ )
108
+ );
109
+ }
110
+
111
+ const isSelected = selectableItems[selectedIndex]?.id === item.id;
112
+
113
+ return React.createElement(
114
+ Box,
115
+ {
116
+ key: item.id,
117
+ paddingX: 1,
118
+ gap: 1,
119
+ },
120
+ React.createElement(
121
+ Text,
122
+ { color: isSelected ? tokens.colors.accent : tokens.colors.muted },
123
+ isSelected ? "▸" : " "
124
+ ),
125
+ item.icon ? React.createElement(Text, null, item.icon) : null,
126
+ React.createElement(
127
+ Text,
128
+ {
129
+ color: item.disabled
130
+ ? tokens.colors.muted
131
+ : item.danger
132
+ ? tokens.colors.danger
133
+ : isSelected
134
+ ? tokens.colors.fg
135
+ : tokens.colors.fg,
136
+ dimColor: item.disabled,
137
+ bold: isSelected,
138
+ },
139
+ item.label
140
+ ),
141
+ item.shortcut
142
+ ? React.createElement(
143
+ Box,
144
+ { flexGrow: 1, justifyContent: "flex-end" },
145
+ React.createElement(
146
+ Text,
147
+ { color: tokens.colors.muted, dimColor: true },
148
+ item.shortcut
149
+ )
150
+ )
151
+ : null
152
+ );
153
+ });
154
+
155
+ return React.createElement(
156
+ Box,
157
+ {
158
+ flexDirection: "column",
159
+ borderStyle: "round",
160
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
161
+ width,
162
+ paddingY: 0,
163
+ },
164
+ title
165
+ ? React.createElement(
166
+ Box,
167
+ { paddingX: 1, marginBottom: 0 },
168
+ React.createElement(
169
+ Text,
170
+ { bold: true, color: tokens.colors.muted },
171
+ title
172
+ )
173
+ )
174
+ : null,
175
+ title
176
+ ? React.createElement(
177
+ Box,
178
+ { paddingX: 1 },
179
+ React.createElement(
180
+ Text,
181
+ { color: tokens.colors.border },
182
+ "─".repeat(width - 4)
183
+ )
184
+ )
185
+ : null,
186
+ ...menuItems
187
+ );
188
+ }
@@ -0,0 +1,2 @@
1
+ export { ContextMenu } from "./context-menu.js";
2
+ export type { ContextMenuProps, ContextMenuItem } from "./context-menu.js";