@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.
- package/components/accordion/accordion.tsx +146 -0
- package/components/accordion/index.ts +2 -0
- package/components/alert/alert.tsx +69 -0
- package/components/alert/index.ts +2 -0
- package/components/alert-dialog/alert-dialog.tsx +185 -0
- package/components/alert-dialog/index.ts +2 -0
- package/components/avatar/avatar.tsx +57 -0
- package/components/avatar/index.ts +2 -0
- package/components/avatar-group/avatar-group.tsx +144 -0
- package/components/avatar-group/index.ts +2 -0
- package/components/badge/badge.tsx +52 -0
- package/components/badge/index.ts +2 -0
- package/components/banner/banner.tsx +407 -0
- package/components/banner/index.ts +2 -0
- package/components/breadcrumb/breadcrumb.tsx +58 -0
- package/components/breadcrumb/index.ts +2 -0
- package/components/button/button.tsx +114 -0
- package/components/button/index.ts +2 -0
- package/components/calendar/calendar.tsx +250 -0
- package/components/calendar/index.ts +2 -0
- package/components/card/card.tsx +88 -0
- package/components/card/index.ts +2 -0
- package/components/carousel/carousel.tsx +185 -0
- package/components/carousel/index.ts +2 -0
- package/components/chart/chart.tsx +189 -0
- package/components/chart/index.ts +2 -0
- package/components/checkbox/checkbox.tsx +98 -0
- package/components/checkbox/index.ts +2 -0
- package/components/code-block/code-block.tsx +214 -0
- package/components/code-block/index.ts +2 -0
- package/components/collapsible/collapsible.tsx +123 -0
- package/components/collapsible/index.ts +2 -0
- package/components/color-picker/color-picker.tsx +211 -0
- package/components/color-picker/index.ts +2 -0
- package/components/combobox/combobox.tsx +275 -0
- package/components/combobox/index.ts +2 -0
- package/components/command/command.tsx +304 -0
- package/components/command/index.ts +2 -0
- package/components/confirm-dialog/confirm-dialog.tsx +140 -0
- package/components/confirm-dialog/index.ts +2 -0
- package/components/context-menu/context-menu.tsx +188 -0
- package/components/context-menu/index.ts +2 -0
- package/components/countdown/countdown.tsx +165 -0
- package/components/countdown/index.ts +2 -0
- package/components/data-table/data-table.tsx +256 -0
- package/components/data-table/index.ts +2 -0
- package/components/date-picker/date-picker.tsx +280 -0
- package/components/date-picker/index.ts +2 -0
- package/components/dialog/dialog.tsx +84 -0
- package/components/dialog/index.ts +2 -0
- package/components/drawer/drawer.tsx +141 -0
- package/components/drawer/index.ts +2 -0
- package/components/dropdown-menu/dropdown-menu.tsx +188 -0
- package/components/dropdown-menu/index.ts +2 -0
- package/components/empty/empty.tsx +107 -0
- package/components/empty/index.ts +2 -0
- package/components/field/field.tsx +83 -0
- package/components/field/index.ts +2 -0
- package/components/form/form.tsx +202 -0
- package/components/form/index.ts +8 -0
- package/components/hover-card/hover-card.tsx +72 -0
- package/components/hover-card/index.ts +2 -0
- package/components/input-otp/index.ts +2 -0
- package/components/input-otp/input-otp.tsx +176 -0
- package/components/kbd/index.ts +2 -0
- package/components/kbd/kbd.tsx +30 -0
- package/components/label/index.ts +2 -0
- package/components/label/label.tsx +56 -0
- package/components/list/index.ts +2 -0
- package/components/list/list.tsx +247 -0
- package/components/menubar/index.ts +2 -0
- package/components/menubar/menubar.tsx +220 -0
- package/components/navigation-menu/index.ts +6 -0
- package/components/navigation-menu/navigation-menu.tsx +216 -0
- package/components/pagination/index.ts +2 -0
- package/components/pagination/pagination.tsx +158 -0
- package/components/password-input/index.ts +2 -0
- package/components/password-input/password-input.tsx +198 -0
- package/components/popover/index.ts +2 -0
- package/components/popover/popover.tsx +102 -0
- package/components/progress/index.ts +2 -0
- package/components/progress/progress.tsx +73 -0
- package/components/radio-group/index.ts +2 -0
- package/components/radio-group/radio-group.tsx +167 -0
- package/components/resizable/index.ts +2 -0
- package/components/resizable/resizable.tsx +141 -0
- package/components/scroll-area/index.ts +2 -0
- package/components/scroll-area/scroll-area.tsx +133 -0
- package/components/select/index.ts +2 -0
- package/components/select/select.tsx +185 -0
- package/components/separator/index.ts +2 -0
- package/components/separator/separator.tsx +63 -0
- package/components/sheet/index.ts +2 -0
- package/components/sheet/sheet.tsx +137 -0
- package/components/sidebar/index.ts +2 -0
- package/components/sidebar/sidebar.tsx +225 -0
- package/components/skeleton/index.ts +2 -0
- package/components/skeleton/skeleton.tsx +64 -0
- package/components/slider/index.ts +2 -0
- package/components/slider/slider.tsx +128 -0
- package/components/spinner/index.ts +2 -0
- package/components/spinner/spinner.tsx +57 -0
- package/components/stat/index.ts +2 -0
- package/components/stat/stat.tsx +138 -0
- package/components/stepper/index.ts +2 -0
- package/components/stepper/stepper.tsx +219 -0
- package/components/switch/index.ts +2 -0
- package/components/switch/switch.tsx +102 -0
- package/components/table/index.ts +2 -0
- package/components/table/table.tsx +242 -0
- package/components/tabs/index.ts +2 -0
- package/components/tabs/tabs.tsx +240 -0
- package/components/tag-input/index.ts +2 -0
- package/components/tag-input/tag-input.tsx +180 -0
- package/components/terminal/index.ts +2 -0
- package/components/terminal/terminal.tsx +162 -0
- package/components/text-input/index.ts +2 -0
- package/components/text-input/text-input.tsx +179 -0
- package/components/textarea/index.ts +2 -0
- package/components/textarea/textarea.tsx +206 -0
- package/components/timeline/index.ts +2 -0
- package/components/timeline/timeline.tsx +167 -0
- package/components/toast/index.ts +2 -0
- package/components/toast/toast.tsx +93 -0
- package/components/toggle/index.ts +2 -0
- package/components/toggle/toggle.tsx +114 -0
- package/components/toggle-group/index.ts +2 -0
- package/components/toggle-group/toggle-group.tsx +176 -0
- package/components/tooltip/index.ts +2 -0
- package/components/tooltip/tooltip.tsx +65 -0
- package/components/tree-view/index.ts +2 -0
- package/components/tree-view/tree-view.tsx +245 -0
- package/components/typography/index.ts +12 -0
- package/components/typography/typography.tsx +154 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +938 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/registry.json +923 -0
|
@@ -0,0 +1,158 @@
|
|
|
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 PaginationProps {
|
|
8
|
+
/** Current page (1-indexed) */
|
|
9
|
+
page: number;
|
|
10
|
+
/** Total number of pages */
|
|
11
|
+
totalPages: number;
|
|
12
|
+
/** Callback when page changes */
|
|
13
|
+
onChange: (page: number) => void;
|
|
14
|
+
/** Whether the pagination is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Custom tokens override */
|
|
17
|
+
tokens?: Tokens;
|
|
18
|
+
/** Focus ID for focus management */
|
|
19
|
+
focusId?: string;
|
|
20
|
+
/** Number of page buttons to show */
|
|
21
|
+
siblingCount?: number;
|
|
22
|
+
/** Show first/last buttons */
|
|
23
|
+
showBoundaryButtons?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Pagination({
|
|
27
|
+
page,
|
|
28
|
+
totalPages,
|
|
29
|
+
onChange,
|
|
30
|
+
disabled = false,
|
|
31
|
+
tokens: propTokens,
|
|
32
|
+
focusId,
|
|
33
|
+
siblingCount = 1,
|
|
34
|
+
showBoundaryButtons = true,
|
|
35
|
+
}: PaginationProps): React.ReactElement {
|
|
36
|
+
const contextTokens = useTokens();
|
|
37
|
+
const tokens = propTokens ?? contextTokens;
|
|
38
|
+
const id = focusId ?? stableId("pagination");
|
|
39
|
+
const { isFocused } = useFocusable(id);
|
|
40
|
+
|
|
41
|
+
const goToPage = useCallback(
|
|
42
|
+
(newPage: number) => {
|
|
43
|
+
const clampedPage = clamp(newPage, 1, totalPages);
|
|
44
|
+
if (clampedPage !== page) {
|
|
45
|
+
onChange(clampedPage);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[page, totalPages, onChange]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
useInput(
|
|
52
|
+
(input, key) => {
|
|
53
|
+
if (!isFocused || disabled) return;
|
|
54
|
+
|
|
55
|
+
if (key.leftArrow || input === "h") {
|
|
56
|
+
goToPage(page - 1);
|
|
57
|
+
} else if (key.rightArrow || input === "l") {
|
|
58
|
+
goToPage(page + 1);
|
|
59
|
+
} else if (input === "g" && !key.shift) {
|
|
60
|
+
goToPage(1);
|
|
61
|
+
} else if (input === "G") {
|
|
62
|
+
goToPage(totalPages);
|
|
63
|
+
} else {
|
|
64
|
+
const num = parseInt(input, 10);
|
|
65
|
+
if (num >= 1 && num <= 9 && num <= totalPages) {
|
|
66
|
+
goToPage(num);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{ isActive: isFocused }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Calculate visible page numbers
|
|
74
|
+
const getPageNumbers = (): (number | "...")[] => {
|
|
75
|
+
const pages: (number | "...")[] = [];
|
|
76
|
+
|
|
77
|
+
const leftSibling = Math.max(page - siblingCount, 1);
|
|
78
|
+
const rightSibling = Math.min(page + siblingCount, totalPages);
|
|
79
|
+
|
|
80
|
+
const showLeftEllipsis = leftSibling > 2;
|
|
81
|
+
const showRightEllipsis = rightSibling < totalPages - 1;
|
|
82
|
+
|
|
83
|
+
if (showBoundaryButtons && leftSibling > 1) {
|
|
84
|
+
pages.push(1);
|
|
85
|
+
if (showLeftEllipsis) pages.push("...");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let i = leftSibling; i <= rightSibling; i++) {
|
|
89
|
+
pages.push(i);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (showBoundaryButtons && rightSibling < totalPages) {
|
|
93
|
+
if (showRightEllipsis) pages.push("...");
|
|
94
|
+
pages.push(totalPages);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return pages;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const pageNumbers = getPageNumbers();
|
|
101
|
+
|
|
102
|
+
return React.createElement(
|
|
103
|
+
Box,
|
|
104
|
+
{
|
|
105
|
+
gap: 1,
|
|
106
|
+
borderStyle: isFocused ? "round" : "single",
|
|
107
|
+
borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
|
|
108
|
+
paddingX: 1,
|
|
109
|
+
},
|
|
110
|
+
// Previous button
|
|
111
|
+
React.createElement(
|
|
112
|
+
Text,
|
|
113
|
+
{
|
|
114
|
+
color: page > 1 ? tokens.colors.accent : tokens.colors.disabled,
|
|
115
|
+
dimColor: page <= 1,
|
|
116
|
+
},
|
|
117
|
+
"←"
|
|
118
|
+
),
|
|
119
|
+
// Page numbers
|
|
120
|
+
...pageNumbers.map((pageNum, index) => {
|
|
121
|
+
if (pageNum === "...") {
|
|
122
|
+
return React.createElement(
|
|
123
|
+
Text,
|
|
124
|
+
{ key: `ellipsis-${index}`, color: tokens.colors.muted },
|
|
125
|
+
"..."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const isCurrent = pageNum === page;
|
|
130
|
+
return React.createElement(
|
|
131
|
+
Text,
|
|
132
|
+
{
|
|
133
|
+
key: pageNum,
|
|
134
|
+
color: isCurrent ? tokens.colors.accent : tokens.colors.fg,
|
|
135
|
+
bold: isCurrent,
|
|
136
|
+
inverse: isCurrent && isFocused,
|
|
137
|
+
},
|
|
138
|
+
String(pageNum)
|
|
139
|
+
);
|
|
140
|
+
}),
|
|
141
|
+
// Next button
|
|
142
|
+
React.createElement(
|
|
143
|
+
Text,
|
|
144
|
+
{
|
|
145
|
+
color:
|
|
146
|
+
page < totalPages ? tokens.colors.accent : tokens.colors.disabled,
|
|
147
|
+
dimColor: page >= totalPages,
|
|
148
|
+
},
|
|
149
|
+
"→"
|
|
150
|
+
),
|
|
151
|
+
// Page indicator
|
|
152
|
+
React.createElement(
|
|
153
|
+
Text,
|
|
154
|
+
{ color: tokens.colors.muted, dimColor: true },
|
|
155
|
+
`(${page}/${totalPages})`
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
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 PasswordInputProps {
|
|
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 (default: "*") */
|
|
29
|
+
mask?: string;
|
|
30
|
+
/** Show password reveal hint */
|
|
31
|
+
showHint?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function PasswordInput({
|
|
35
|
+
value: controlledValue,
|
|
36
|
+
defaultValue = "",
|
|
37
|
+
onChange,
|
|
38
|
+
onSubmit,
|
|
39
|
+
placeholder = "Enter password",
|
|
40
|
+
label,
|
|
41
|
+
disabled = false,
|
|
42
|
+
maxLength,
|
|
43
|
+
tokens: propTokens,
|
|
44
|
+
focusId,
|
|
45
|
+
mask = "*",
|
|
46
|
+
showHint = true,
|
|
47
|
+
}: PasswordInputProps): React.ReactElement {
|
|
48
|
+
const contextTokens = useTokens();
|
|
49
|
+
const tokens = propTokens ?? contextTokens;
|
|
50
|
+
const id = focusId ?? stableId("password-input");
|
|
51
|
+
const { isFocused } = useFocusable(id);
|
|
52
|
+
|
|
53
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
54
|
+
const [cursorPosition, setCursorPosition] = useState(defaultValue.length);
|
|
55
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
56
|
+
|
|
57
|
+
const isControlled = controlledValue !== undefined;
|
|
58
|
+
const currentValue = isControlled ? controlledValue : internalValue;
|
|
59
|
+
|
|
60
|
+
const updateValue = useCallback(
|
|
61
|
+
(newValue: string) => {
|
|
62
|
+
if (maxLength !== undefined && newValue.length > maxLength) {
|
|
63
|
+
newValue = newValue.slice(0, maxLength);
|
|
64
|
+
}
|
|
65
|
+
if (!isControlled) {
|
|
66
|
+
setInternalValue(newValue);
|
|
67
|
+
}
|
|
68
|
+
onChange?.(newValue);
|
|
69
|
+
},
|
|
70
|
+
[isControlled, maxLength, onChange]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
useInput(
|
|
74
|
+
(input, key) => {
|
|
75
|
+
if (!isFocused || disabled) return;
|
|
76
|
+
|
|
77
|
+
if (key.return) {
|
|
78
|
+
onSubmit?.(currentValue);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Toggle password visibility with Ctrl+R
|
|
83
|
+
if (key.ctrl && input === "r") {
|
|
84
|
+
setShowPassword((prev) => !prev);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (key.backspace || key.delete) {
|
|
89
|
+
if (cursorPosition > 0) {
|
|
90
|
+
const newValue =
|
|
91
|
+
currentValue.slice(0, cursorPosition - 1) +
|
|
92
|
+
currentValue.slice(cursorPosition);
|
|
93
|
+
updateValue(newValue);
|
|
94
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (key.leftArrow) {
|
|
100
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (key.rightArrow) {
|
|
105
|
+
setCursorPosition(Math.min(currentValue.length, cursorPosition + 1));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Home key
|
|
110
|
+
if (key.ctrl && input === "a") {
|
|
111
|
+
setCursorPosition(0);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// End key
|
|
116
|
+
if (key.ctrl && input === "e") {
|
|
117
|
+
setCursorPosition(currentValue.length);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Regular character input
|
|
122
|
+
if (input && !key.ctrl && !key.meta) {
|
|
123
|
+
const newValue =
|
|
124
|
+
currentValue.slice(0, cursorPosition) +
|
|
125
|
+
input +
|
|
126
|
+
currentValue.slice(cursorPosition);
|
|
127
|
+
updateValue(newValue);
|
|
128
|
+
setCursorPosition(cursorPosition + input.length);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{ isActive: isFocused }
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Display value (masked or plain)
|
|
135
|
+
const displayValue = showPassword
|
|
136
|
+
? currentValue
|
|
137
|
+
: mask.repeat(currentValue.length);
|
|
138
|
+
|
|
139
|
+
// Render with cursor
|
|
140
|
+
const renderValue = () => {
|
|
141
|
+
if (!isFocused) {
|
|
142
|
+
if (displayValue.length === 0) {
|
|
143
|
+
return React.createElement(Text, { color: tokens.colors.muted }, placeholder);
|
|
144
|
+
}
|
|
145
|
+
return React.createElement(Text, { color: tokens.colors.fg }, displayValue);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const before = displayValue.slice(0, cursorPosition);
|
|
149
|
+
const cursor = displayValue[cursorPosition] ?? " ";
|
|
150
|
+
const after = displayValue.slice(cursorPosition + 1);
|
|
151
|
+
|
|
152
|
+
return React.createElement(
|
|
153
|
+
Text,
|
|
154
|
+
null,
|
|
155
|
+
React.createElement(Text, { color: tokens.colors.fg }, before),
|
|
156
|
+
React.createElement(
|
|
157
|
+
Text,
|
|
158
|
+
{ backgroundColor: tokens.colors.accent, color: tokens.colors.bg },
|
|
159
|
+
cursor
|
|
160
|
+
),
|
|
161
|
+
React.createElement(Text, { color: tokens.colors.fg }, after)
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return React.createElement(
|
|
166
|
+
Box,
|
|
167
|
+
{ flexDirection: "column" },
|
|
168
|
+
label
|
|
169
|
+
? React.createElement(
|
|
170
|
+
Text,
|
|
171
|
+
{ color: isFocused ? tokens.colors.accent : tokens.colors.muted },
|
|
172
|
+
label
|
|
173
|
+
)
|
|
174
|
+
: null,
|
|
175
|
+
React.createElement(
|
|
176
|
+
Box,
|
|
177
|
+
{
|
|
178
|
+
borderStyle: "round",
|
|
179
|
+
borderColor: disabled
|
|
180
|
+
? tokens.colors.disabled
|
|
181
|
+
: isFocused
|
|
182
|
+
? tokens.colors.focus
|
|
183
|
+
: tokens.colors.border,
|
|
184
|
+
paddingX: 1,
|
|
185
|
+
},
|
|
186
|
+
React.createElement(Text, { color: tokens.colors.muted }, showPassword ? "* " : " "),
|
|
187
|
+
renderValue()
|
|
188
|
+
),
|
|
189
|
+
showHint && isFocused
|
|
190
|
+
? React.createElement(
|
|
191
|
+
Text,
|
|
192
|
+
{ color: tokens.colors.muted, dimColor: true },
|
|
193
|
+
"Ctrl+R to ",
|
|
194
|
+
showPassword ? "hide" : "reveal"
|
|
195
|
+
)
|
|
196
|
+
: null
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } 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 PopoverProps {
|
|
8
|
+
/** Content to display in the popover */
|
|
9
|
+
content: React.ReactNode;
|
|
10
|
+
/** Trigger element */
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
/** Whether the popover is open */
|
|
13
|
+
open: boolean;
|
|
14
|
+
/** Position relative to trigger */
|
|
15
|
+
position?: "top" | "bottom" | "left" | "right";
|
|
16
|
+
/** Popover width */
|
|
17
|
+
width?: number;
|
|
18
|
+
/** Custom tokens override */
|
|
19
|
+
tokens?: Tokens;
|
|
20
|
+
/** Focus ID for focus management */
|
|
21
|
+
focusId?: string;
|
|
22
|
+
/** Optional title */
|
|
23
|
+
title?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Popover({
|
|
27
|
+
content,
|
|
28
|
+
children,
|
|
29
|
+
open,
|
|
30
|
+
position = "bottom",
|
|
31
|
+
width = 30,
|
|
32
|
+
tokens: propTokens,
|
|
33
|
+
focusId,
|
|
34
|
+
title,
|
|
35
|
+
}: PopoverProps): React.ReactElement {
|
|
36
|
+
const contextTokens = useTokens();
|
|
37
|
+
const tokens = propTokens ?? contextTokens;
|
|
38
|
+
const id = focusId ?? stableId("popover");
|
|
39
|
+
const { isFocused } = useFocusable(id);
|
|
40
|
+
|
|
41
|
+
const popoverContent = React.createElement(
|
|
42
|
+
Box,
|
|
43
|
+
{
|
|
44
|
+
flexDirection: "column",
|
|
45
|
+
borderStyle: "round",
|
|
46
|
+
borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
|
|
47
|
+
width,
|
|
48
|
+
paddingX: 1,
|
|
49
|
+
paddingY: 0,
|
|
50
|
+
},
|
|
51
|
+
title
|
|
52
|
+
? React.createElement(
|
|
53
|
+
Box,
|
|
54
|
+
{ marginBottom: 0 },
|
|
55
|
+
React.createElement(
|
|
56
|
+
Text,
|
|
57
|
+
{ bold: true, color: tokens.colors.fg },
|
|
58
|
+
title
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
: null,
|
|
62
|
+
title
|
|
63
|
+
? React.createElement(
|
|
64
|
+
Box,
|
|
65
|
+
{ marginBottom: 0 },
|
|
66
|
+
React.createElement(
|
|
67
|
+
Text,
|
|
68
|
+
{ color: tokens.colors.border },
|
|
69
|
+
"─".repeat(width - 4)
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
: null,
|
|
73
|
+
React.createElement(Box, { flexDirection: "column" }, content)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!open) {
|
|
77
|
+
return React.createElement(Box, null, children);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isVertical = position === "top" || position === "bottom";
|
|
81
|
+
|
|
82
|
+
const containerProps: Record<string, unknown> = {
|
|
83
|
+
flexDirection: isVertical ? "column" : "row",
|
|
84
|
+
alignItems: isVertical ? "flex-start" : "center",
|
|
85
|
+
gap: 0,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (position === "top") {
|
|
89
|
+
return React.createElement(Box, containerProps, popoverContent, children);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (position === "bottom") {
|
|
93
|
+
return React.createElement(Box, containerProps, children, popoverContent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (position === "left") {
|
|
97
|
+
return React.createElement(Box, containerProps, popoverContent, children);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// right
|
|
101
|
+
return React.createElement(Box, containerProps, children, popoverContent);
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 ProgressProps {
|
|
7
|
+
/** Progress value (0-100) */
|
|
8
|
+
value: number;
|
|
9
|
+
/** Progress bar width in characters */
|
|
10
|
+
width?: number;
|
|
11
|
+
/** Show percentage label */
|
|
12
|
+
showLabel?: boolean;
|
|
13
|
+
/** Label position */
|
|
14
|
+
labelPosition?: "left" | "right" | "inside";
|
|
15
|
+
/** Custom tokens override */
|
|
16
|
+
tokens?: Tokens;
|
|
17
|
+
/** Fill character */
|
|
18
|
+
fillChar?: string;
|
|
19
|
+
/** Empty character */
|
|
20
|
+
emptyChar?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Progress({
|
|
24
|
+
value,
|
|
25
|
+
width = 20,
|
|
26
|
+
showLabel = true,
|
|
27
|
+
labelPosition = "right",
|
|
28
|
+
tokens: propTokens,
|
|
29
|
+
fillChar = "█",
|
|
30
|
+
emptyChar = "░",
|
|
31
|
+
}: ProgressProps): React.ReactElement {
|
|
32
|
+
const contextTokens = useTokens();
|
|
33
|
+
const tokens = propTokens ?? contextTokens;
|
|
34
|
+
|
|
35
|
+
// Clamp value between 0 and 100
|
|
36
|
+
const clampedValue = Math.max(0, Math.min(100, value));
|
|
37
|
+
const filled = Math.round((clampedValue / 100) * width);
|
|
38
|
+
const empty = width - filled;
|
|
39
|
+
|
|
40
|
+
const bar = fillChar.repeat(filled) + emptyChar.repeat(empty);
|
|
41
|
+
const label = `${Math.round(clampedValue)}%`;
|
|
42
|
+
|
|
43
|
+
// Determine color based on progress
|
|
44
|
+
let color = tokens.colors.accent;
|
|
45
|
+
if (clampedValue >= 100) {
|
|
46
|
+
color = tokens.colors.success;
|
|
47
|
+
} else if (clampedValue < 30) {
|
|
48
|
+
color = tokens.colors.warning;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const labelElement = showLabel
|
|
52
|
+
? React.createElement(Text, { color: tokens.colors.muted }, label)
|
|
53
|
+
: null;
|
|
54
|
+
|
|
55
|
+
if (labelPosition === "inside") {
|
|
56
|
+
return React.createElement(
|
|
57
|
+
Box,
|
|
58
|
+
null,
|
|
59
|
+
React.createElement(Text, { color }, "["),
|
|
60
|
+
React.createElement(Text, { color }, bar),
|
|
61
|
+
React.createElement(Text, { color }, "] "),
|
|
62
|
+
labelElement
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return React.createElement(
|
|
67
|
+
Box,
|
|
68
|
+
{ gap: 1 },
|
|
69
|
+
labelPosition === "left" ? labelElement : null,
|
|
70
|
+
React.createElement(Text, { color }, bar),
|
|
71
|
+
labelPosition === "right" ? labelElement : null
|
|
72
|
+
);
|
|
73
|
+
}
|