@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,138 @@
|
|
|
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 StatProps {
|
|
7
|
+
/** Stat label */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Stat value */
|
|
10
|
+
value: string | number;
|
|
11
|
+
/** Optional delta/change indicator */
|
|
12
|
+
delta?: {
|
|
13
|
+
value: string | number;
|
|
14
|
+
type: "increase" | "decrease" | "neutral";
|
|
15
|
+
};
|
|
16
|
+
/** Optional icon */
|
|
17
|
+
icon?: string;
|
|
18
|
+
/** Size variant */
|
|
19
|
+
size?: "sm" | "md" | "lg";
|
|
20
|
+
/** Custom tokens override */
|
|
21
|
+
tokens?: Tokens;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Stat({
|
|
25
|
+
label,
|
|
26
|
+
value,
|
|
27
|
+
delta,
|
|
28
|
+
icon,
|
|
29
|
+
size = "md",
|
|
30
|
+
tokens: propTokens,
|
|
31
|
+
}: StatProps): React.ReactElement {
|
|
32
|
+
const contextTokens = useTokens();
|
|
33
|
+
const tokens = propTokens ?? contextTokens;
|
|
34
|
+
|
|
35
|
+
// Get delta color and symbol
|
|
36
|
+
const getDeltaStyle = () => {
|
|
37
|
+
if (!delta) return null;
|
|
38
|
+
|
|
39
|
+
switch (delta.type) {
|
|
40
|
+
case "increase":
|
|
41
|
+
return { color: tokens.colors.success, symbol: "↑" };
|
|
42
|
+
case "decrease":
|
|
43
|
+
return { color: tokens.colors.danger, symbol: "↓" };
|
|
44
|
+
default:
|
|
45
|
+
return { color: tokens.colors.muted, symbol: "→" };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const deltaStyle = getDeltaStyle();
|
|
50
|
+
|
|
51
|
+
// Get value size based on size prop
|
|
52
|
+
const getValueSize = () => {
|
|
53
|
+
switch (size) {
|
|
54
|
+
case "sm":
|
|
55
|
+
return false;
|
|
56
|
+
case "lg":
|
|
57
|
+
return true;
|
|
58
|
+
default:
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return React.createElement(
|
|
64
|
+
Box,
|
|
65
|
+
{ flexDirection: "column" },
|
|
66
|
+
// Label with optional icon
|
|
67
|
+
React.createElement(
|
|
68
|
+
Box,
|
|
69
|
+
{ gap: 1 },
|
|
70
|
+
icon && React.createElement(Text, {}, icon),
|
|
71
|
+
React.createElement(Text, { color: tokens.colors.muted }, label)
|
|
72
|
+
),
|
|
73
|
+
// Value
|
|
74
|
+
React.createElement(
|
|
75
|
+
Box,
|
|
76
|
+
{ gap: 1, alignItems: "baseline" },
|
|
77
|
+
React.createElement(
|
|
78
|
+
Text,
|
|
79
|
+
{
|
|
80
|
+
bold: getValueSize(),
|
|
81
|
+
color: tokens.colors.fg,
|
|
82
|
+
},
|
|
83
|
+
String(value)
|
|
84
|
+
),
|
|
85
|
+
// Delta indicator
|
|
86
|
+
delta &&
|
|
87
|
+
deltaStyle &&
|
|
88
|
+
React.createElement(
|
|
89
|
+
Text,
|
|
90
|
+
{ color: deltaStyle.color },
|
|
91
|
+
`${deltaStyle.symbol} ${delta.value}`
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface StatGroupProps {
|
|
98
|
+
/** Stats to display */
|
|
99
|
+
stats: StatProps[];
|
|
100
|
+
/** Layout direction */
|
|
101
|
+
direction?: "horizontal" | "vertical";
|
|
102
|
+
/** Show dividers between stats */
|
|
103
|
+
showDividers?: boolean;
|
|
104
|
+
/** Custom tokens override */
|
|
105
|
+
tokens?: Tokens;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function StatGroup({
|
|
109
|
+
stats,
|
|
110
|
+
direction = "horizontal",
|
|
111
|
+
showDividers = true,
|
|
112
|
+
tokens: propTokens,
|
|
113
|
+
}: StatGroupProps): React.ReactElement {
|
|
114
|
+
const contextTokens = useTokens();
|
|
115
|
+
const tokens = propTokens ?? contextTokens;
|
|
116
|
+
|
|
117
|
+
return React.createElement(
|
|
118
|
+
Box,
|
|
119
|
+
{
|
|
120
|
+
flexDirection: direction === "horizontal" ? "row" : "column",
|
|
121
|
+
gap: direction === "horizontal" ? 3 : 1,
|
|
122
|
+
},
|
|
123
|
+
stats.map((stat, index) =>
|
|
124
|
+
React.createElement(
|
|
125
|
+
React.Fragment,
|
|
126
|
+
{ key: index },
|
|
127
|
+
index > 0 &&
|
|
128
|
+
showDividers &&
|
|
129
|
+
React.createElement(
|
|
130
|
+
Text,
|
|
131
|
+
{ color: tokens.colors.border },
|
|
132
|
+
direction === "horizontal" ? "│" : "───────────"
|
|
133
|
+
),
|
|
134
|
+
React.createElement(Stat, { ...stat, tokens })
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import type { Tokens } from "@hauktui/tokens";
|
|
4
|
+
import { useTokens, useFocusable } from "@hauktui/primitives-ink";
|
|
5
|
+
import { stableId } from "@hauktui/core";
|
|
6
|
+
|
|
7
|
+
export interface Step {
|
|
8
|
+
/** Step title */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Step description */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Optional icon */
|
|
13
|
+
icon?: string;
|
|
14
|
+
/** Whether this step is optional */
|
|
15
|
+
optional?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StepperProps {
|
|
19
|
+
/** Steps to display */
|
|
20
|
+
steps: Step[];
|
|
21
|
+
/** Current active step (0-indexed) */
|
|
22
|
+
activeStep: number;
|
|
23
|
+
/** Callback when step changes */
|
|
24
|
+
onStepChange?: (step: number) => void;
|
|
25
|
+
/** Orientation */
|
|
26
|
+
orientation?: "horizontal" | "vertical";
|
|
27
|
+
/** Allow clicking on steps to navigate */
|
|
28
|
+
clickable?: boolean;
|
|
29
|
+
/** Custom tokens override */
|
|
30
|
+
tokens?: Tokens;
|
|
31
|
+
/** Focus ID for focus management */
|
|
32
|
+
focusId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Stepper({
|
|
36
|
+
steps,
|
|
37
|
+
activeStep,
|
|
38
|
+
onStepChange,
|
|
39
|
+
orientation = "horizontal",
|
|
40
|
+
clickable = false,
|
|
41
|
+
tokens: propTokens,
|
|
42
|
+
focusId,
|
|
43
|
+
}: StepperProps): React.ReactElement {
|
|
44
|
+
const contextTokens = useTokens();
|
|
45
|
+
const tokens = propTokens ?? contextTokens;
|
|
46
|
+
const id = focusId ?? stableId("stepper");
|
|
47
|
+
const { isFocused } = useFocusable(id);
|
|
48
|
+
|
|
49
|
+
const [focusedStep, setFocusedStep] = useState(activeStep);
|
|
50
|
+
|
|
51
|
+
// Handle keyboard input
|
|
52
|
+
useInput(
|
|
53
|
+
(input, key) => {
|
|
54
|
+
if (!isFocused || !clickable) return;
|
|
55
|
+
|
|
56
|
+
if (orientation === "horizontal") {
|
|
57
|
+
if (key.leftArrow || input === "h") {
|
|
58
|
+
setFocusedStep((prev) => Math.max(0, prev - 1));
|
|
59
|
+
} else if (key.rightArrow || input === "l") {
|
|
60
|
+
setFocusedStep((prev) => Math.min(steps.length - 1, prev + 1));
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
if (key.upArrow || input === "k") {
|
|
64
|
+
setFocusedStep((prev) => Math.max(0, prev - 1));
|
|
65
|
+
} else if (key.downArrow || input === "j") {
|
|
66
|
+
setFocusedStep((prev) => Math.min(steps.length - 1, prev + 1));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (key.return || input === " ") {
|
|
71
|
+
onStepChange?.(focusedStep);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{ isActive: isFocused && clickable }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Get step status
|
|
78
|
+
const getStepStatus = (
|
|
79
|
+
index: number
|
|
80
|
+
): "completed" | "active" | "upcoming" => {
|
|
81
|
+
if (index < activeStep) return "completed";
|
|
82
|
+
if (index === activeStep) return "active";
|
|
83
|
+
return "upcoming";
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Get step icon
|
|
87
|
+
const getStepIcon = (step: Step, index: number): string => {
|
|
88
|
+
if (step.icon) return step.icon;
|
|
89
|
+
const status = getStepStatus(index);
|
|
90
|
+
if (status === "completed") return "✓";
|
|
91
|
+
return String(index + 1);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Get step colors
|
|
95
|
+
const getStepColors = (
|
|
96
|
+
index: number,
|
|
97
|
+
isFocusedItem: boolean
|
|
98
|
+
): { bg: string; fg: string; text: string } => {
|
|
99
|
+
const status = getStepStatus(index);
|
|
100
|
+
|
|
101
|
+
if (isFocusedItem && clickable) {
|
|
102
|
+
return {
|
|
103
|
+
bg: tokens.colors.accent,
|
|
104
|
+
fg: tokens.colors.bg,
|
|
105
|
+
text: tokens.colors.accent,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
switch (status) {
|
|
110
|
+
case "completed":
|
|
111
|
+
return {
|
|
112
|
+
bg: tokens.colors.success,
|
|
113
|
+
fg: tokens.colors.bg,
|
|
114
|
+
text: tokens.colors.success,
|
|
115
|
+
};
|
|
116
|
+
case "active":
|
|
117
|
+
return {
|
|
118
|
+
bg: tokens.colors.accent,
|
|
119
|
+
fg: tokens.colors.bg,
|
|
120
|
+
text: tokens.colors.fg,
|
|
121
|
+
};
|
|
122
|
+
default:
|
|
123
|
+
return {
|
|
124
|
+
bg: tokens.colors.border,
|
|
125
|
+
fg: tokens.colors.muted,
|
|
126
|
+
text: tokens.colors.muted,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Render step
|
|
132
|
+
const renderStep = (step: Step, index: number) => {
|
|
133
|
+
const status = getStepStatus(index);
|
|
134
|
+
const isFocusedItem = isFocused && clickable && index === focusedStep;
|
|
135
|
+
const colors = getStepColors(index, isFocusedItem);
|
|
136
|
+
const icon = getStepIcon(step, index);
|
|
137
|
+
const isLast = index === steps.length - 1;
|
|
138
|
+
|
|
139
|
+
return React.createElement(
|
|
140
|
+
Box,
|
|
141
|
+
{
|
|
142
|
+
key: index,
|
|
143
|
+
flexDirection: orientation === "horizontal" ? "column" : "row",
|
|
144
|
+
alignItems: orientation === "horizontal" ? "center" : "flex-start",
|
|
145
|
+
gap: 1,
|
|
146
|
+
},
|
|
147
|
+
// Step indicator
|
|
148
|
+
React.createElement(
|
|
149
|
+
Box,
|
|
150
|
+
{
|
|
151
|
+
flexDirection: orientation === "horizontal" ? "row" : "column",
|
|
152
|
+
alignItems: "center",
|
|
153
|
+
},
|
|
154
|
+
// Step circle
|
|
155
|
+
React.createElement(
|
|
156
|
+
Text,
|
|
157
|
+
{
|
|
158
|
+
backgroundColor: colors.bg,
|
|
159
|
+
color: colors.fg,
|
|
160
|
+
bold: status === "active",
|
|
161
|
+
},
|
|
162
|
+
` ${icon} `
|
|
163
|
+
),
|
|
164
|
+
// Connector line
|
|
165
|
+
!isLast &&
|
|
166
|
+
React.createElement(
|
|
167
|
+
Text,
|
|
168
|
+
{
|
|
169
|
+
color:
|
|
170
|
+
status === "completed"
|
|
171
|
+
? tokens.colors.success
|
|
172
|
+
: tokens.colors.border,
|
|
173
|
+
},
|
|
174
|
+
orientation === "horizontal" ? "───" : "\n│\n│"
|
|
175
|
+
)
|
|
176
|
+
),
|
|
177
|
+
// Step content
|
|
178
|
+
React.createElement(
|
|
179
|
+
Box,
|
|
180
|
+
{
|
|
181
|
+
flexDirection: "column",
|
|
182
|
+
alignItems: orientation === "horizontal" ? "center" : "flex-start",
|
|
183
|
+
},
|
|
184
|
+
React.createElement(
|
|
185
|
+
Text,
|
|
186
|
+
{
|
|
187
|
+
color: colors.text,
|
|
188
|
+
bold: status === "active",
|
|
189
|
+
},
|
|
190
|
+
step.title
|
|
191
|
+
),
|
|
192
|
+
step.description &&
|
|
193
|
+
React.createElement(
|
|
194
|
+
Text,
|
|
195
|
+
{ color: tokens.colors.muted, dimColor: true },
|
|
196
|
+
step.description
|
|
197
|
+
),
|
|
198
|
+
step.optional &&
|
|
199
|
+
React.createElement(
|
|
200
|
+
Text,
|
|
201
|
+
{ color: tokens.colors.muted, italic: true },
|
|
202
|
+
"(Optional)"
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return React.createElement(
|
|
209
|
+
Box,
|
|
210
|
+
{
|
|
211
|
+
flexDirection: orientation === "horizontal" ? "row" : "column",
|
|
212
|
+
borderStyle: isFocused && clickable ? "round" : undefined,
|
|
213
|
+
borderColor: isFocused ? tokens.colors.focus : undefined,
|
|
214
|
+
padding: isFocused && clickable ? 1 : 0,
|
|
215
|
+
gap: orientation === "horizontal" ? 0 : 1,
|
|
216
|
+
},
|
|
217
|
+
steps.map(renderStep)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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 interface SwitchProps {
|
|
8
|
+
/** Whether the switch is on */
|
|
9
|
+
checked: boolean;
|
|
10
|
+
/** Callback when switch is toggled */
|
|
11
|
+
onCheckedChange: (checked: boolean) => void;
|
|
12
|
+
/** Label for the switch */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Whether the switch is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Size of the switch */
|
|
17
|
+
size?: "sm" | "md" | "lg";
|
|
18
|
+
/** Custom tokens override */
|
|
19
|
+
tokens?: Tokens;
|
|
20
|
+
/** Focus ID for focus management */
|
|
21
|
+
focusId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Switch({
|
|
25
|
+
checked,
|
|
26
|
+
onCheckedChange,
|
|
27
|
+
label,
|
|
28
|
+
disabled = false,
|
|
29
|
+
size = "md",
|
|
30
|
+
tokens: propTokens,
|
|
31
|
+
focusId,
|
|
32
|
+
}: SwitchProps): React.ReactElement {
|
|
33
|
+
const contextTokens = useTokens();
|
|
34
|
+
const tokens = propTokens ?? contextTokens;
|
|
35
|
+
const id = focusId ?? stableId("switch");
|
|
36
|
+
const { isFocused } = useFocusable(id);
|
|
37
|
+
|
|
38
|
+
useInput(
|
|
39
|
+
(input, key) => {
|
|
40
|
+
if (disabled) return;
|
|
41
|
+
if (input === " " || key.return) {
|
|
42
|
+
onCheckedChange(!checked);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{ isActive: isFocused }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const sizes = {
|
|
49
|
+
sm: { width: 4, thumb: 1 },
|
|
50
|
+
md: { width: 6, thumb: 2 },
|
|
51
|
+
lg: { width: 8, thumb: 3 },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const { width, thumb } = sizes[size];
|
|
55
|
+
|
|
56
|
+
const trackColor = checked ? tokens.colors.accent : tokens.colors.muted;
|
|
57
|
+
|
|
58
|
+
const thumbPosition = checked ? width - thumb - 2 : 0;
|
|
59
|
+
|
|
60
|
+
const track = React.createElement(
|
|
61
|
+
Box,
|
|
62
|
+
{
|
|
63
|
+
borderStyle: "round",
|
|
64
|
+
borderColor: isFocused ? tokens.colors.focus : trackColor,
|
|
65
|
+
paddingX: 0,
|
|
66
|
+
},
|
|
67
|
+
React.createElement(
|
|
68
|
+
Box,
|
|
69
|
+
{ width, justifyContent: checked ? "flex-end" : "flex-start" },
|
|
70
|
+
React.createElement(
|
|
71
|
+
Text,
|
|
72
|
+
{
|
|
73
|
+
backgroundColor: checked ? tokens.colors.accent : tokens.colors.muted,
|
|
74
|
+
color: tokens.colors.bg,
|
|
75
|
+
},
|
|
76
|
+
checked ? " ON " : " OFF"
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!label) {
|
|
82
|
+
return track;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return React.createElement(
|
|
86
|
+
Box,
|
|
87
|
+
{
|
|
88
|
+
gap: 2,
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
opacity: disabled ? 0.5 : 1,
|
|
91
|
+
},
|
|
92
|
+
track,
|
|
93
|
+
React.createElement(
|
|
94
|
+
Text,
|
|
95
|
+
{
|
|
96
|
+
color: disabled ? tokens.colors.muted : tokens.colors.fg,
|
|
97
|
+
dimColor: disabled,
|
|
98
|
+
},
|
|
99
|
+
label
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
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, truncate } from "@hauktui/core";
|
|
6
|
+
|
|
7
|
+
export interface TableColumn<T> {
|
|
8
|
+
/** Column header */
|
|
9
|
+
header: string;
|
|
10
|
+
/** Key or accessor function */
|
|
11
|
+
accessor: keyof T | ((row: T) => string);
|
|
12
|
+
/** Column width */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** Text alignment */
|
|
15
|
+
align?: "left" | "center" | "right";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TableProps<T> {
|
|
19
|
+
/** Data rows */
|
|
20
|
+
data: T[];
|
|
21
|
+
/** Column definitions */
|
|
22
|
+
columns: TableColumn<T>[];
|
|
23
|
+
/** Callback when row is selected */
|
|
24
|
+
onSelect?: (row: T, index: number) => void;
|
|
25
|
+
/** Whether rows are selectable */
|
|
26
|
+
selectable?: boolean;
|
|
27
|
+
/** Custom tokens override */
|
|
28
|
+
tokens?: Tokens;
|
|
29
|
+
/** Focus ID for focus management */
|
|
30
|
+
focusId?: string;
|
|
31
|
+
/** Maximum visible rows (for scrolling) */
|
|
32
|
+
maxRows?: number;
|
|
33
|
+
/** Show row numbers */
|
|
34
|
+
showRowNumbers?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Table<T>({
|
|
38
|
+
data,
|
|
39
|
+
columns,
|
|
40
|
+
onSelect,
|
|
41
|
+
selectable = true,
|
|
42
|
+
tokens: propTokens,
|
|
43
|
+
focusId,
|
|
44
|
+
maxRows = 10,
|
|
45
|
+
showRowNumbers = false,
|
|
46
|
+
}: TableProps<T>): React.ReactElement {
|
|
47
|
+
const contextTokens = useTokens();
|
|
48
|
+
const tokens = propTokens ?? contextTokens;
|
|
49
|
+
const id = focusId ?? stableId("table");
|
|
50
|
+
const { isFocused } = useFocusable(id);
|
|
51
|
+
|
|
52
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
53
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
54
|
+
|
|
55
|
+
const getCellValue = useCallback(
|
|
56
|
+
(row: T, column: TableColumn<T>): string => {
|
|
57
|
+
if (typeof column.accessor === "function") {
|
|
58
|
+
return column.accessor(row);
|
|
59
|
+
}
|
|
60
|
+
const value = row[column.accessor];
|
|
61
|
+
return String(value ?? "");
|
|
62
|
+
},
|
|
63
|
+
[]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useInput(
|
|
67
|
+
(input, key) => {
|
|
68
|
+
if (!isFocused || !selectable) return;
|
|
69
|
+
|
|
70
|
+
if (key.upArrow) {
|
|
71
|
+
setSelectedIndex((prev) => {
|
|
72
|
+
const next = Math.max(0, prev - 1);
|
|
73
|
+
if (next < scrollOffset) {
|
|
74
|
+
setScrollOffset(next);
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
});
|
|
78
|
+
} else if (key.downArrow) {
|
|
79
|
+
setSelectedIndex((prev) => {
|
|
80
|
+
const next = Math.min(data.length - 1, prev + 1);
|
|
81
|
+
if (next >= scrollOffset + maxRows) {
|
|
82
|
+
setScrollOffset(next - maxRows + 1);
|
|
83
|
+
}
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
} else if (key.return) {
|
|
87
|
+
const selectedRow = data[selectedIndex];
|
|
88
|
+
if (selectedRow) {
|
|
89
|
+
onSelect?.(selectedRow, selectedIndex);
|
|
90
|
+
}
|
|
91
|
+
} else if (key.pageUp || (key.ctrl && input === "u")) {
|
|
92
|
+
setSelectedIndex((prev) => {
|
|
93
|
+
const next = Math.max(0, prev - maxRows);
|
|
94
|
+
setScrollOffset(Math.max(0, scrollOffset - maxRows));
|
|
95
|
+
return next;
|
|
96
|
+
});
|
|
97
|
+
} else if (key.pageDown || (key.ctrl && input === "d")) {
|
|
98
|
+
setSelectedIndex((prev) => {
|
|
99
|
+
const next = Math.min(data.length - 1, prev + maxRows);
|
|
100
|
+
setScrollOffset(Math.min(data.length - maxRows, scrollOffset + maxRows));
|
|
101
|
+
return next;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{ isActive: isFocused }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const visibleData = data.slice(scrollOffset, scrollOffset + maxRows);
|
|
109
|
+
const defaultWidth = 15;
|
|
110
|
+
|
|
111
|
+
// Render header
|
|
112
|
+
const renderHeader = () => {
|
|
113
|
+
const cells: React.ReactElement[] = [];
|
|
114
|
+
|
|
115
|
+
if (showRowNumbers) {
|
|
116
|
+
cells.push(
|
|
117
|
+
React.createElement(
|
|
118
|
+
Box,
|
|
119
|
+
{ key: "rownum", width: 4 },
|
|
120
|
+
React.createElement(Text, { bold: true, color: tokens.colors.muted }, "#")
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const column of columns) {
|
|
126
|
+
const width = column.width ?? defaultWidth;
|
|
127
|
+
cells.push(
|
|
128
|
+
React.createElement(
|
|
129
|
+
Box,
|
|
130
|
+
{ key: column.header, width },
|
|
131
|
+
React.createElement(
|
|
132
|
+
Text,
|
|
133
|
+
{ bold: true, color: tokens.colors.accent },
|
|
134
|
+
truncate(column.header, width)
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return React.createElement(Box, { gap: 1 }, ...cells);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Render rows
|
|
144
|
+
const renderRows = () => {
|
|
145
|
+
return visibleData.map((row, visibleIndex) => {
|
|
146
|
+
const actualIndex = scrollOffset + visibleIndex;
|
|
147
|
+
const isSelected = actualIndex === selectedIndex;
|
|
148
|
+
const cells: React.ReactElement[] = [];
|
|
149
|
+
|
|
150
|
+
if (showRowNumbers) {
|
|
151
|
+
cells.push(
|
|
152
|
+
React.createElement(
|
|
153
|
+
Box,
|
|
154
|
+
{ key: "rownum", width: 4 },
|
|
155
|
+
React.createElement(
|
|
156
|
+
Text,
|
|
157
|
+
{ color: tokens.colors.muted },
|
|
158
|
+
String(actualIndex + 1)
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const column of columns) {
|
|
165
|
+
const width = column.width ?? defaultWidth;
|
|
166
|
+
const value = getCellValue(row, column);
|
|
167
|
+
|
|
168
|
+
cells.push(
|
|
169
|
+
React.createElement(
|
|
170
|
+
Box,
|
|
171
|
+
{ key: column.header, width },
|
|
172
|
+
React.createElement(
|
|
173
|
+
Text,
|
|
174
|
+
{
|
|
175
|
+
color: isSelected && isFocused
|
|
176
|
+
? tokens.colors.accent
|
|
177
|
+
: tokens.colors.fg,
|
|
178
|
+
bold: isSelected && isFocused,
|
|
179
|
+
},
|
|
180
|
+
truncate(value, width)
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return React.createElement(
|
|
187
|
+
Box,
|
|
188
|
+
{
|
|
189
|
+
key: actualIndex,
|
|
190
|
+
gap: 1,
|
|
191
|
+
paddingLeft: isSelected && isFocused ? 0 : 1,
|
|
192
|
+
},
|
|
193
|
+
isSelected && isFocused
|
|
194
|
+
? React.createElement(Text, { color: tokens.colors.accent }, "›")
|
|
195
|
+
: null,
|
|
196
|
+
...cells
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Render scroll indicator
|
|
202
|
+
const renderScrollIndicator = () => {
|
|
203
|
+
if (data.length <= maxRows) return null;
|
|
204
|
+
|
|
205
|
+
const showUp = scrollOffset > 0;
|
|
206
|
+
const showDown = scrollOffset + maxRows < data.length;
|
|
207
|
+
|
|
208
|
+
return React.createElement(
|
|
209
|
+
Box,
|
|
210
|
+
{ justifyContent: "flex-end", paddingRight: 1 },
|
|
211
|
+
React.createElement(
|
|
212
|
+
Text,
|
|
213
|
+
{ color: tokens.colors.muted },
|
|
214
|
+
showUp ? "↑" : " ",
|
|
215
|
+
` ${scrollOffset + 1}-${Math.min(scrollOffset + maxRows, data.length)}/${data.length} `,
|
|
216
|
+
showDown ? "↓" : " "
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return React.createElement(
|
|
222
|
+
Box,
|
|
223
|
+
{
|
|
224
|
+
flexDirection: "column",
|
|
225
|
+
borderStyle: isFocused ? "round" : "single",
|
|
226
|
+
borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
|
|
227
|
+
paddingX: 1,
|
|
228
|
+
},
|
|
229
|
+
renderHeader(),
|
|
230
|
+
React.createElement(
|
|
231
|
+
Box,
|
|
232
|
+
{ marginY: 0 },
|
|
233
|
+
React.createElement(
|
|
234
|
+
Text,
|
|
235
|
+
{ color: tokens.colors.border },
|
|
236
|
+
"─".repeat(columns.reduce((sum, c) => sum + (c.width ?? defaultWidth) + 1, showRowNumbers ? 5 : 0))
|
|
237
|
+
)
|
|
238
|
+
),
|
|
239
|
+
React.createElement(Box, { flexDirection: "column" }, ...renderRows()),
|
|
240
|
+
renderScrollIndicator()
|
|
241
|
+
);
|
|
242
|
+
}
|