@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,137 @@
1
+ import React 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 type SheetSide = "left" | "right" | "top" | "bottom";
8
+
9
+ export interface SheetProps {
10
+ /** Whether the sheet is open */
11
+ open: boolean;
12
+ /** Callback when sheet should close */
13
+ onClose: () => void;
14
+ /** Content of the sheet */
15
+ children: React.ReactNode;
16
+ /** Side to open from */
17
+ side?: SheetSide;
18
+ /** Sheet title */
19
+ title?: string;
20
+ /** Sheet description */
21
+ description?: string;
22
+ /** Sheet width (for left/right) or height (for top/bottom) */
23
+ size?: number;
24
+ /** Custom tokens override */
25
+ tokens?: Tokens;
26
+ /** Focus ID for focus management */
27
+ focusId?: string;
28
+ /** Whether to show overlay effect */
29
+ overlay?: boolean;
30
+ }
31
+
32
+ export function Sheet({
33
+ open,
34
+ onClose,
35
+ children,
36
+ side = "right",
37
+ title,
38
+ description,
39
+ size = 50,
40
+ tokens: propTokens,
41
+ focusId,
42
+ overlay = true,
43
+ }: SheetProps): React.ReactElement | null {
44
+ const contextTokens = useTokens();
45
+ const tokens = propTokens ?? contextTokens;
46
+ const id = focusId ?? stableId("sheet");
47
+ const { isFocused } = useFocusable(id);
48
+
49
+ useInput(
50
+ (input, key) => {
51
+ if (!open) return;
52
+ if (key.escape || input === "q") {
53
+ onClose();
54
+ }
55
+ },
56
+ { isActive: open && isFocused }
57
+ );
58
+
59
+ if (!open) return null;
60
+
61
+ const isHorizontal = side === "left" || side === "right";
62
+
63
+ const sheetContent = React.createElement(
64
+ Box,
65
+ {
66
+ flexDirection: "column",
67
+ width: isHorizontal ? size : "100%",
68
+ height: isHorizontal ? "100%" : size,
69
+ borderStyle: "round",
70
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
71
+ backgroundColor: tokens.colors.bg,
72
+ paddingX: 2,
73
+ paddingY: 1,
74
+ },
75
+ // Header
76
+ title || description
77
+ ? React.createElement(
78
+ Box,
79
+ { flexDirection: "column", marginBottom: 1 },
80
+ title
81
+ ? React.createElement(
82
+ Text,
83
+ { bold: true, color: tokens.colors.fg },
84
+ title
85
+ )
86
+ : null,
87
+ description
88
+ ? React.createElement(
89
+ Text,
90
+ { color: tokens.colors.muted, dimColor: true },
91
+ description
92
+ )
93
+ : null
94
+ )
95
+ : null,
96
+ // Separator
97
+ title || description
98
+ ? React.createElement(
99
+ Box,
100
+ { marginBottom: 1 },
101
+ React.createElement(
102
+ Text,
103
+ { color: tokens.colors.border },
104
+ "─".repeat(size - 6)
105
+ )
106
+ )
107
+ : null,
108
+ // Content
109
+ React.createElement(
110
+ Box,
111
+ { flexDirection: "column", flexGrow: 1 },
112
+ children
113
+ ),
114
+ // Footer
115
+ React.createElement(
116
+ Box,
117
+ { marginTop: 1, justifyContent: "space-between" },
118
+ React.createElement(
119
+ Text,
120
+ { color: tokens.colors.muted, dimColor: true },
121
+ "ESC to close"
122
+ )
123
+ )
124
+ );
125
+
126
+ const containerProps: Record<string, unknown> = {
127
+ flexDirection: isHorizontal ? "row" : "column",
128
+ width: "100%",
129
+ height: "100%",
130
+ };
131
+
132
+ if (side === "right" || side === "bottom") {
133
+ containerProps.justifyContent = "flex-end";
134
+ }
135
+
136
+ return React.createElement(Box, containerProps, sheetContent);
137
+ }
@@ -0,0 +1,2 @@
1
+ export { Sidebar } from "./sidebar.js";
2
+ export type { SidebarProps, SidebarItem } from "./sidebar.js";
@@ -0,0 +1,225 @@
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 SidebarItem {
8
+ /** Unique identifier */
9
+ id: string;
10
+ /** Display label */
11
+ label: string;
12
+ /** Optional icon */
13
+ icon?: string;
14
+ /** Whether this item is disabled */
15
+ disabled?: boolean;
16
+ /** Child items for nested navigation */
17
+ children?: SidebarItem[];
18
+ /** Whether this is a section header */
19
+ section?: boolean;
20
+ }
21
+
22
+ export interface SidebarProps {
23
+ /** Navigation items */
24
+ items: SidebarItem[];
25
+ /** Selected item ID */
26
+ value?: string;
27
+ /** Callback when selection changes */
28
+ onChange?: (id: string) => void;
29
+ /** Sidebar title/header */
30
+ title?: string;
31
+ /** Sidebar width */
32
+ width?: number;
33
+ /** Whether the sidebar is collapsed */
34
+ collapsed?: boolean;
35
+ /** Custom tokens override */
36
+ tokens?: Tokens;
37
+ /** Focus ID for focus management */
38
+ focusId?: string;
39
+ /** Footer content */
40
+ footer?: React.ReactNode;
41
+ }
42
+
43
+ export function Sidebar({
44
+ items,
45
+ value,
46
+ onChange,
47
+ title,
48
+ width = 25,
49
+ collapsed = false,
50
+ tokens: propTokens,
51
+ focusId,
52
+ footer,
53
+ }: SidebarProps): React.ReactElement {
54
+ const contextTokens = useTokens();
55
+ const tokens = propTokens ?? contextTokens;
56
+ const id = focusId ?? stableId("sidebar");
57
+ const { isFocused } = useFocusable(id);
58
+
59
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
60
+
61
+ // Flatten items for navigation
62
+ const flatItems = items.reduce<SidebarItem[]>((acc, item) => {
63
+ if (!item.section && !item.disabled) {
64
+ acc.push(item);
65
+ }
66
+ if (item.children) {
67
+ acc.push(...item.children.filter((c) => !c.disabled));
68
+ }
69
+ return acc;
70
+ }, []);
71
+
72
+ const handleSelect = useCallback(
73
+ (item: SidebarItem) => {
74
+ if (item.disabled || item.section) return;
75
+ onChange?.(item.id);
76
+ },
77
+ [onChange]
78
+ );
79
+
80
+ useInput(
81
+ (input, key) => {
82
+ if (!isFocused) return;
83
+
84
+ if (key.upArrow || input === "k") {
85
+ setHighlightedIndex((prev) => clamp(prev - 1, 0, flatItems.length - 1));
86
+ } else if (key.downArrow || input === "j") {
87
+ setHighlightedIndex((prev) => clamp(prev + 1, 0, flatItems.length - 1));
88
+ } else if (key.return) {
89
+ const item = flatItems[highlightedIndex];
90
+ if (item) handleSelect(item);
91
+ }
92
+ },
93
+ { isActive: isFocused }
94
+ );
95
+
96
+ if (collapsed) {
97
+ return React.createElement(
98
+ Box,
99
+ {
100
+ flexDirection: "column",
101
+ width: 4,
102
+ borderStyle: "single",
103
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
104
+ borderLeft: false,
105
+ borderTop: false,
106
+ borderBottom: false,
107
+ paddingY: 1,
108
+ },
109
+ items.map((item) =>
110
+ item.section
111
+ ? null
112
+ : React.createElement(
113
+ Box,
114
+ { key: item.id, justifyContent: "center" },
115
+ React.createElement(
116
+ Text,
117
+ {
118
+ color:
119
+ value === item.id
120
+ ? tokens.colors.accent
121
+ : tokens.colors.muted,
122
+ },
123
+ item.icon || item.label.charAt(0)
124
+ )
125
+ )
126
+ )
127
+ );
128
+ }
129
+
130
+ let flatIndex = 0;
131
+ const renderItem = (
132
+ item: SidebarItem,
133
+ depth = 0
134
+ ): React.ReactElement | null => {
135
+ if (item.section) {
136
+ return React.createElement(
137
+ Box,
138
+ { key: item.id, marginTop: depth === 0 ? 1 : 0, paddingX: 1 },
139
+ React.createElement(
140
+ Text,
141
+ { color: tokens.colors.muted, bold: true, dimColor: true },
142
+ item.label.toUpperCase()
143
+ )
144
+ );
145
+ }
146
+
147
+ const currentIndex = flatIndex;
148
+ flatIndex++;
149
+ const isHighlighted = currentIndex === highlightedIndex;
150
+ const isSelected = value === item.id;
151
+
152
+ return React.createElement(
153
+ Box,
154
+ { key: item.id, flexDirection: "column" },
155
+ React.createElement(
156
+ Box,
157
+ { paddingX: 1, paddingLeft: 1 + depth * 2, gap: 1 },
158
+ React.createElement(
159
+ Text,
160
+ {
161
+ color: isHighlighted ? tokens.colors.accent : tokens.colors.muted,
162
+ },
163
+ isHighlighted ? "▸" : " "
164
+ ),
165
+ item.icon ? React.createElement(Text, null, item.icon) : null,
166
+ React.createElement(
167
+ Text,
168
+ {
169
+ color: item.disabled
170
+ ? tokens.colors.muted
171
+ : isSelected
172
+ ? tokens.colors.accent
173
+ : tokens.colors.fg,
174
+ dimColor: item.disabled,
175
+ bold: isSelected,
176
+ },
177
+ item.label
178
+ )
179
+ ),
180
+ item.children
181
+ ? React.createElement(
182
+ Box,
183
+ { flexDirection: "column" },
184
+ ...item.children.map((child) => renderItem(child, depth + 1))
185
+ )
186
+ : null
187
+ );
188
+ };
189
+
190
+ return React.createElement(
191
+ Box,
192
+ {
193
+ flexDirection: "column",
194
+ width,
195
+ borderStyle: "single",
196
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
197
+ borderLeft: false,
198
+ borderTop: false,
199
+ borderBottom: false,
200
+ paddingY: 1,
201
+ },
202
+ // Title
203
+ title
204
+ ? React.createElement(
205
+ Box,
206
+ { paddingX: 1, marginBottom: 1 },
207
+ React.createElement(
208
+ Text,
209
+ { bold: true, color: tokens.colors.fg },
210
+ title
211
+ )
212
+ )
213
+ : null,
214
+ // Items
215
+ ...items.map((item) => renderItem(item)),
216
+ // Footer
217
+ footer
218
+ ? React.createElement(
219
+ Box,
220
+ { marginTop: 1, paddingX: 1, flexGrow: 1, alignItems: "flex-end" },
221
+ footer
222
+ )
223
+ : null
224
+ );
225
+ }
@@ -0,0 +1,2 @@
1
+ export { Skeleton } from "./skeleton.js";
2
+ export type { SkeletonProps } from "./skeleton.js";
@@ -0,0 +1,64 @@
1
+ import React, { useState, useEffect } 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 SkeletonProps {
7
+ /** Width of the skeleton in characters */
8
+ width?: number;
9
+ /** Height (number of lines) */
10
+ height?: number;
11
+ /** Skeleton variant */
12
+ variant?: "text" | "circular" | "rectangular";
13
+ /** Whether to animate */
14
+ animate?: boolean;
15
+ /** Custom tokens override */
16
+ tokens?: Tokens;
17
+ }
18
+
19
+ export function Skeleton({
20
+ width = 20,
21
+ height = 1,
22
+ variant = "text",
23
+ animate = true,
24
+ tokens: propTokens,
25
+ }: SkeletonProps): React.ReactElement {
26
+ const contextTokens = useTokens();
27
+ const tokens = propTokens ?? contextTokens;
28
+ const [phase, setPhase] = useState(0);
29
+
30
+ useEffect(() => {
31
+ if (!animate) return;
32
+ const timer = setInterval(() => {
33
+ setPhase((p) => (p + 1) % 3);
34
+ }, 500);
35
+ return () => clearInterval(timer);
36
+ }, [animate]);
37
+
38
+ const chars = ["░", "▒", "▓"];
39
+ const char = animate ? chars[phase] : "░";
40
+
41
+ if (variant === "circular") {
42
+ return React.createElement(
43
+ Text,
44
+ { color: tokens.colors.border },
45
+ "(",
46
+ char!.repeat(Math.max(1, width - 2)),
47
+ ")"
48
+ );
49
+ }
50
+
51
+ const lines = Array.from({ length: height }, (_, i) => i);
52
+
53
+ return React.createElement(
54
+ Box,
55
+ { flexDirection: "column" },
56
+ lines.map((lineIndex) =>
57
+ React.createElement(
58
+ Text,
59
+ { key: lineIndex, color: tokens.colors.border },
60
+ char!.repeat(width)
61
+ )
62
+ )
63
+ );
64
+ }
@@ -0,0 +1,2 @@
1
+ export { Slider } from "./slider.js";
2
+ export type { SliderProps } from "./slider.js";
@@ -0,0 +1,128 @@
1
+ import React, { 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 SliderProps {
8
+ /** Current value */
9
+ value: number;
10
+ /** Minimum value */
11
+ min?: number;
12
+ /** Maximum value */
13
+ max?: number;
14
+ /** Step increment */
15
+ step?: number;
16
+ /** Callback when value changes */
17
+ onChange: (value: number) => void;
18
+ /** Slider width in characters */
19
+ width?: number;
20
+ /** Whether the slider is disabled */
21
+ disabled?: boolean;
22
+ /** Label text */
23
+ label?: string;
24
+ /** Show value label */
25
+ showValue?: boolean;
26
+ /** Custom tokens override */
27
+ tokens?: Tokens;
28
+ /** Focus ID for focus management */
29
+ focusId?: string;
30
+ }
31
+
32
+ export function Slider({
33
+ value,
34
+ min = 0,
35
+ max = 100,
36
+ step = 1,
37
+ onChange,
38
+ width = 20,
39
+ disabled = false,
40
+ label,
41
+ showValue = true,
42
+ tokens: propTokens,
43
+ focusId,
44
+ }: SliderProps): React.ReactElement {
45
+ const contextTokens = useTokens();
46
+ const tokens = propTokens ?? contextTokens;
47
+ const id = focusId ?? stableId("slider");
48
+ const { isFocused } = useFocusable(id);
49
+
50
+ const updateValue = useCallback(
51
+ (delta: number) => {
52
+ const newValue = clamp(value + delta, min, max);
53
+ if (newValue !== value) {
54
+ onChange(newValue);
55
+ }
56
+ },
57
+ [value, min, max, onChange]
58
+ );
59
+
60
+ useInput(
61
+ (input, key) => {
62
+ if (!isFocused || disabled) return;
63
+
64
+ if (key.leftArrow || input === "h") {
65
+ updateValue(-step);
66
+ } else if (key.rightArrow || input === "l") {
67
+ updateValue(step);
68
+ } else if (input === "0") {
69
+ onChange(min);
70
+ } else if (input === "$") {
71
+ onChange(max);
72
+ }
73
+ },
74
+ { isActive: isFocused }
75
+ );
76
+
77
+ // Calculate filled portion
78
+ const percentage = ((value - min) / (max - min)) * 100;
79
+ const filled = Math.round((percentage / 100) * width);
80
+ const empty = width - filled;
81
+
82
+ // Create slider track
83
+ const track = "█".repeat(filled) + "░".repeat(empty);
84
+
85
+ return React.createElement(
86
+ Box,
87
+ { flexDirection: "column" },
88
+ label
89
+ ? React.createElement(
90
+ Text,
91
+ { color: tokens.colors.fg, bold: true },
92
+ label
93
+ )
94
+ : null,
95
+ React.createElement(
96
+ Box,
97
+ {
98
+ gap: 1,
99
+ borderStyle: isFocused ? "round" : "single",
100
+ borderColor: disabled
101
+ ? tokens.colors.disabled
102
+ : isFocused
103
+ ? tokens.colors.focus
104
+ : tokens.colors.border,
105
+ paddingX: 1,
106
+ },
107
+ React.createElement(
108
+ Text,
109
+ { color: disabled ? tokens.colors.disabled : tokens.colors.accent },
110
+ track
111
+ ),
112
+ showValue
113
+ ? React.createElement(
114
+ Text,
115
+ { color: tokens.colors.muted },
116
+ `${value}/${max}`
117
+ )
118
+ : null
119
+ ),
120
+ isFocused
121
+ ? React.createElement(
122
+ Text,
123
+ { color: tokens.colors.muted, dimColor: true },
124
+ "←/→ to adjust, 0/$ for min/max"
125
+ )
126
+ : null
127
+ );
128
+ }
@@ -0,0 +1,2 @@
1
+ export { Spinner } from "./spinner.js";
2
+ export type { SpinnerProps, SpinnerVariant } from "./spinner.js";
@@ -0,0 +1,57 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Text } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens } from "@hauktui/primitives-ink";
5
+
6
+ export type SpinnerVariant = "dots" | "line" | "arc" | "circle" | "bounce";
7
+
8
+ export interface SpinnerProps {
9
+ /** Loading text to display */
10
+ label?: string;
11
+ /** Spinner variant */
12
+ variant?: SpinnerVariant;
13
+ /** Custom tokens override */
14
+ tokens?: Tokens;
15
+ /** Spin speed in milliseconds */
16
+ speed?: number;
17
+ }
18
+
19
+ const SPINNERS: Record<SpinnerVariant, string[]> = {
20
+ dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
21
+ line: ["-", "\\", "|", "/"],
22
+ arc: ["◜", "◠", "◝", "◞", "◡", "◟"],
23
+ circle: ["◐", "◓", "◑", "◒"],
24
+ bounce: ["⠁", "⠂", "⠄", "⠂"],
25
+ };
26
+
27
+ export function Spinner({
28
+ label,
29
+ variant = "dots",
30
+ tokens: propTokens,
31
+ speed = 80,
32
+ }: SpinnerProps): React.ReactElement {
33
+ const contextTokens = useTokens();
34
+ const tokens = propTokens ?? contextTokens;
35
+ const frames = SPINNERS[variant];
36
+ const [frameIndex, setFrameIndex] = useState(0);
37
+
38
+ useEffect(() => {
39
+ const timer = setInterval(() => {
40
+ setFrameIndex((prev) => (prev + 1) % frames.length);
41
+ }, speed);
42
+ return () => clearInterval(timer);
43
+ }, [frames.length, speed]);
44
+
45
+ return React.createElement(
46
+ Text,
47
+ null,
48
+ React.createElement(
49
+ Text,
50
+ { color: tokens.colors.accent },
51
+ frames[frameIndex]
52
+ ),
53
+ label
54
+ ? React.createElement(Text, { color: tokens.colors.fg }, " ", label)
55
+ : null
56
+ );
57
+ }
@@ -0,0 +1,2 @@
1
+ export { Stat, StatGroup } from "./stat.js";
2
+ export type { StatProps, StatGroupProps } from "./stat.js";