@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,179 @@
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 } from "@hauktui/core";
6
+
7
+ export interface TextInputProps {
8
+ /** Current input value (controlled) */
9
+ value?: string;
10
+ /** Default value (uncontrolled) */
11
+ defaultValue?: string;
12
+ /** Callback when value changes */
13
+ onChange?: (value: string) => void;
14
+ /** Callback when Enter is pressed */
15
+ onSubmit?: (value: string) => void;
16
+ /** Placeholder text */
17
+ placeholder?: string;
18
+ /** Input label */
19
+ label?: string;
20
+ /** Whether the input is disabled */
21
+ disabled?: boolean;
22
+ /** Maximum input length */
23
+ maxLength?: number;
24
+ /** Custom tokens override */
25
+ tokens?: Tokens;
26
+ /** Focus ID for focus management */
27
+ focusId?: string;
28
+ /** Mask character for password inputs */
29
+ mask?: string;
30
+ }
31
+
32
+ export function TextInput({
33
+ value: controlledValue,
34
+ defaultValue = "",
35
+ onChange,
36
+ onSubmit,
37
+ placeholder = "",
38
+ label,
39
+ disabled = false,
40
+ maxLength,
41
+ tokens: propTokens,
42
+ focusId,
43
+ mask,
44
+ }: TextInputProps): React.ReactElement {
45
+ const contextTokens = useTokens();
46
+ const tokens = propTokens ?? contextTokens;
47
+ const id = focusId ?? stableId("text-input");
48
+ const { isFocused } = useFocusable(id);
49
+
50
+ const [internalValue, setInternalValue] = useState(defaultValue);
51
+ const [cursorPosition, setCursorPosition] = useState(defaultValue.length);
52
+
53
+ const isControlled = controlledValue !== undefined;
54
+ const currentValue = isControlled ? controlledValue : internalValue;
55
+
56
+ const updateValue = useCallback(
57
+ (newValue: string) => {
58
+ if (maxLength !== undefined && newValue.length > maxLength) {
59
+ newValue = newValue.slice(0, maxLength);
60
+ }
61
+ if (!isControlled) {
62
+ setInternalValue(newValue);
63
+ }
64
+ onChange?.(newValue);
65
+ },
66
+ [isControlled, maxLength, onChange]
67
+ );
68
+
69
+ useInput(
70
+ (input, key) => {
71
+ if (!isFocused || disabled) return;
72
+
73
+ if (key.return) {
74
+ onSubmit?.(currentValue);
75
+ return;
76
+ }
77
+
78
+ if (key.backspace || key.delete) {
79
+ if (cursorPosition > 0) {
80
+ const newValue =
81
+ currentValue.slice(0, cursorPosition - 1) +
82
+ currentValue.slice(cursorPosition);
83
+ updateValue(newValue);
84
+ setCursorPosition(Math.max(0, cursorPosition - 1));
85
+ }
86
+ return;
87
+ }
88
+
89
+ if (key.leftArrow) {
90
+ setCursorPosition(Math.max(0, cursorPosition - 1));
91
+ return;
92
+ }
93
+
94
+ if (key.rightArrow) {
95
+ setCursorPosition(Math.min(currentValue.length, cursorPosition + 1));
96
+ return;
97
+ }
98
+
99
+ // Home key
100
+ if (key.ctrl && input === "a") {
101
+ setCursorPosition(0);
102
+ return;
103
+ }
104
+
105
+ // End key
106
+ if (key.ctrl && input === "e") {
107
+ setCursorPosition(currentValue.length);
108
+ return;
109
+ }
110
+
111
+ // Regular character input
112
+ if (input && !key.ctrl && !key.meta) {
113
+ const newValue =
114
+ currentValue.slice(0, cursorPosition) +
115
+ input +
116
+ currentValue.slice(cursorPosition);
117
+ updateValue(newValue);
118
+ setCursorPosition(cursorPosition + input.length);
119
+ }
120
+ },
121
+ { isActive: isFocused }
122
+ );
123
+
124
+ // Display value (masked if needed)
125
+ const displayValue = mask
126
+ ? mask.repeat(currentValue.length)
127
+ : currentValue;
128
+
129
+ // Render with cursor
130
+ const renderValue = () => {
131
+ if (!isFocused) {
132
+ if (displayValue.length === 0) {
133
+ return React.createElement(Text, { color: tokens.colors.muted }, placeholder);
134
+ }
135
+ return React.createElement(Text, { color: tokens.colors.fg }, displayValue);
136
+ }
137
+
138
+ const before = displayValue.slice(0, cursorPosition);
139
+ const cursor = displayValue[cursorPosition] ?? " ";
140
+ const after = displayValue.slice(cursorPosition + 1);
141
+
142
+ return React.createElement(
143
+ Text,
144
+ null,
145
+ React.createElement(Text, { color: tokens.colors.fg }, before),
146
+ React.createElement(
147
+ Text,
148
+ { backgroundColor: tokens.colors.accent, color: tokens.colors.bg },
149
+ cursor
150
+ ),
151
+ React.createElement(Text, { color: tokens.colors.fg }, after)
152
+ );
153
+ };
154
+
155
+ return React.createElement(
156
+ Box,
157
+ { flexDirection: "column" },
158
+ label
159
+ ? React.createElement(
160
+ Text,
161
+ { color: isFocused ? tokens.colors.accent : tokens.colors.muted },
162
+ label
163
+ )
164
+ : null,
165
+ React.createElement(
166
+ Box,
167
+ {
168
+ borderStyle: "round",
169
+ borderColor: disabled
170
+ ? tokens.colors.disabled
171
+ : isFocused
172
+ ? tokens.colors.focus
173
+ : tokens.colors.border,
174
+ paddingX: 1,
175
+ },
176
+ renderValue()
177
+ )
178
+ );
179
+ }
@@ -0,0 +1,2 @@
1
+ export { Textarea } from "./textarea.js";
2
+ export type { TextareaProps } from "./textarea.js";
@@ -0,0 +1,206 @@
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 } from "@hauktui/core";
6
+
7
+ export interface TextareaProps {
8
+ /** Controlled value */
9
+ value?: string;
10
+ /** Default value for uncontrolled mode */
11
+ defaultValue?: string;
12
+ /** Callback when value changes */
13
+ onChange?: (value: string) => void;
14
+ /** Placeholder text */
15
+ placeholder?: string;
16
+ /** Label text */
17
+ label?: string;
18
+ /** Whether the textarea is disabled */
19
+ disabled?: boolean;
20
+ /** Custom tokens override */
21
+ tokens?: Tokens;
22
+ /** Focus ID for focus management */
23
+ focusId?: string;
24
+ /** Number of visible rows */
25
+ rows?: number;
26
+ /** Maximum length */
27
+ maxLength?: number;
28
+ }
29
+
30
+ export function Textarea({
31
+ value: controlledValue,
32
+ defaultValue = "",
33
+ onChange,
34
+ placeholder = "",
35
+ label,
36
+ disabled = false,
37
+ tokens: propTokens,
38
+ focusId,
39
+ rows = 3,
40
+ maxLength,
41
+ }: TextareaProps): React.ReactElement {
42
+ const contextTokens = useTokens();
43
+ const tokens = propTokens ?? contextTokens;
44
+ const id = focusId ?? stableId("textarea");
45
+ const { isFocused } = useFocusable(id);
46
+
47
+ const [internalValue, setInternalValue] = useState(defaultValue);
48
+ const [cursorRow, setCursorRow] = useState(0);
49
+ const [cursorCol, setCursorCol] = useState(0);
50
+
51
+ const isControlled = controlledValue !== undefined;
52
+ const currentValue = isControlled ? controlledValue : internalValue;
53
+ const lines = currentValue.split("\n");
54
+
55
+ const updateValue = useCallback(
56
+ (newValue: string) => {
57
+ if (maxLength && newValue.length > maxLength) {
58
+ newValue = newValue.slice(0, maxLength);
59
+ }
60
+ if (!isControlled) {
61
+ setInternalValue(newValue);
62
+ }
63
+ onChange?.(newValue);
64
+ },
65
+ [isControlled, onChange, maxLength]
66
+ );
67
+
68
+ useInput(
69
+ (input, key) => {
70
+ if (!isFocused || disabled) return;
71
+
72
+ if (key.return) {
73
+ // Insert newline
74
+ const before = lines.slice(0, cursorRow).join("\n");
75
+ const currentLine = lines[cursorRow] || "";
76
+ const lineStart = currentLine.slice(0, cursorCol);
77
+ const lineEnd = currentLine.slice(cursorCol);
78
+ const after = lines.slice(cursorRow + 1).join("\n");
79
+
80
+ let newValue = before ? before + "\n" : "";
81
+ newValue += lineStart + "\n" + lineEnd;
82
+ if (after) newValue += "\n" + after;
83
+
84
+ updateValue(newValue);
85
+ setCursorRow(cursorRow + 1);
86
+ setCursorCol(0);
87
+ } else if (key.backspace || key.delete) {
88
+ if (cursorCol > 0) {
89
+ const currentLine = lines[cursorRow] || "";
90
+ const newLine =
91
+ currentLine.slice(0, cursorCol - 1) + currentLine.slice(cursorCol);
92
+ lines[cursorRow] = newLine;
93
+ updateValue(lines.join("\n"));
94
+ setCursorCol(cursorCol - 1);
95
+ } else if (cursorRow > 0) {
96
+ const prevLine = lines[cursorRow - 1] || "";
97
+ const currentLine = lines[cursorRow] || "";
98
+ lines[cursorRow - 1] = prevLine + currentLine;
99
+ lines.splice(cursorRow, 1);
100
+ updateValue(lines.join("\n"));
101
+ setCursorRow(cursorRow - 1);
102
+ setCursorCol(prevLine.length);
103
+ }
104
+ } else if (key.upArrow) {
105
+ if (cursorRow > 0) {
106
+ setCursorRow(cursorRow - 1);
107
+ const prevLineLen = (lines[cursorRow - 1] || "").length;
108
+ setCursorCol(Math.min(cursorCol, prevLineLen));
109
+ }
110
+ } else if (key.downArrow) {
111
+ if (cursorRow < lines.length - 1) {
112
+ setCursorRow(cursorRow + 1);
113
+ const nextLineLen = (lines[cursorRow + 1] || "").length;
114
+ setCursorCol(Math.min(cursorCol, nextLineLen));
115
+ }
116
+ } else if (key.leftArrow) {
117
+ if (cursorCol > 0) {
118
+ setCursorCol(cursorCol - 1);
119
+ } else if (cursorRow > 0) {
120
+ setCursorRow(cursorRow - 1);
121
+ setCursorCol((lines[cursorRow - 1] || "").length);
122
+ }
123
+ } else if (key.rightArrow) {
124
+ const lineLen = (lines[cursorRow] || "").length;
125
+ if (cursorCol < lineLen) {
126
+ setCursorCol(cursorCol + 1);
127
+ } else if (cursorRow < lines.length - 1) {
128
+ setCursorRow(cursorRow + 1);
129
+ setCursorCol(0);
130
+ }
131
+ } else if (input && !key.ctrl && !key.meta) {
132
+ const currentLine = lines[cursorRow] || "";
133
+ const newLine =
134
+ currentLine.slice(0, cursorCol) +
135
+ input +
136
+ currentLine.slice(cursorCol);
137
+ lines[cursorRow] = newLine;
138
+ updateValue(lines.join("\n"));
139
+ setCursorCol(cursorCol + input.length);
140
+ }
141
+ },
142
+ { isActive: isFocused }
143
+ );
144
+
145
+ const isEmpty = currentValue.length === 0;
146
+ const displayLines = isEmpty ? [placeholder] : lines;
147
+
148
+ return React.createElement(
149
+ Box,
150
+ { flexDirection: "column" },
151
+ label
152
+ ? React.createElement(
153
+ Text,
154
+ { color: tokens.colors.fg, bold: true },
155
+ label
156
+ )
157
+ : null,
158
+ React.createElement(
159
+ Box,
160
+ {
161
+ flexDirection: "column",
162
+ borderStyle: isFocused ? "round" : "single",
163
+ borderColor: disabled
164
+ ? tokens.colors.disabled
165
+ : isFocused
166
+ ? tokens.colors.focus
167
+ : tokens.colors.border,
168
+ paddingX: 1,
169
+ },
170
+ displayLines.slice(0, rows).map((line, rowIndex) => {
171
+ const isCurrentRow = rowIndex === cursorRow && isFocused && !isEmpty;
172
+
173
+ if (isCurrentRow) {
174
+ const before = line.slice(0, cursorCol);
175
+ const cursor = line[cursorCol] || " ";
176
+ const after = line.slice(cursorCol + 1);
177
+
178
+ return React.createElement(
179
+ Text,
180
+ { key: rowIndex, color: tokens.colors.fg },
181
+ before,
182
+ React.createElement(Text, { inverse: true }, cursor),
183
+ after
184
+ );
185
+ }
186
+
187
+ return React.createElement(
188
+ Text,
189
+ {
190
+ key: rowIndex,
191
+ color: isEmpty ? tokens.colors.muted : tokens.colors.fg,
192
+ dimColor: isEmpty,
193
+ },
194
+ line || " "
195
+ );
196
+ })
197
+ ),
198
+ maxLength
199
+ ? React.createElement(
200
+ Text,
201
+ { color: tokens.colors.muted, dimColor: true },
202
+ `${currentValue.length}/${maxLength}`
203
+ )
204
+ : null
205
+ );
206
+ }
@@ -0,0 +1,2 @@
1
+ export { Timeline } from "./timeline.js";
2
+ export type { TimelineProps, TimelineItem } from "./timeline.js";
@@ -0,0 +1,167 @@
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 TimelineItem {
7
+ /** Unique identifier */
8
+ id: string;
9
+ /** Item title */
10
+ title: string;
11
+ /** Item description */
12
+ description?: string;
13
+ /** Timestamp or date label */
14
+ date?: string;
15
+ /** Status of this item */
16
+ status?: "completed" | "current" | "upcoming" | "error";
17
+ /** Custom icon */
18
+ icon?: string;
19
+ }
20
+
21
+ export interface TimelineProps {
22
+ /** Timeline items */
23
+ items: TimelineItem[];
24
+ /** Orientation */
25
+ orientation?: "vertical" | "horizontal";
26
+ /** Show connecting lines */
27
+ showLines?: boolean;
28
+ /** Custom tokens override */
29
+ tokens?: Tokens;
30
+ }
31
+
32
+ export function Timeline({
33
+ items,
34
+ orientation = "vertical",
35
+ showLines = true,
36
+ tokens: propTokens,
37
+ }: TimelineProps): React.ReactElement {
38
+ const contextTokens = useTokens();
39
+ const tokens = propTokens ?? contextTokens;
40
+
41
+ // Get icon and color for status
42
+ const getStatusStyle = (
43
+ status: TimelineItem["status"]
44
+ ): { icon: string; color: string } => {
45
+ switch (status) {
46
+ case "completed":
47
+ return { icon: "✓", color: tokens.colors.success };
48
+ case "current":
49
+ return { icon: "●", color: tokens.colors.accent };
50
+ case "error":
51
+ return { icon: "✗", color: tokens.colors.danger };
52
+ case "upcoming":
53
+ default:
54
+ return { icon: "○", color: tokens.colors.muted };
55
+ }
56
+ };
57
+
58
+ if (orientation === "horizontal") {
59
+ return React.createElement(
60
+ Box,
61
+ { flexDirection: "row", gap: 1 },
62
+ items.map((item, index) => {
63
+ const style = getStatusStyle(item.status);
64
+ const isLast = index === items.length - 1;
65
+
66
+ return React.createElement(
67
+ Box,
68
+ { key: item.id, flexDirection: "column", alignItems: "center" },
69
+ // Icon and line
70
+ React.createElement(
71
+ Box,
72
+ { gap: 0 },
73
+ React.createElement(
74
+ Text,
75
+ { color: style.color, bold: item.status === "current" },
76
+ item.icon ?? style.icon
77
+ ),
78
+ showLines &&
79
+ !isLast &&
80
+ React.createElement(Text, { color: tokens.colors.border }, "───")
81
+ ),
82
+ // Content
83
+ React.createElement(
84
+ Box,
85
+ { flexDirection: "column", alignItems: "center", marginTop: 1 },
86
+ item.date &&
87
+ React.createElement(
88
+ Text,
89
+ { color: tokens.colors.muted, dimColor: true },
90
+ item.date
91
+ ),
92
+ React.createElement(
93
+ Text,
94
+ {
95
+ color:
96
+ item.status === "current"
97
+ ? tokens.colors.accent
98
+ : tokens.colors.fg,
99
+ bold: item.status === "current",
100
+ },
101
+ item.title
102
+ )
103
+ )
104
+ );
105
+ })
106
+ );
107
+ }
108
+
109
+ // Vertical timeline
110
+ return React.createElement(
111
+ Box,
112
+ { flexDirection: "column" },
113
+ items.map((item, index) => {
114
+ const style = getStatusStyle(item.status);
115
+ const isLast = index === items.length - 1;
116
+
117
+ return React.createElement(
118
+ Box,
119
+ { key: item.id, flexDirection: "row" },
120
+ // Icon column
121
+ React.createElement(
122
+ Box,
123
+ { flexDirection: "column", alignItems: "center", marginRight: 1 },
124
+ React.createElement(
125
+ Text,
126
+ { color: style.color, bold: item.status === "current" },
127
+ item.icon ?? style.icon
128
+ ),
129
+ showLines &&
130
+ !isLast &&
131
+ React.createElement(Text, { color: tokens.colors.border }, "│")
132
+ ),
133
+ // Content column
134
+ React.createElement(
135
+ Box,
136
+ { flexDirection: "column", marginBottom: isLast ? 0 : 1 },
137
+ // Date
138
+ item.date &&
139
+ React.createElement(
140
+ Text,
141
+ { color: tokens.colors.muted, dimColor: true },
142
+ item.date
143
+ ),
144
+ // Title
145
+ React.createElement(
146
+ Text,
147
+ {
148
+ color:
149
+ item.status === "current"
150
+ ? tokens.colors.accent
151
+ : tokens.colors.fg,
152
+ bold: item.status === "current",
153
+ },
154
+ item.title
155
+ ),
156
+ // Description
157
+ item.description &&
158
+ React.createElement(
159
+ Text,
160
+ { color: tokens.colors.muted },
161
+ item.description
162
+ )
163
+ )
164
+ );
165
+ })
166
+ );
167
+ }
@@ -0,0 +1,2 @@
1
+ export { Toast } from "./toast.js";
2
+ export type { ToastProps, ToastVariant } from "./toast.js";
@@ -0,0 +1,93 @@
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 type ToastVariant = "default" | "success" | "error" | "warning" | "info";
7
+
8
+ export interface ToastProps {
9
+ /** Toast message */
10
+ message: string;
11
+ /** Toast title */
12
+ title?: string;
13
+ /** Toast variant */
14
+ variant?: ToastVariant;
15
+ /** Duration in milliseconds (0 = persistent) */
16
+ duration?: number;
17
+ /** Callback when toast should be dismissed */
18
+ onDismiss?: () => void;
19
+ /** Custom tokens override */
20
+ tokens?: Tokens;
21
+ /** Show icon */
22
+ showIcon?: boolean;
23
+ }
24
+
25
+ const ICONS: Record<ToastVariant, string> = {
26
+ default: "●",
27
+ success: "✓",
28
+ error: "✗",
29
+ warning: "⚠",
30
+ info: "ℹ",
31
+ };
32
+
33
+ export function Toast({
34
+ message,
35
+ title,
36
+ variant = "default",
37
+ duration = 3000,
38
+ onDismiss,
39
+ tokens: propTokens,
40
+ showIcon = true,
41
+ }: ToastProps): React.ReactElement | null {
42
+ const contextTokens = useTokens();
43
+ const tokens = propTokens ?? contextTokens;
44
+ const [visible, setVisible] = useState(true);
45
+
46
+ useEffect(() => {
47
+ if (duration > 0) {
48
+ const timer = setTimeout(() => {
49
+ setVisible(false);
50
+ onDismiss?.();
51
+ }, duration);
52
+ return () => clearTimeout(timer);
53
+ }
54
+ }, [duration, onDismiss]);
55
+
56
+ if (!visible) return null;
57
+
58
+ const variantColors: Record<ToastVariant, string> = {
59
+ default: tokens.colors.fg,
60
+ success: tokens.colors.success,
61
+ error: tokens.colors.danger,
62
+ warning: tokens.colors.warning,
63
+ info: tokens.colors.accent,
64
+ };
65
+
66
+ const color = variantColors[variant];
67
+ const icon = ICONS[variant];
68
+
69
+ return React.createElement(
70
+ Box,
71
+ {
72
+ flexDirection: "column",
73
+ borderStyle: "round",
74
+ borderColor: color,
75
+ paddingX: 1,
76
+ },
77
+ React.createElement(
78
+ Box,
79
+ { gap: 1 },
80
+ showIcon ? React.createElement(Text, { color }, icon) : null,
81
+ title
82
+ ? React.createElement(Text, { color, bold: true }, title)
83
+ : React.createElement(Text, { color: tokens.colors.fg }, message)
84
+ ),
85
+ title
86
+ ? React.createElement(
87
+ Box,
88
+ { paddingLeft: showIcon ? 2 : 0 },
89
+ React.createElement(Text, { color: tokens.colors.fg }, message)
90
+ )
91
+ : null
92
+ );
93
+ }
@@ -0,0 +1,2 @@
1
+ export { Toggle } from "./toggle.js";
2
+ export type { ToggleProps } from "./toggle.js";