@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,247 @@
1
+ import React, { useState, useCallback, useEffect, 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 ListItem<T = string> {
8
+ /** Display label */
9
+ label: string;
10
+ /** Item value */
11
+ value: T;
12
+ /** Secondary text/description */
13
+ description?: string;
14
+ /** Whether this item is disabled */
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export interface ListProps<T = string> {
19
+ /** List items */
20
+ items: ListItem<T>[];
21
+ /** Controlled selected value */
22
+ value?: T;
23
+ /** Default value for uncontrolled mode */
24
+ defaultValue?: T;
25
+ /** Callback when selection changes */
26
+ onChange?: (value: T) => void;
27
+ /** Callback when Enter is pressed on selected item */
28
+ onSelect?: (value: T) => void;
29
+ /** Whether the list is disabled */
30
+ disabled?: boolean;
31
+ /** Custom tokens override */
32
+ tokens?: Tokens;
33
+ /** Focus ID for focus management */
34
+ focusId?: string;
35
+ /** Maximum visible items before scrolling */
36
+ maxHeight?: number;
37
+ /** Show item descriptions */
38
+ showDescriptions?: boolean;
39
+ /** Show scroll indicators */
40
+ showScrollIndicators?: boolean;
41
+ }
42
+
43
+ export function List<T = string>({
44
+ items,
45
+ value: controlledValue,
46
+ defaultValue,
47
+ onChange,
48
+ onSelect,
49
+ disabled = false,
50
+ tokens: propTokens,
51
+ focusId,
52
+ maxHeight = 10,
53
+ showDescriptions = false,
54
+ showScrollIndicators = true,
55
+ }: ListProps<T>): React.ReactElement {
56
+ const contextTokens = useTokens();
57
+ const tokens = propTokens ?? contextTokens;
58
+ const id = focusId ?? stableId("list");
59
+ const { isFocused } = useFocusable(id);
60
+
61
+ // Internal state for uncontrolled mode
62
+ const [internalValue, setInternalValue] = useState<T | undefined>(defaultValue);
63
+ const isControlled = controlledValue !== undefined;
64
+ const currentValue = isControlled ? controlledValue : internalValue;
65
+
66
+ // Highlighted index for keyboard navigation
67
+ const [highlightedIndex, setHighlightedIndex] = useState(() => {
68
+ if (currentValue !== undefined) {
69
+ const idx = items.findIndex((item) => item.value === currentValue);
70
+ return idx >= 0 ? idx : 0;
71
+ }
72
+ return 0;
73
+ });
74
+
75
+ // Scroll offset
76
+ const [scrollOffset, setScrollOffset] = useState(0);
77
+
78
+ // Update highlighted index when value changes externally
79
+ useEffect(() => {
80
+ if (currentValue !== undefined) {
81
+ const idx = items.findIndex((item) => item.value === currentValue);
82
+ if (idx >= 0) {
83
+ setHighlightedIndex(idx);
84
+ }
85
+ }
86
+ }, [currentValue, items]);
87
+
88
+ // Ensure scroll keeps highlighted item visible
89
+ useEffect(() => {
90
+ if (highlightedIndex < scrollOffset) {
91
+ setScrollOffset(highlightedIndex);
92
+ } else if (highlightedIndex >= scrollOffset + maxHeight) {
93
+ setScrollOffset(highlightedIndex - maxHeight + 1);
94
+ }
95
+ }, [highlightedIndex, scrollOffset, maxHeight]);
96
+
97
+ const selectItem = useCallback(
98
+ (index: number) => {
99
+ const item = items[index];
100
+ if (!item || item.disabled) return;
101
+
102
+ if (!isControlled) {
103
+ setInternalValue(item.value);
104
+ }
105
+ onChange?.(item.value);
106
+ },
107
+ [items, isControlled, onChange]
108
+ );
109
+
110
+ // Handle keyboard navigation
111
+ useInput(
112
+ (input, key) => {
113
+ if (!isFocused || disabled) return;
114
+
115
+ if (key.upArrow || input === "k") {
116
+ setHighlightedIndex((prev) => {
117
+ let next = prev - 1;
118
+ // Skip disabled items
119
+ while (next >= 0 && items[next]?.disabled) {
120
+ next--;
121
+ }
122
+ return clamp(next, 0, items.length - 1);
123
+ });
124
+ } else if (key.downArrow || input === "j") {
125
+ setHighlightedIndex((prev) => {
126
+ let next = prev + 1;
127
+ // Skip disabled items
128
+ while (next < items.length && items[next]?.disabled) {
129
+ next++;
130
+ }
131
+ return clamp(next, 0, items.length - 1);
132
+ });
133
+ } else if (key.return || input === " ") {
134
+ selectItem(highlightedIndex);
135
+ const item = items[highlightedIndex];
136
+ if (item && !item.disabled) {
137
+ onSelect?.(item.value);
138
+ }
139
+ } else if (input === "g") {
140
+ // Go to top
141
+ setHighlightedIndex(0);
142
+ setScrollOffset(0);
143
+ } else if (input === "G") {
144
+ // Go to bottom
145
+ setHighlightedIndex(items.length - 1);
146
+ } else if (key.pageUp) {
147
+ setHighlightedIndex((prev) => clamp(prev - maxHeight, 0, items.length - 1));
148
+ } else if (key.pageDown) {
149
+ setHighlightedIndex((prev) => clamp(prev + maxHeight, 0, items.length - 1));
150
+ }
151
+ },
152
+ { isActive: isFocused }
153
+ );
154
+
155
+ // Calculate visible items
156
+ const visibleItems = useMemo(() => {
157
+ return items.slice(scrollOffset, scrollOffset + maxHeight);
158
+ }, [items, scrollOffset, maxHeight]);
159
+
160
+ // Check if we need scroll indicators
161
+ const canScrollUp = scrollOffset > 0;
162
+ const canScrollDown = scrollOffset + maxHeight < items.length;
163
+
164
+ return React.createElement(
165
+ Box,
166
+ {
167
+ flexDirection: "column",
168
+ borderStyle: isFocused ? "round" : "single",
169
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
170
+ },
171
+ // Scroll up indicator
172
+ showScrollIndicators && canScrollUp
173
+ ? React.createElement(
174
+ Box,
175
+ { justifyContent: "center" },
176
+ React.createElement(Text, { color: tokens.colors.muted }, "▲ more")
177
+ )
178
+ : null,
179
+ // List items
180
+ visibleItems.map((item, visibleIndex) => {
181
+ const actualIndex = scrollOffset + visibleIndex;
182
+ const isSelected = item.value === currentValue;
183
+ const isHighlighted = actualIndex === highlightedIndex;
184
+ const isDisabled = item.disabled;
185
+
186
+ let color = tokens.colors.fg;
187
+ if (isDisabled) {
188
+ color = tokens.colors.disabled;
189
+ } else if (isHighlighted) {
190
+ color = tokens.colors.accent;
191
+ }
192
+
193
+ const prefix = isSelected ? "● " : " ";
194
+ const highlight = isHighlighted && isFocused;
195
+
196
+ return React.createElement(
197
+ Box,
198
+ {
199
+ key: actualIndex,
200
+ flexDirection: "column",
201
+ paddingX: 1,
202
+ },
203
+ React.createElement(
204
+ Box,
205
+ null,
206
+ React.createElement(
207
+ Text,
208
+ {
209
+ color,
210
+ bold: highlight,
211
+ dimColor: isDisabled,
212
+ inverse: highlight,
213
+ },
214
+ prefix,
215
+ item.label
216
+ )
217
+ ),
218
+ showDescriptions && item.description
219
+ ? React.createElement(
220
+ Text,
221
+ { color: tokens.colors.muted, dimColor: true },
222
+ " ",
223
+ item.description
224
+ )
225
+ : null
226
+ );
227
+ }),
228
+ // Scroll down indicator
229
+ showScrollIndicators && canScrollDown
230
+ ? React.createElement(
231
+ Box,
232
+ { justifyContent: "center" },
233
+ React.createElement(Text, { color: tokens.colors.muted }, "▼ more")
234
+ )
235
+ : null,
236
+ // Item count indicator
237
+ React.createElement(
238
+ Box,
239
+ { justifyContent: "flex-end", paddingX: 1 },
240
+ React.createElement(
241
+ Text,
242
+ { color: tokens.colors.muted, dimColor: true },
243
+ `${highlightedIndex + 1}/${items.length}`
244
+ )
245
+ )
246
+ );
247
+ }
@@ -0,0 +1,2 @@
1
+ export { Menubar } from "./menubar.js";
2
+ export type { MenubarProps, MenubarItem, MenubarSubItem } from "./menubar.js";
@@ -0,0 +1,220 @@
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 MenubarItem {
8
+ /** Unique identifier */
9
+ id: string;
10
+ /** Display label */
11
+ label: string;
12
+ /** Optional keyboard shortcut hint */
13
+ shortcut?: string;
14
+ /** Sub-items for dropdown */
15
+ items?: MenubarSubItem[];
16
+ }
17
+
18
+ export interface MenubarSubItem {
19
+ /** Unique identifier */
20
+ id: string;
21
+ /** Display label */
22
+ label: string;
23
+ /** Optional keyboard shortcut */
24
+ shortcut?: string;
25
+ /** Whether this item is disabled */
26
+ disabled?: boolean;
27
+ /** Whether this is a separator */
28
+ separator?: boolean;
29
+ }
30
+
31
+ export interface MenubarProps {
32
+ /** Menu items */
33
+ items: MenubarItem[];
34
+ /** Callback when an item is selected */
35
+ onSelect: (menuId: string, itemId: string) => void;
36
+ /** Custom tokens override */
37
+ tokens?: Tokens;
38
+ /** Focus ID for focus management */
39
+ focusId?: string;
40
+ }
41
+
42
+ export function Menubar({
43
+ items,
44
+ onSelect,
45
+ tokens: propTokens,
46
+ focusId,
47
+ }: MenubarProps): React.ReactElement {
48
+ const contextTokens = useTokens();
49
+ const tokens = propTokens ?? contextTokens;
50
+ const id = focusId ?? stableId("menubar");
51
+ const { isFocused } = useFocusable(id);
52
+
53
+ const [activeMenu, setActiveMenu] = useState<string | null>(null);
54
+ const [menuIndex, setMenuIndex] = useState(0);
55
+ const [subIndex, setSubIndex] = useState(0);
56
+
57
+ const activeMenuItem = items.find((item) => item.id === activeMenu);
58
+ const selectableSubItems =
59
+ activeMenuItem?.items?.filter(
60
+ (item) => !item.separator && !item.disabled
61
+ ) || [];
62
+
63
+ const handleSelect = useCallback(() => {
64
+ if (activeMenu && selectableSubItems[subIndex]) {
65
+ onSelect(activeMenu, selectableSubItems[subIndex]!.id);
66
+ setActiveMenu(null);
67
+ }
68
+ }, [activeMenu, selectableSubItems, subIndex, onSelect]);
69
+
70
+ useInput(
71
+ (input, key) => {
72
+ if (!isFocused) return;
73
+
74
+ if (key.escape) {
75
+ setActiveMenu(null);
76
+ return;
77
+ }
78
+
79
+ if (!activeMenu) {
80
+ // Top-level navigation
81
+ if (key.leftArrow || input === "h") {
82
+ setMenuIndex((prev) => clamp(prev - 1, 0, items.length - 1));
83
+ } else if (key.rightArrow || input === "l") {
84
+ setMenuIndex((prev) => clamp(prev + 1, 0, items.length - 1));
85
+ } else if (key.return || key.downArrow) {
86
+ setActiveMenu(items[menuIndex]?.id || null);
87
+ setSubIndex(0);
88
+ }
89
+ } else {
90
+ // Submenu navigation
91
+ if (key.upArrow || input === "k") {
92
+ setSubIndex((prev) =>
93
+ clamp(prev - 1, 0, selectableSubItems.length - 1)
94
+ );
95
+ } else if (key.downArrow || input === "j") {
96
+ setSubIndex((prev) =>
97
+ clamp(prev + 1, 0, selectableSubItems.length - 1)
98
+ );
99
+ } else if (key.leftArrow || input === "h") {
100
+ const newIndex = clamp(menuIndex - 1, 0, items.length - 1);
101
+ setMenuIndex(newIndex);
102
+ setActiveMenu(items[newIndex]?.id || null);
103
+ setSubIndex(0);
104
+ } else if (key.rightArrow || input === "l") {
105
+ const newIndex = clamp(menuIndex + 1, 0, items.length - 1);
106
+ setMenuIndex(newIndex);
107
+ setActiveMenu(items[newIndex]?.id || null);
108
+ setSubIndex(0);
109
+ } else if (key.return) {
110
+ handleSelect();
111
+ }
112
+ }
113
+ },
114
+ { isActive: isFocused }
115
+ );
116
+
117
+ return React.createElement(
118
+ Box,
119
+ { flexDirection: "column" },
120
+ // Menu bar
121
+ React.createElement(
122
+ Box,
123
+ {
124
+ borderStyle: "single",
125
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
126
+ borderBottom: true,
127
+ borderTop: false,
128
+ borderLeft: false,
129
+ borderRight: false,
130
+ paddingX: 1,
131
+ gap: 2,
132
+ },
133
+ ...items.map((item, index) => {
134
+ const isActive = index === menuIndex;
135
+ const isOpen = activeMenu === item.id;
136
+
137
+ return React.createElement(
138
+ Text,
139
+ {
140
+ key: item.id,
141
+ color: isOpen
142
+ ? tokens.colors.accent
143
+ : isActive
144
+ ? tokens.colors.fg
145
+ : tokens.colors.muted,
146
+ bold: isOpen || isActive,
147
+ inverse: isOpen,
148
+ },
149
+ ` ${item.label} `
150
+ );
151
+ })
152
+ ),
153
+ // Dropdown
154
+ activeMenuItem?.items
155
+ ? React.createElement(
156
+ Box,
157
+ {
158
+ flexDirection: "column",
159
+ borderStyle: "round",
160
+ borderColor: tokens.colors.border,
161
+ width: 25,
162
+ marginLeft: menuIndex * 10,
163
+ },
164
+ ...activeMenuItem.items.map((item, index) => {
165
+ if (item.separator) {
166
+ return React.createElement(
167
+ Box,
168
+ { key: `sep-${index}`, paddingX: 1 },
169
+ React.createElement(
170
+ Text,
171
+ { color: tokens.colors.border },
172
+ "─".repeat(21)
173
+ )
174
+ );
175
+ }
176
+
177
+ const isSelected = selectableSubItems[subIndex]?.id === item.id;
178
+
179
+ return React.createElement(
180
+ Box,
181
+ { key: item.id, paddingX: 1, justifyContent: "space-between" },
182
+ React.createElement(
183
+ Box,
184
+ { gap: 1 },
185
+ React.createElement(
186
+ Text,
187
+ {
188
+ color: isSelected
189
+ ? tokens.colors.accent
190
+ : tokens.colors.muted,
191
+ },
192
+ isSelected ? "▸" : " "
193
+ ),
194
+ React.createElement(
195
+ Text,
196
+ {
197
+ color: item.disabled
198
+ ? tokens.colors.muted
199
+ : isSelected
200
+ ? tokens.colors.fg
201
+ : tokens.colors.fg,
202
+ dimColor: item.disabled,
203
+ bold: isSelected,
204
+ },
205
+ item.label
206
+ )
207
+ ),
208
+ item.shortcut
209
+ ? React.createElement(
210
+ Text,
211
+ { color: tokens.colors.muted, dimColor: true },
212
+ item.shortcut
213
+ )
214
+ : null
215
+ );
216
+ })
217
+ )
218
+ : null
219
+ );
220
+ }
@@ -0,0 +1,6 @@
1
+ export { NavigationMenu } from "./navigation-menu.js";
2
+ export type {
3
+ NavigationMenuProps,
4
+ NavigationMenuItem,
5
+ NavigationMenuChild,
6
+ } from "./navigation-menu.js";
@@ -0,0 +1,216 @@
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, clamp } from "@hauktui/core";
6
+
7
+ export interface NavigationMenuItem {
8
+ /** Unique identifier */
9
+ id: string;
10
+ /** Display label */
11
+ label: string;
12
+ /** Optional href/path */
13
+ href?: string;
14
+ /** Child items for dropdown */
15
+ children?: NavigationMenuChild[];
16
+ /** Whether this item is disabled */
17
+ disabled?: boolean;
18
+ }
19
+
20
+ export interface NavigationMenuChild {
21
+ /** Unique identifier */
22
+ id: string;
23
+ /** Display label */
24
+ label: string;
25
+ /** Description text */
26
+ description?: string;
27
+ /** Optional href/path */
28
+ href?: string;
29
+ /** Whether this item is disabled */
30
+ disabled?: boolean;
31
+ }
32
+
33
+ export interface NavigationMenuProps {
34
+ /** Menu items */
35
+ items: NavigationMenuItem[];
36
+ /** Callback when an item is selected */
37
+ onSelect?: (item: NavigationMenuItem | NavigationMenuChild) => void;
38
+ /** Custom tokens override */
39
+ tokens?: Tokens;
40
+ /** Focus ID for focus management */
41
+ focusId?: string;
42
+ /** Orientation */
43
+ orientation?: "horizontal" | "vertical";
44
+ }
45
+
46
+ export function NavigationMenu({
47
+ items,
48
+ onSelect,
49
+ tokens: propTokens,
50
+ focusId,
51
+ orientation = "horizontal",
52
+ }: NavigationMenuProps): React.ReactElement {
53
+ const contextTokens = useTokens();
54
+ const tokens = propTokens ?? contextTokens;
55
+ const id = focusId ?? stableId("navigation-menu");
56
+ const { isFocused } = useFocusable(id);
57
+
58
+ const [activeIndex, setActiveIndex] = useState(0);
59
+ const [openMenu, setOpenMenu] = useState<string | null>(null);
60
+ const [childIndex, setChildIndex] = useState(0);
61
+
62
+ const activeItem = items.find((item) => item.id === openMenu);
63
+
64
+ useInput(
65
+ (input, key) => {
66
+ if (!isFocused) return;
67
+
68
+ if (key.escape) {
69
+ setOpenMenu(null);
70
+ return;
71
+ }
72
+
73
+ if (!openMenu) {
74
+ // Top level navigation
75
+ if (orientation === "horizontal") {
76
+ if (key.leftArrow || input === "h") {
77
+ setActiveIndex((prev) => clamp(prev - 1, 0, items.length - 1));
78
+ } else if (key.rightArrow || input === "l") {
79
+ setActiveIndex((prev) => clamp(prev + 1, 0, items.length - 1));
80
+ }
81
+ } else {
82
+ if (key.upArrow || input === "k") {
83
+ setActiveIndex((prev) => clamp(prev - 1, 0, items.length - 1));
84
+ } else if (key.downArrow || input === "j") {
85
+ setActiveIndex((prev) => clamp(prev + 1, 0, items.length - 1));
86
+ }
87
+ }
88
+
89
+ if (key.return || key.downArrow) {
90
+ const item = items[activeIndex];
91
+ if (item?.children && item.children.length > 0) {
92
+ setOpenMenu(item.id);
93
+ setChildIndex(0);
94
+ } else if (item) {
95
+ onSelect?.(item);
96
+ }
97
+ }
98
+ } else {
99
+ // Child navigation
100
+ const children = activeItem?.children || [];
101
+ if (key.upArrow || input === "k") {
102
+ setChildIndex((prev) => clamp(prev - 1, 0, children.length - 1));
103
+ } else if (key.downArrow || input === "j") {
104
+ setChildIndex((prev) => clamp(prev + 1, 0, children.length - 1));
105
+ } else if (key.return) {
106
+ const child = children[childIndex];
107
+ if (child && !child.disabled) {
108
+ onSelect?.(child);
109
+ setOpenMenu(null);
110
+ }
111
+ }
112
+ }
113
+ },
114
+ { isActive: isFocused }
115
+ );
116
+
117
+ const menuItems = items.map((item, index) => {
118
+ const isActive = index === activeIndex;
119
+ const isOpen = openMenu === item.id;
120
+
121
+ return React.createElement(
122
+ Text,
123
+ {
124
+ key: item.id,
125
+ color: item.disabled
126
+ ? tokens.colors.muted
127
+ : isActive || isOpen
128
+ ? tokens.colors.accent
129
+ : tokens.colors.fg,
130
+ bold: isActive || isOpen,
131
+ dimColor: item.disabled,
132
+ },
133
+ ` ${item.label} `,
134
+ item.children ? "▾" : ""
135
+ );
136
+ });
137
+
138
+ const dropdown = activeItem?.children
139
+ ? React.createElement(
140
+ Box,
141
+ {
142
+ flexDirection: "column",
143
+ borderStyle: "round",
144
+ borderColor: tokens.colors.border,
145
+ marginTop: 0,
146
+ width: 35,
147
+ paddingY: 0,
148
+ },
149
+ ...activeItem.children.map((child, index) => {
150
+ const isSelected = index === childIndex;
151
+ return React.createElement(
152
+ Box,
153
+ {
154
+ key: child.id,
155
+ flexDirection: "column",
156
+ paddingX: 1,
157
+ },
158
+ React.createElement(
159
+ Box,
160
+ { gap: 1 },
161
+ React.createElement(
162
+ Text,
163
+ {
164
+ color: isSelected
165
+ ? tokens.colors.accent
166
+ : tokens.colors.muted,
167
+ },
168
+ isSelected ? "▸" : " "
169
+ ),
170
+ React.createElement(
171
+ Text,
172
+ {
173
+ color: child.disabled
174
+ ? tokens.colors.muted
175
+ : isSelected
176
+ ? tokens.colors.fg
177
+ : tokens.colors.fg,
178
+ bold: isSelected,
179
+ dimColor: child.disabled,
180
+ },
181
+ child.label
182
+ )
183
+ ),
184
+ child.description
185
+ ? React.createElement(
186
+ Text,
187
+ { color: tokens.colors.muted, dimColor: true },
188
+ " ",
189
+ child.description
190
+ )
191
+ : null
192
+ );
193
+ })
194
+ )
195
+ : null;
196
+
197
+ return React.createElement(
198
+ Box,
199
+ { flexDirection: "column" },
200
+ React.createElement(
201
+ Box,
202
+ {
203
+ flexDirection: orientation === "horizontal" ? "row" : "column",
204
+ borderStyle: "single",
205
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
206
+ borderBottom: orientation === "horizontal",
207
+ borderRight: orientation === "vertical",
208
+ borderTop: false,
209
+ borderLeft: false,
210
+ gap: orientation === "horizontal" ? 1 : 0,
211
+ },
212
+ ...menuItems
213
+ ),
214
+ dropdown
215
+ );
216
+ }
@@ -0,0 +1,2 @@
1
+ export { Pagination } from "./pagination.js";
2
+ export type { PaginationProps } from "./pagination.js";