@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,167 @@
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 RadioOption<T = string> {
8
+ /** Display label */
9
+ label: string;
10
+ /** Option value */
11
+ value: T;
12
+ /** Whether this option is disabled */
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export interface RadioGroupProps<T = string> {
17
+ /** Available options */
18
+ options: RadioOption<T>[];
19
+ /** Controlled value */
20
+ value?: T;
21
+ /** Default value for uncontrolled mode */
22
+ defaultValue?: T;
23
+ /** Callback when selection changes */
24
+ onChange?: (value: T) => void;
25
+ /** Group label */
26
+ label?: string;
27
+ /** Whether the entire group is disabled */
28
+ disabled?: boolean;
29
+ /** Layout direction */
30
+ direction?: "vertical" | "horizontal";
31
+ /** Custom tokens override */
32
+ tokens?: Tokens;
33
+ /** Focus ID for focus management */
34
+ focusId?: string;
35
+ }
36
+
37
+ export function RadioGroup<T = string>({
38
+ options,
39
+ value: controlledValue,
40
+ defaultValue,
41
+ onChange,
42
+ label,
43
+ disabled = false,
44
+ direction = "vertical",
45
+ tokens: propTokens,
46
+ focusId,
47
+ }: RadioGroupProps<T>): React.ReactElement {
48
+ const contextTokens = useTokens();
49
+ const tokens = propTokens ?? contextTokens;
50
+ const id = focusId ?? stableId("radio-group");
51
+ const { isFocused } = useFocusable(id);
52
+
53
+ const [internalValue, setInternalValue] = useState<T | undefined>(defaultValue);
54
+ const [highlightedIndex, setHighlightedIndex] = useState(() => {
55
+ const currentValue = controlledValue ?? defaultValue;
56
+ if (currentValue !== undefined) {
57
+ const idx = options.findIndex((o) => o.value === currentValue);
58
+ return idx >= 0 ? idx : 0;
59
+ }
60
+ return 0;
61
+ });
62
+
63
+ const isControlled = controlledValue !== undefined;
64
+ const currentValue = isControlled ? controlledValue : internalValue;
65
+
66
+ const selectOption = useCallback(
67
+ (index: number) => {
68
+ const option = options[index];
69
+ if (!option || option.disabled || disabled) return;
70
+
71
+ if (!isControlled) {
72
+ setInternalValue(option.value);
73
+ }
74
+ onChange?.(option.value);
75
+ },
76
+ [options, isControlled, disabled, onChange]
77
+ );
78
+
79
+ useInput(
80
+ (input, key) => {
81
+ if (!isFocused || disabled) return;
82
+
83
+ const isVertical = direction === "vertical";
84
+ const prevKey = isVertical ? key.upArrow : key.leftArrow;
85
+ const nextKey = isVertical ? key.downArrow : key.rightArrow;
86
+
87
+ if (prevKey) {
88
+ setHighlightedIndex((prev) => {
89
+ let next = prev - 1;
90
+ while (next >= 0 && options[next]?.disabled) {
91
+ next--;
92
+ }
93
+ return clamp(next, 0, options.length - 1);
94
+ });
95
+ } else if (nextKey) {
96
+ setHighlightedIndex((prev) => {
97
+ let next = prev + 1;
98
+ while (next < options.length && options[next]?.disabled) {
99
+ next++;
100
+ }
101
+ return clamp(next, 0, options.length - 1);
102
+ });
103
+ } else if (key.return || input === " ") {
104
+ selectOption(highlightedIndex);
105
+ }
106
+ },
107
+ { isActive: isFocused }
108
+ );
109
+
110
+ return React.createElement(
111
+ Box,
112
+ {
113
+ flexDirection: "column",
114
+ borderStyle: isFocused ? "round" : undefined,
115
+ borderColor: isFocused ? tokens.colors.focus : undefined,
116
+ paddingX: isFocused ? 1 : 0,
117
+ },
118
+ label
119
+ ? React.createElement(
120
+ Box,
121
+ { marginBottom: 1 },
122
+ React.createElement(
123
+ Text,
124
+ { bold: true, color: tokens.colors.fg },
125
+ label
126
+ )
127
+ )
128
+ : null,
129
+ React.createElement(
130
+ Box,
131
+ {
132
+ flexDirection: direction === "vertical" ? "column" : "row",
133
+ gap: direction === "vertical" ? 0 : 2,
134
+ },
135
+ options.map((option, index) => {
136
+ const isSelected = option.value === currentValue;
137
+ const isHighlighted = index === highlightedIndex;
138
+ const isDisabled = option.disabled ?? disabled;
139
+
140
+ let color = tokens.colors.fg;
141
+ if (isDisabled) {
142
+ color = tokens.colors.disabled;
143
+ } else if (isHighlighted && isFocused) {
144
+ color = tokens.colors.accent;
145
+ }
146
+
147
+ const indicator = isSelected ? "●" : "○";
148
+
149
+ return React.createElement(
150
+ Box,
151
+ { key: index },
152
+ React.createElement(
153
+ Text,
154
+ {
155
+ color: isSelected ? tokens.colors.accent : color,
156
+ bold: isHighlighted && isFocused,
157
+ dimColor: isDisabled,
158
+ },
159
+ indicator,
160
+ " ",
161
+ option.label
162
+ )
163
+ );
164
+ })
165
+ )
166
+ );
167
+ }
@@ -0,0 +1,2 @@
1
+ export { Resizable } from "./resizable.js";
2
+ export type { ResizableProps } from "./resizable.js";
@@ -0,0 +1,141 @@
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 ResizableProps {
8
+ /** Child panels */
9
+ children: React.ReactNode[];
10
+ /** Direction of resize */
11
+ direction?: "horizontal" | "vertical";
12
+ /** Initial sizes (percentages, should sum to 100) */
13
+ defaultSizes?: number[];
14
+ /** Minimum size for each panel */
15
+ minSize?: number;
16
+ /** Custom tokens override */
17
+ tokens?: Tokens;
18
+ /** Focus ID for focus management */
19
+ focusId?: string;
20
+ }
21
+
22
+ export function Resizable({
23
+ children,
24
+ direction = "horizontal",
25
+ defaultSizes,
26
+ minSize = 10,
27
+ tokens: propTokens,
28
+ focusId,
29
+ }: ResizableProps): React.ReactElement {
30
+ const contextTokens = useTokens();
31
+ const tokens = propTokens ?? contextTokens;
32
+ const id = focusId ?? stableId("resizable");
33
+ const { isFocused } = useFocusable(id);
34
+
35
+ const panelCount = React.Children.count(children);
36
+ const initialSizes = defaultSizes || Array(panelCount).fill(100 / panelCount);
37
+ const [sizes, setSizes] = useState<number[]>(initialSizes);
38
+ const [activeHandle, setActiveHandle] = useState(0);
39
+
40
+ useInput(
41
+ (input, key) => {
42
+ if (!isFocused) return;
43
+
44
+ // Switch between handles
45
+ if (key.tab) {
46
+ setActiveHandle((prev) => (prev + 1) % (panelCount - 1));
47
+ return;
48
+ }
49
+
50
+ const delta = 5;
51
+ const newSizes = [...sizes];
52
+
53
+ if (direction === "horizontal") {
54
+ if (key.leftArrow || input === "h") {
55
+ if (newSizes[activeHandle]! > minSize) {
56
+ newSizes[activeHandle] = newSizes[activeHandle]! - delta;
57
+ newSizes[activeHandle + 1] = newSizes[activeHandle + 1]! + delta;
58
+ }
59
+ } else if (key.rightArrow || input === "l") {
60
+ if (newSizes[activeHandle + 1]! > minSize) {
61
+ newSizes[activeHandle] = newSizes[activeHandle]! + delta;
62
+ newSizes[activeHandle + 1] = newSizes[activeHandle + 1]! - delta;
63
+ }
64
+ }
65
+ } else {
66
+ if (key.upArrow || input === "k") {
67
+ if (newSizes[activeHandle]! > minSize) {
68
+ newSizes[activeHandle] = newSizes[activeHandle]! - delta;
69
+ newSizes[activeHandle + 1] = newSizes[activeHandle + 1]! + delta;
70
+ }
71
+ } else if (key.downArrow || input === "j") {
72
+ if (newSizes[activeHandle + 1]! > minSize) {
73
+ newSizes[activeHandle] = newSizes[activeHandle]! + delta;
74
+ newSizes[activeHandle + 1] = newSizes[activeHandle + 1]! - delta;
75
+ }
76
+ }
77
+ }
78
+
79
+ setSizes(newSizes);
80
+ },
81
+ { isActive: isFocused }
82
+ );
83
+
84
+ const panels: React.ReactElement[] = [];
85
+ const childArray = React.Children.toArray(children);
86
+
87
+ childArray.forEach((child, index) => {
88
+ // Add panel
89
+ panels.push(
90
+ React.createElement(
91
+ Box,
92
+ {
93
+ key: `panel-${index}`,
94
+ flexDirection: "column",
95
+ flexGrow: sizes[index],
96
+ flexBasis: 0,
97
+ borderStyle: "single",
98
+ borderColor: tokens.colors.border,
99
+ },
100
+ child
101
+ )
102
+ );
103
+
104
+ // Add resize handle between panels
105
+ if (index < childArray.length - 1) {
106
+ const isActive = activeHandle === index;
107
+ panels.push(
108
+ React.createElement(
109
+ Box,
110
+ {
111
+ key: `handle-${index}`,
112
+ flexDirection: direction === "horizontal" ? "column" : "row",
113
+ alignItems: "center",
114
+ justifyContent: "center",
115
+ },
116
+ React.createElement(
117
+ Text,
118
+ {
119
+ color:
120
+ isActive && isFocused
121
+ ? tokens.colors.accent
122
+ : tokens.colors.muted,
123
+ bold: isActive && isFocused,
124
+ },
125
+ direction === "horizontal" ? "┃" : "━"
126
+ )
127
+ )
128
+ );
129
+ }
130
+ });
131
+
132
+ return React.createElement(
133
+ Box,
134
+ {
135
+ flexDirection: direction === "horizontal" ? "row" : "column",
136
+ borderStyle: isFocused ? "double" : "single",
137
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
138
+ },
139
+ ...panels
140
+ );
141
+ }
@@ -0,0 +1,2 @@
1
+ export { ScrollArea } from "./scroll-area.js";
2
+ export type { ScrollAreaProps } from "./scroll-area.js";
@@ -0,0 +1,133 @@
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 ScrollAreaProps {
8
+ /** Content to scroll */
9
+ children: React.ReactNode;
10
+ /** Maximum visible height (in lines) */
11
+ height: number;
12
+ /** Whether to show scrollbar */
13
+ showScrollbar?: boolean;
14
+ /** Whether the scroll area is disabled */
15
+ disabled?: boolean;
16
+ /** Custom tokens override */
17
+ tokens?: Tokens;
18
+ /** Focus ID for focus management */
19
+ focusId?: string;
20
+ /** Scroll step (lines per scroll) */
21
+ scrollStep?: number;
22
+ }
23
+
24
+ export function ScrollArea({
25
+ children,
26
+ height,
27
+ showScrollbar = true,
28
+ disabled = false,
29
+ tokens: propTokens,
30
+ focusId,
31
+ scrollStep = 1,
32
+ }: ScrollAreaProps): React.ReactElement {
33
+ const contextTokens = useTokens();
34
+ const tokens = propTokens ?? contextTokens;
35
+ const id = focusId ?? stableId("scroll-area");
36
+ const { isFocused } = useFocusable(id);
37
+
38
+ const [scrollOffset, setScrollOffset] = useState(0);
39
+
40
+ // Convert children to array of lines for rendering
41
+ const childArray = React.Children.toArray(children);
42
+ const totalLines = childArray.length;
43
+ const maxScroll = Math.max(0, totalLines - height);
44
+
45
+ const scroll = useCallback(
46
+ (delta: number) => {
47
+ setScrollOffset((prev) => clamp(prev + delta, 0, maxScroll));
48
+ },
49
+ [maxScroll]
50
+ );
51
+
52
+ useInput(
53
+ (input, key) => {
54
+ if (!isFocused || disabled) return;
55
+
56
+ if (key.upArrow || input === "k") {
57
+ scroll(-scrollStep);
58
+ } else if (key.downArrow || input === "j") {
59
+ scroll(scrollStep);
60
+ } else if (key.pageUp || (key.ctrl && input === "u")) {
61
+ scroll(-height);
62
+ } else if (key.pageDown || (key.ctrl && input === "d")) {
63
+ scroll(height);
64
+ } else if (input === "g") {
65
+ setScrollOffset(0);
66
+ } else if (input === "G") {
67
+ setScrollOffset(maxScroll);
68
+ }
69
+ },
70
+ { isActive: isFocused }
71
+ );
72
+
73
+ // Get visible content
74
+ const visibleChildren = childArray.slice(scrollOffset, scrollOffset + height);
75
+
76
+ // Render scrollbar
77
+ const renderScrollbar = () => {
78
+ if (!showScrollbar || totalLines <= height) return null;
79
+
80
+ const scrollbarHeight = height;
81
+ const thumbHeight = Math.max(
82
+ 1,
83
+ Math.floor((height / totalLines) * scrollbarHeight)
84
+ );
85
+ const thumbPosition = Math.floor(
86
+ (scrollOffset / maxScroll) * (scrollbarHeight - thumbHeight)
87
+ );
88
+
89
+ const scrollbar: React.ReactElement[] = [];
90
+ for (let i = 0; i < scrollbarHeight; i++) {
91
+ const isThumb = i >= thumbPosition && i < thumbPosition + thumbHeight;
92
+ scrollbar.push(
93
+ React.createElement(
94
+ Text,
95
+ {
96
+ key: i,
97
+ color: isThumb ? tokens.colors.accent : tokens.colors.border,
98
+ },
99
+ isThumb ? "█" : "░"
100
+ )
101
+ );
102
+ }
103
+
104
+ return React.createElement(
105
+ Box,
106
+ { flexDirection: "column", marginLeft: 1 },
107
+ scrollbar
108
+ );
109
+ };
110
+
111
+ return React.createElement(
112
+ Box,
113
+ {
114
+ flexDirection: "row",
115
+ borderStyle: isFocused ? "round" : "single",
116
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
117
+ },
118
+ // Content area
119
+ React.createElement(
120
+ Box,
121
+ {
122
+ flexDirection: "column",
123
+ flexGrow: 1,
124
+ paddingX: 1,
125
+ height,
126
+ overflow: "hidden",
127
+ },
128
+ ...visibleChildren
129
+ ),
130
+ // Scrollbar
131
+ renderScrollbar()
132
+ );
133
+ }
@@ -0,0 +1,2 @@
1
+ export { Select } from "./select.js";
2
+ export type { SelectProps, SelectOption } from "./select.js";
@@ -0,0 +1,185 @@
1
+ import React, { useState, useCallback, useEffect } 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 SelectOption<T = string> {
8
+ /** Display label */
9
+ label: string;
10
+ /** Option value */
11
+ value: T;
12
+ /** Whether this option is disabled */
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export interface SelectProps<T = string> {
17
+ /** Available options */
18
+ options: SelectOption<T>[];
19
+ /** Controlled value */
20
+ value?: T;
21
+ /** Default value for uncontrolled mode */
22
+ defaultValue?: T;
23
+ /** Callback when selection changes */
24
+ onChange?: (value: T) => void;
25
+ /** Placeholder when no selection */
26
+ placeholder?: string;
27
+ /** Whether the select is disabled */
28
+ disabled?: boolean;
29
+ /** Custom tokens override */
30
+ tokens?: Tokens;
31
+ /** Focus ID for focus management */
32
+ focusId?: string;
33
+ /** Show options in expanded list view */
34
+ expanded?: boolean;
35
+ }
36
+
37
+ export function Select<T = string>({
38
+ options,
39
+ value: controlledValue,
40
+ defaultValue,
41
+ onChange,
42
+ placeholder = "Select an option",
43
+ disabled = false,
44
+ tokens: propTokens,
45
+ focusId,
46
+ expanded = false,
47
+ }: SelectProps<T>): React.ReactElement {
48
+ const contextTokens = useTokens();
49
+ const tokens = propTokens ?? contextTokens;
50
+ const id = focusId ?? stableId("select");
51
+ const { isFocused } = useFocusable(id);
52
+
53
+ // Internal state for uncontrolled mode
54
+ const [internalValue, setInternalValue] = useState<T | undefined>(defaultValue);
55
+ const isControlled = controlledValue !== undefined;
56
+ const currentValue = isControlled ? controlledValue : internalValue;
57
+
58
+ // Highlighted index for keyboard navigation
59
+ const [highlightedIndex, setHighlightedIndex] = useState(() => {
60
+ if (currentValue !== undefined) {
61
+ const idx = options.findIndex((o) => o.value === currentValue);
62
+ return idx >= 0 ? idx : 0;
63
+ }
64
+ return 0;
65
+ });
66
+
67
+ // Update highlighted index when value changes externally
68
+ useEffect(() => {
69
+ if (currentValue !== undefined) {
70
+ const idx = options.findIndex((o) => o.value === currentValue);
71
+ if (idx >= 0) setHighlightedIndex(idx);
72
+ }
73
+ }, [currentValue, options]);
74
+
75
+ const selectOption = useCallback(
76
+ (index: number) => {
77
+ const option = options[index];
78
+ if (!option || option.disabled) return;
79
+
80
+ if (!isControlled) {
81
+ setInternalValue(option.value);
82
+ }
83
+ onChange?.(option.value);
84
+ },
85
+ [options, isControlled, onChange]
86
+ );
87
+
88
+ // Handle keyboard navigation
89
+ useInput(
90
+ (input, key) => {
91
+ if (!isFocused || disabled) return;
92
+
93
+ if (key.upArrow) {
94
+ setHighlightedIndex((prev) => {
95
+ let next = prev - 1;
96
+ // Skip disabled options
97
+ while (next >= 0 && options[next]?.disabled) {
98
+ next--;
99
+ }
100
+ return clamp(next, 0, options.length - 1);
101
+ });
102
+ } else if (key.downArrow) {
103
+ setHighlightedIndex((prev) => {
104
+ let next = prev + 1;
105
+ // Skip disabled options
106
+ while (next < options.length && options[next]?.disabled) {
107
+ next++;
108
+ }
109
+ return clamp(next, 0, options.length - 1);
110
+ });
111
+ } else if (key.return || input === " ") {
112
+ selectOption(highlightedIndex);
113
+ }
114
+ },
115
+ { isActive: isFocused }
116
+ );
117
+
118
+ // Find the selected option
119
+ const selectedOption = options.find((o) => o.value === currentValue);
120
+
121
+ // Render collapsed view (single line)
122
+ if (!expanded) {
123
+ return React.createElement(
124
+ Box,
125
+ {
126
+ borderStyle: isFocused ? "round" : "single",
127
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
128
+ paddingX: 1,
129
+ },
130
+ React.createElement(
131
+ Text,
132
+ {
133
+ color: selectedOption
134
+ ? tokens.colors.fg
135
+ : tokens.colors.muted,
136
+ },
137
+ selectedOption?.label ?? placeholder
138
+ ),
139
+ React.createElement(
140
+ Text,
141
+ { color: tokens.colors.muted },
142
+ " ▼"
143
+ )
144
+ );
145
+ }
146
+
147
+ // Render expanded view (list)
148
+ return React.createElement(
149
+ Box,
150
+ {
151
+ flexDirection: "column",
152
+ borderStyle: isFocused ? "round" : "single",
153
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
154
+ },
155
+ options.map((option, index) => {
156
+ const isSelected = option.value === currentValue;
157
+ const isHighlighted = index === highlightedIndex;
158
+ const isDisabled = option.disabled;
159
+
160
+ let color = tokens.colors.fg;
161
+ if (isDisabled) {
162
+ color = tokens.colors.disabled;
163
+ } else if (isHighlighted) {
164
+ color = tokens.colors.accent;
165
+ }
166
+
167
+ const prefix = isSelected ? "● " : "○ ";
168
+
169
+ return React.createElement(
170
+ Box,
171
+ { key: index, paddingX: 1 },
172
+ React.createElement(
173
+ Text,
174
+ {
175
+ color,
176
+ bold: isHighlighted,
177
+ dimColor: isDisabled,
178
+ },
179
+ prefix,
180
+ option.label
181
+ )
182
+ );
183
+ })
184
+ );
185
+ }
@@ -0,0 +1,2 @@
1
+ export { Separator } from "./separator.js";
2
+ export type { SeparatorProps } from "./separator.js";
@@ -0,0 +1,63 @@
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 SeparatorProps {
7
+ /** Separator width in characters (or "full" for full width) */
8
+ width?: number | "full";
9
+ /** Separator character */
10
+ char?: string;
11
+ /** Optional label to show in the middle */
12
+ label?: string;
13
+ /** Custom tokens override */
14
+ tokens?: Tokens;
15
+ /** Orientation */
16
+ orientation?: "horizontal" | "vertical";
17
+ }
18
+
19
+ export function Separator({
20
+ width = 40,
21
+ char = "─",
22
+ label,
23
+ tokens: propTokens,
24
+ orientation = "horizontal",
25
+ }: SeparatorProps): React.ReactElement {
26
+ const contextTokens = useTokens();
27
+ const tokens = propTokens ?? contextTokens;
28
+
29
+ if (orientation === "vertical") {
30
+ return React.createElement(Text, { color: tokens.colors.border }, "│");
31
+ }
32
+
33
+ const actualWidth = width === "full" ? 80 : width;
34
+
35
+ if (label) {
36
+ const labelLength = label.length + 2; // padding around label
37
+ const sideWidth = Math.floor((actualWidth - labelLength) / 2);
38
+ const leftLine = char.repeat(Math.max(0, sideWidth));
39
+ const rightLine = char.repeat(
40
+ Math.max(0, actualWidth - sideWidth - labelLength)
41
+ );
42
+
43
+ return React.createElement(
44
+ Box,
45
+ null,
46
+ React.createElement(Text, { color: tokens.colors.border }, leftLine),
47
+ React.createElement(
48
+ Text,
49
+ { color: tokens.colors.muted },
50
+ " ",
51
+ label,
52
+ " "
53
+ ),
54
+ React.createElement(Text, { color: tokens.colors.border }, rightLine)
55
+ );
56
+ }
57
+
58
+ return React.createElement(
59
+ Text,
60
+ { color: tokens.colors.border },
61
+ char.repeat(actualWidth)
62
+ );
63
+ }
@@ -0,0 +1,2 @@
1
+ export { Sheet } from "./sheet.js";
2
+ export type { SheetProps, SheetSide } from "./sheet.js";