@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,84 @@
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens, useFocusable, FocusGroup } from "@hauktui/primitives-ink";
5
+ import { stableId } from "@hauktui/core";
6
+
7
+ export interface DialogProps {
8
+ /** Dialog title */
9
+ title: string;
10
+ /** Dialog description/content */
11
+ children: React.ReactNode;
12
+ /** Whether the dialog is open */
13
+ open?: boolean;
14
+ /** Callback when dialog should close */
15
+ onClose?: () => void;
16
+ /** Custom tokens override */
17
+ tokens?: Tokens;
18
+ /** Focus ID for focus management */
19
+ focusId?: string;
20
+ /** Dialog width */
21
+ width?: number;
22
+ /** Show close hint */
23
+ showCloseHint?: boolean;
24
+ }
25
+
26
+ export function Dialog({
27
+ title,
28
+ children,
29
+ open = true,
30
+ onClose,
31
+ tokens: propTokens,
32
+ focusId,
33
+ width = 50,
34
+ showCloseHint = true,
35
+ }: DialogProps): React.ReactElement | null {
36
+ const contextTokens = useTokens();
37
+ const tokens = propTokens ?? contextTokens;
38
+ const id = focusId ?? stableId("dialog");
39
+ const { isFocused } = useFocusable(id);
40
+
41
+ useInput(
42
+ (input, key) => {
43
+ if (!isFocused) return;
44
+ if (key.escape || input === "q") {
45
+ onClose?.();
46
+ }
47
+ },
48
+ { isActive: isFocused }
49
+ );
50
+
51
+ if (!open) return null;
52
+
53
+ return React.createElement(
54
+ Box,
55
+ {
56
+ flexDirection: "column",
57
+ borderStyle: "round",
58
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
59
+ paddingX: 1,
60
+ paddingY: 0,
61
+ width,
62
+ },
63
+ // Title
64
+ React.createElement(
65
+ Box,
66
+ { marginBottom: 1 },
67
+ React.createElement(Text, { color: tokens.colors.fg, bold: true }, title)
68
+ ),
69
+ // Content
70
+ React.createElement(Box, { flexDirection: "column" }, children),
71
+ // Close hint
72
+ showCloseHint
73
+ ? React.createElement(
74
+ Box,
75
+ { marginTop: 1 },
76
+ React.createElement(
77
+ Text,
78
+ { color: tokens.colors.muted, dimColor: true },
79
+ "Press ESC or Q to close"
80
+ )
81
+ )
82
+ : null
83
+ );
84
+ }
@@ -0,0 +1,2 @@
1
+ export { Dialog } from "./dialog.js";
2
+ export type { DialogProps } from "./dialog.js";
@@ -0,0 +1,141 @@
1
+ import React, { 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 type DrawerSide = "left" | "right" | "top" | "bottom";
8
+
9
+ export interface DrawerProps {
10
+ /** Whether the drawer is open */
11
+ open: boolean;
12
+ /** Callback when drawer should close */
13
+ onClose: () => void;
14
+ /** Content of the drawer */
15
+ children: React.ReactNode;
16
+ /** Side to open from */
17
+ side?: DrawerSide;
18
+ /** Drawer title */
19
+ title?: string;
20
+ /** Drawer width (for left/right) or height (for top/bottom) */
21
+ size?: number;
22
+ /** Custom tokens override */
23
+ tokens?: Tokens;
24
+ /** Focus ID for focus management */
25
+ focusId?: string;
26
+ /** Show close button hint */
27
+ showCloseHint?: boolean;
28
+ }
29
+
30
+ export function Drawer({
31
+ open,
32
+ onClose,
33
+ children,
34
+ side = "right",
35
+ title,
36
+ size = 40,
37
+ tokens: propTokens,
38
+ focusId,
39
+ showCloseHint = true,
40
+ }: DrawerProps): React.ReactElement | null {
41
+ const contextTokens = useTokens();
42
+ const tokens = propTokens ?? contextTokens;
43
+ const id = focusId ?? stableId("drawer");
44
+ const { isFocused } = useFocusable(id);
45
+
46
+ useInput(
47
+ (input, key) => {
48
+ if (!open) return;
49
+ if (key.escape || input === "q") {
50
+ onClose();
51
+ }
52
+ },
53
+ { isActive: open && isFocused }
54
+ );
55
+
56
+ if (!open) return null;
57
+
58
+ const isHorizontal = side === "left" || side === "right";
59
+
60
+ const borderStyle = {
61
+ left: {
62
+ borderRight: true,
63
+ borderLeft: false,
64
+ borderTop: false,
65
+ borderBottom: false,
66
+ },
67
+ right: {
68
+ borderLeft: true,
69
+ borderRight: false,
70
+ borderTop: false,
71
+ borderBottom: false,
72
+ },
73
+ top: {
74
+ borderBottom: true,
75
+ borderTop: false,
76
+ borderLeft: false,
77
+ borderRight: false,
78
+ },
79
+ bottom: {
80
+ borderTop: true,
81
+ borderBottom: false,
82
+ borderLeft: false,
83
+ borderRight: false,
84
+ },
85
+ }[side];
86
+
87
+ const drawerContent = React.createElement(
88
+ Box,
89
+ {
90
+ flexDirection: "column",
91
+ width: isHorizontal ? size : "100%",
92
+ height: isHorizontal ? "100%" : size,
93
+ borderStyle: "single",
94
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
95
+ ...borderStyle,
96
+ paddingX: 1,
97
+ paddingY: 1,
98
+ },
99
+ // Header
100
+ title
101
+ ? React.createElement(
102
+ Box,
103
+ { marginBottom: 1 },
104
+ React.createElement(
105
+ Text,
106
+ { bold: true, color: tokens.colors.fg },
107
+ title
108
+ )
109
+ )
110
+ : null,
111
+ // Content
112
+ React.createElement(
113
+ Box,
114
+ { flexDirection: "column", flexGrow: 1 },
115
+ children
116
+ ),
117
+ // Close hint
118
+ showCloseHint
119
+ ? React.createElement(
120
+ Box,
121
+ { marginTop: 1 },
122
+ React.createElement(
123
+ Text,
124
+ { color: tokens.colors.muted, dimColor: true },
125
+ "Press ESC or Q to close"
126
+ )
127
+ )
128
+ : null
129
+ );
130
+
131
+ // Position the drawer based on side
132
+ const containerProps: Record<string, unknown> = {
133
+ flexDirection: isHorizontal ? "row" : "column",
134
+ };
135
+
136
+ if (side === "right" || side === "bottom") {
137
+ containerProps.justifyContent = "flex-end";
138
+ }
139
+
140
+ return React.createElement(Box, containerProps, drawerContent);
141
+ }
@@ -0,0 +1,2 @@
1
+ export { Drawer } from "./drawer.js";
2
+ export type { DrawerProps, DrawerSide } from "./drawer.js";
@@ -0,0 +1,188 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens, useFocusable } from "@hauktui/primitives-ink";
5
+ import { stableId, clamp } from "@hauktui/core";
6
+
7
+ export interface DropdownMenuItem {
8
+ /** Unique identifier */
9
+ id: string;
10
+ /** Display label */
11
+ label: string;
12
+ /** Optional description */
13
+ description?: string;
14
+ /** Optional keyboard shortcut */
15
+ shortcut?: string;
16
+ /** Whether this item is disabled */
17
+ disabled?: boolean;
18
+ /** Whether this is a separator */
19
+ separator?: boolean;
20
+ /** Optional icon/prefix */
21
+ icon?: string;
22
+ }
23
+
24
+ export interface DropdownMenuProps {
25
+ /** Menu items */
26
+ items: DropdownMenuItem[];
27
+ /** Whether the menu is open */
28
+ open: boolean;
29
+ /** Callback when menu should close */
30
+ onClose: () => void;
31
+ /** Callback when an item is selected */
32
+ onSelect: (item: DropdownMenuItem) => void;
33
+ /** Menu title/trigger label */
34
+ trigger?: string;
35
+ /** Custom tokens override */
36
+ tokens?: Tokens;
37
+ /** Focus ID for focus management */
38
+ focusId?: string;
39
+ /** Menu width */
40
+ width?: number;
41
+ }
42
+
43
+ export function DropdownMenu({
44
+ items,
45
+ open,
46
+ onClose,
47
+ onSelect,
48
+ trigger,
49
+ tokens: propTokens,
50
+ focusId,
51
+ width = 30,
52
+ }: DropdownMenuProps): React.ReactElement {
53
+ const contextTokens = useTokens();
54
+ const tokens = propTokens ?? contextTokens;
55
+ const id = focusId ?? stableId("dropdown-menu");
56
+ const { isFocused } = useFocusable(id);
57
+
58
+ const [selectedIndex, setSelectedIndex] = useState(0);
59
+
60
+ const selectableItems = items.filter(
61
+ (item) => !item.separator && !item.disabled
62
+ );
63
+
64
+ const handleSelect = useCallback(() => {
65
+ const item = selectableItems[selectedIndex];
66
+ if (item && !item.disabled) {
67
+ onSelect(item);
68
+ onClose();
69
+ }
70
+ }, [selectableItems, selectedIndex, onSelect, onClose]);
71
+
72
+ useInput(
73
+ (input, key) => {
74
+ if (!open || !isFocused) return;
75
+
76
+ if (key.escape || input === "q") {
77
+ onClose();
78
+ return;
79
+ }
80
+
81
+ if (key.upArrow || input === "k") {
82
+ setSelectedIndex((prev) =>
83
+ clamp(prev - 1, 0, selectableItems.length - 1)
84
+ );
85
+ } else if (key.downArrow || input === "j") {
86
+ setSelectedIndex((prev) =>
87
+ clamp(prev + 1, 0, selectableItems.length - 1)
88
+ );
89
+ } else if (key.return) {
90
+ handleSelect();
91
+ }
92
+ },
93
+ { isActive: open && isFocused }
94
+ );
95
+
96
+ const triggerElement = trigger
97
+ ? React.createElement(
98
+ Box,
99
+ { marginBottom: open ? 0 : 0 },
100
+ React.createElement(
101
+ Text,
102
+ { color: isFocused ? tokens.colors.focus : tokens.colors.fg },
103
+ trigger,
104
+ " ",
105
+ open ? "▲" : "▼"
106
+ )
107
+ )
108
+ : null;
109
+
110
+ if (!open) {
111
+ return triggerElement || React.createElement(Box, null);
112
+ }
113
+
114
+ let selectableIndex = 0;
115
+ const menuItems = items.map((item, index) => {
116
+ if (item.separator) {
117
+ return React.createElement(
118
+ Box,
119
+ { key: `sep-${index}`, paddingX: 1 },
120
+ React.createElement(
121
+ Text,
122
+ { color: tokens.colors.border },
123
+ "─".repeat(width - 4)
124
+ )
125
+ );
126
+ }
127
+
128
+ const isSelected = selectableItems[selectedIndex]?.id === item.id;
129
+ const currentSelectableIndex = selectableIndex;
130
+ selectableIndex++;
131
+
132
+ return React.createElement(
133
+ Box,
134
+ {
135
+ key: item.id,
136
+ paddingX: 1,
137
+ gap: 1,
138
+ },
139
+ React.createElement(
140
+ Text,
141
+ { color: isSelected ? tokens.colors.accent : tokens.colors.muted },
142
+ isSelected ? "▸" : " "
143
+ ),
144
+ item.icon ? React.createElement(Text, null, item.icon) : null,
145
+ React.createElement(
146
+ Text,
147
+ {
148
+ color: item.disabled
149
+ ? tokens.colors.muted
150
+ : isSelected
151
+ ? tokens.colors.fg
152
+ : tokens.colors.fg,
153
+ dimColor: item.disabled,
154
+ bold: isSelected,
155
+ },
156
+ item.label
157
+ ),
158
+ item.shortcut
159
+ ? React.createElement(
160
+ Box,
161
+ { flexGrow: 1, justifyContent: "flex-end" },
162
+ React.createElement(
163
+ Text,
164
+ { color: tokens.colors.muted, dimColor: true },
165
+ item.shortcut
166
+ )
167
+ )
168
+ : null
169
+ );
170
+ });
171
+
172
+ return React.createElement(
173
+ Box,
174
+ { flexDirection: "column" },
175
+ triggerElement,
176
+ React.createElement(
177
+ Box,
178
+ {
179
+ flexDirection: "column",
180
+ borderStyle: "round",
181
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
182
+ width,
183
+ paddingY: 0,
184
+ },
185
+ ...menuItems
186
+ )
187
+ );
188
+ }
@@ -0,0 +1,2 @@
1
+ export { DropdownMenu } from "./dropdown-menu.js";
2
+ export type { DropdownMenuProps, DropdownMenuItem } from "./dropdown-menu.js";
@@ -0,0 +1,107 @@
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 EmptyProps {
7
+ /** Main title/message */
8
+ title?: string;
9
+ /** Description text */
10
+ description?: string;
11
+ /** Icon or emoji to display */
12
+ icon?: string;
13
+ /** Action element (e.g., button) */
14
+ action?: React.ReactNode;
15
+ /** Custom tokens override */
16
+ tokens?: Tokens;
17
+ /** Size variant */
18
+ size?: "sm" | "md" | "lg";
19
+ }
20
+
21
+ const ICONS = {
22
+ empty: "📭",
23
+ search: "🔍",
24
+ error: "❌",
25
+ folder: "📁",
26
+ file: "📄",
27
+ inbox: "📥",
28
+ };
29
+
30
+ export function Empty({
31
+ title = "No data",
32
+ description,
33
+ icon = "📭",
34
+ action,
35
+ tokens: propTokens,
36
+ size = "md",
37
+ }: EmptyProps): React.ReactElement {
38
+ const contextTokens = useTokens();
39
+ const tokens = propTokens ?? contextTokens;
40
+
41
+ const sizeConfig = {
42
+ sm: { iconSize: 1, padding: 1 },
43
+ md: { iconSize: 2, padding: 2 },
44
+ lg: { iconSize: 3, padding: 3 },
45
+ };
46
+
47
+ const config = sizeConfig[size];
48
+
49
+ // ASCII art versions for larger sizes
50
+ const asciiIcons: Record<string, string[]> = {
51
+ "📭": ["┌─────────┐", "│ EMPTY │", "└────┬────┘", " │ "],
52
+ "🔍": [" ╭───╮ ", " │ │╲", " ╰───╯ ╲"],
53
+ "❌": ["╲ ╱", " ╲ ╱ ", " ╱ ╲ ", "╱ ╲"],
54
+ "📁": ["┌──┬────┐", "│ └────┤", "│ │", "└───────┘"],
55
+ };
56
+
57
+ return React.createElement(
58
+ Box,
59
+ {
60
+ flexDirection: "column",
61
+ alignItems: "center",
62
+ padding: config.padding,
63
+ },
64
+ // Icon
65
+ size === "lg" && asciiIcons[icon]
66
+ ? React.createElement(
67
+ Box,
68
+ { flexDirection: "column", marginBottom: 1 },
69
+ asciiIcons[icon]!.map((line, i) =>
70
+ React.createElement(
71
+ Text,
72
+ { key: i, color: tokens.colors.muted },
73
+ line
74
+ )
75
+ )
76
+ )
77
+ : React.createElement(
78
+ Text,
79
+ { color: tokens.colors.muted },
80
+ icon.repeat(size === "md" ? 1 : 1)
81
+ ),
82
+ // Title
83
+ React.createElement(
84
+ Box,
85
+ { marginTop: 1 },
86
+ React.createElement(
87
+ Text,
88
+ { color: tokens.colors.fg, bold: size !== "sm" },
89
+ title
90
+ )
91
+ ),
92
+ // Description
93
+ description
94
+ ? React.createElement(
95
+ Box,
96
+ { marginTop: size === "sm" ? 0 : 1 },
97
+ React.createElement(
98
+ Text,
99
+ { color: tokens.colors.muted, dimColor: true },
100
+ description
101
+ )
102
+ )
103
+ : null,
104
+ // Action
105
+ action ? React.createElement(Box, { marginTop: 1 }, action) : null
106
+ );
107
+ }
@@ -0,0 +1,2 @@
1
+ export { Empty } from "./empty.js";
2
+ export type { EmptyProps } from "./empty.js";
@@ -0,0 +1,83 @@
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 FieldProps {
7
+ /** Field label */
8
+ label: string;
9
+ /** Whether the field is required */
10
+ required?: boolean;
11
+ /** Error message */
12
+ error?: string;
13
+ /** Help text/description */
14
+ description?: string;
15
+ /** The input element */
16
+ children: React.ReactNode;
17
+ /** Custom tokens override */
18
+ tokens?: Tokens;
19
+ /** Label position */
20
+ labelPosition?: "top" | "left";
21
+ /** Label width (for left position) */
22
+ labelWidth?: number;
23
+ }
24
+
25
+ export function Field({
26
+ label,
27
+ required = false,
28
+ error,
29
+ description,
30
+ children,
31
+ tokens: propTokens,
32
+ labelPosition = "top",
33
+ labelWidth = 15,
34
+ }: FieldProps): React.ReactElement {
35
+ const contextTokens = useTokens();
36
+ const tokens = propTokens ?? contextTokens;
37
+
38
+ const labelElement = React.createElement(
39
+ Box,
40
+ {
41
+ width: labelPosition === "left" ? labelWidth : undefined,
42
+ marginBottom: labelPosition === "top" ? 0 : 0,
43
+ },
44
+ React.createElement(Text, { color: tokens.colors.fg, bold: true }, label),
45
+ required
46
+ ? React.createElement(Text, { color: tokens.colors.danger }, " *")
47
+ : null
48
+ );
49
+
50
+ const inputElement = React.createElement(
51
+ Box,
52
+ { flexDirection: "column", flexGrow: 1 },
53
+ children,
54
+ // Description
55
+ description && !error
56
+ ? React.createElement(
57
+ Text,
58
+ { color: tokens.colors.muted, dimColor: true },
59
+ description
60
+ )
61
+ : null,
62
+ // Error
63
+ error
64
+ ? React.createElement(Text, { color: tokens.colors.danger }, "⚠ ", error)
65
+ : null
66
+ );
67
+
68
+ if (labelPosition === "left") {
69
+ return React.createElement(
70
+ Box,
71
+ { gap: 1, alignItems: "flex-start" },
72
+ labelElement,
73
+ inputElement
74
+ );
75
+ }
76
+
77
+ return React.createElement(
78
+ Box,
79
+ { flexDirection: "column", gap: 0 },
80
+ labelElement,
81
+ inputElement
82
+ );
83
+ }
@@ -0,0 +1,2 @@
1
+ export { Field } from "./field.js";
2
+ export type { FieldProps } from "./field.js";