@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,189 @@
|
|
|
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 ChartDataPoint {
|
|
7
|
+
/** Label for the data point */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Value */
|
|
10
|
+
value: number;
|
|
11
|
+
/** Optional color */
|
|
12
|
+
color?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChartProps {
|
|
16
|
+
/** Chart data */
|
|
17
|
+
data: ChartDataPoint[];
|
|
18
|
+
/** Chart type */
|
|
19
|
+
type?: "bar" | "horizontal-bar" | "sparkline";
|
|
20
|
+
/** Chart title */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** Maximum width for bars */
|
|
23
|
+
maxWidth?: number;
|
|
24
|
+
/** Show values */
|
|
25
|
+
showValues?: boolean;
|
|
26
|
+
/** Show labels */
|
|
27
|
+
showLabels?: boolean;
|
|
28
|
+
/** Custom tokens override */
|
|
29
|
+
tokens?: Tokens;
|
|
30
|
+
/** Height for sparkline */
|
|
31
|
+
height?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SPARKLINE_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
35
|
+
|
|
36
|
+
export function Chart({
|
|
37
|
+
data,
|
|
38
|
+
type = "horizontal-bar",
|
|
39
|
+
title,
|
|
40
|
+
maxWidth = 30,
|
|
41
|
+
showValues = true,
|
|
42
|
+
showLabels = true,
|
|
43
|
+
tokens: propTokens,
|
|
44
|
+
height = 1,
|
|
45
|
+
}: ChartProps): React.ReactElement {
|
|
46
|
+
const contextTokens = useTokens();
|
|
47
|
+
const tokens = propTokens ?? contextTokens;
|
|
48
|
+
|
|
49
|
+
const maxValue = Math.max(...data.map((d) => d.value));
|
|
50
|
+
const minValue = Math.min(...data.map((d) => d.value));
|
|
51
|
+
|
|
52
|
+
if (type === "sparkline") {
|
|
53
|
+
const range = maxValue - minValue || 1;
|
|
54
|
+
const sparkline = data.map((point) => {
|
|
55
|
+
const normalized = (point.value - minValue) / range;
|
|
56
|
+
const charIndex = Math.floor(normalized * (SPARKLINE_CHARS.length - 1));
|
|
57
|
+
return SPARKLINE_CHARS[charIndex];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return React.createElement(
|
|
61
|
+
Box,
|
|
62
|
+
{ flexDirection: "column" },
|
|
63
|
+
title
|
|
64
|
+
? React.createElement(
|
|
65
|
+
Text,
|
|
66
|
+
{ bold: true, color: tokens.colors.fg },
|
|
67
|
+
title
|
|
68
|
+
)
|
|
69
|
+
: null,
|
|
70
|
+
React.createElement(
|
|
71
|
+
Text,
|
|
72
|
+
{ color: tokens.colors.accent },
|
|
73
|
+
sparkline.join("")
|
|
74
|
+
),
|
|
75
|
+
showValues
|
|
76
|
+
? React.createElement(
|
|
77
|
+
Box,
|
|
78
|
+
{ justifyContent: "space-between" },
|
|
79
|
+
React.createElement(
|
|
80
|
+
Text,
|
|
81
|
+
{ color: tokens.colors.muted, dimColor: true },
|
|
82
|
+
minValue.toString()
|
|
83
|
+
),
|
|
84
|
+
React.createElement(
|
|
85
|
+
Text,
|
|
86
|
+
{ color: tokens.colors.muted, dimColor: true },
|
|
87
|
+
maxValue.toString()
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
: null
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (type === "bar") {
|
|
95
|
+
// Vertical bar chart
|
|
96
|
+
const barHeight = 8;
|
|
97
|
+
const rows: React.ReactElement[] = [];
|
|
98
|
+
|
|
99
|
+
for (let row = barHeight; row >= 1; row--) {
|
|
100
|
+
const cells = data.map((point, index) => {
|
|
101
|
+
const barLevel = Math.ceil((point.value / maxValue) * barHeight);
|
|
102
|
+
const filled = row <= barLevel;
|
|
103
|
+
return React.createElement(
|
|
104
|
+
Text,
|
|
105
|
+
{
|
|
106
|
+
key: `${point.label}-${row}`,
|
|
107
|
+
color: filled
|
|
108
|
+
? point.color || tokens.colors.accent
|
|
109
|
+
: tokens.colors.muted,
|
|
110
|
+
},
|
|
111
|
+
filled ? "█" : " ",
|
|
112
|
+
" "
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
rows.push(React.createElement(Box, { key: `row-${row}` }, ...cells));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return React.createElement(
|
|
120
|
+
Box,
|
|
121
|
+
{ flexDirection: "column" },
|
|
122
|
+
title
|
|
123
|
+
? React.createElement(
|
|
124
|
+
Text,
|
|
125
|
+
{ bold: true, color: tokens.colors.fg },
|
|
126
|
+
title
|
|
127
|
+
)
|
|
128
|
+
: null,
|
|
129
|
+
...rows,
|
|
130
|
+
showLabels
|
|
131
|
+
? React.createElement(
|
|
132
|
+
Box,
|
|
133
|
+
null,
|
|
134
|
+
...data.map((point) =>
|
|
135
|
+
React.createElement(
|
|
136
|
+
Text,
|
|
137
|
+
{ key: point.label, color: tokens.colors.muted },
|
|
138
|
+
point.label.charAt(0),
|
|
139
|
+
" "
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
: null
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Horizontal bar chart (default)
|
|
148
|
+
const maxLabelLength = Math.max(...data.map((d) => d.label.length));
|
|
149
|
+
|
|
150
|
+
return React.createElement(
|
|
151
|
+
Box,
|
|
152
|
+
{ flexDirection: "column" },
|
|
153
|
+
title
|
|
154
|
+
? React.createElement(
|
|
155
|
+
Text,
|
|
156
|
+
{ bold: true, color: tokens.colors.fg, marginBottom: 1 },
|
|
157
|
+
title
|
|
158
|
+
)
|
|
159
|
+
: null,
|
|
160
|
+
...data.map((point) => {
|
|
161
|
+
const barWidth = Math.round((point.value / maxValue) * maxWidth);
|
|
162
|
+
const bar = "█".repeat(barWidth) + "░".repeat(maxWidth - barWidth);
|
|
163
|
+
|
|
164
|
+
return React.createElement(
|
|
165
|
+
Box,
|
|
166
|
+
{ key: point.label, gap: 1 },
|
|
167
|
+
showLabels
|
|
168
|
+
? React.createElement(
|
|
169
|
+
Text,
|
|
170
|
+
{ color: tokens.colors.fg },
|
|
171
|
+
point.label.padEnd(maxLabelLength)
|
|
172
|
+
)
|
|
173
|
+
: null,
|
|
174
|
+
React.createElement(
|
|
175
|
+
Text,
|
|
176
|
+
{ color: point.color || tokens.colors.accent },
|
|
177
|
+
bar
|
|
178
|
+
),
|
|
179
|
+
showValues
|
|
180
|
+
? React.createElement(
|
|
181
|
+
Text,
|
|
182
|
+
{ color: tokens.colors.muted },
|
|
183
|
+
point.value.toString()
|
|
184
|
+
)
|
|
185
|
+
: null
|
|
186
|
+
);
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 CheckboxProps {
|
|
8
|
+
/** Whether the checkbox is checked (controlled) */
|
|
9
|
+
checked?: boolean;
|
|
10
|
+
/** Default checked state (uncontrolled) */
|
|
11
|
+
defaultChecked?: boolean;
|
|
12
|
+
/** Callback when checked state changes */
|
|
13
|
+
onChange?: (checked: boolean) => void;
|
|
14
|
+
/** Checkbox label */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Whether the checkbox is disabled */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** Custom tokens override */
|
|
19
|
+
tokens?: Tokens;
|
|
20
|
+
/** Focus ID for focus management */
|
|
21
|
+
focusId?: string;
|
|
22
|
+
/** Custom checked indicator */
|
|
23
|
+
checkedIndicator?: string;
|
|
24
|
+
/** Custom unchecked indicator */
|
|
25
|
+
uncheckedIndicator?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Checkbox({
|
|
29
|
+
checked: controlledChecked,
|
|
30
|
+
defaultChecked = false,
|
|
31
|
+
onChange,
|
|
32
|
+
label,
|
|
33
|
+
disabled = false,
|
|
34
|
+
tokens: propTokens,
|
|
35
|
+
focusId,
|
|
36
|
+
checkedIndicator = "◉",
|
|
37
|
+
uncheckedIndicator = "○",
|
|
38
|
+
}: CheckboxProps): React.ReactElement {
|
|
39
|
+
const contextTokens = useTokens();
|
|
40
|
+
const tokens = propTokens ?? contextTokens;
|
|
41
|
+
const id = focusId ?? stableId("checkbox");
|
|
42
|
+
const { isFocused } = useFocusable(id);
|
|
43
|
+
|
|
44
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
45
|
+
const isControlled = controlledChecked !== undefined;
|
|
46
|
+
const isChecked = isControlled ? controlledChecked : internalChecked;
|
|
47
|
+
|
|
48
|
+
const toggle = useCallback(() => {
|
|
49
|
+
if (disabled) return;
|
|
50
|
+
const newValue = !isChecked;
|
|
51
|
+
if (!isControlled) {
|
|
52
|
+
setInternalChecked(newValue);
|
|
53
|
+
}
|
|
54
|
+
onChange?.(newValue);
|
|
55
|
+
}, [disabled, isChecked, isControlled, onChange]);
|
|
56
|
+
|
|
57
|
+
useInput(
|
|
58
|
+
(input, key) => {
|
|
59
|
+
if (!isFocused || disabled) return;
|
|
60
|
+
if (key.return || input === " ") {
|
|
61
|
+
toggle();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{ isActive: isFocused }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const indicatorColor = disabled
|
|
68
|
+
? tokens.colors.disabled
|
|
69
|
+
: isChecked
|
|
70
|
+
? tokens.colors.accent
|
|
71
|
+
: tokens.colors.muted;
|
|
72
|
+
|
|
73
|
+
const labelColor = disabled
|
|
74
|
+
? tokens.colors.disabled
|
|
75
|
+
: isFocused
|
|
76
|
+
? tokens.colors.fg
|
|
77
|
+
: tokens.colors.muted;
|
|
78
|
+
|
|
79
|
+
return React.createElement(
|
|
80
|
+
Box,
|
|
81
|
+
{
|
|
82
|
+
paddingX: 1,
|
|
83
|
+
borderStyle: isFocused ? "round" : undefined,
|
|
84
|
+
borderColor: isFocused ? tokens.colors.focus : undefined,
|
|
85
|
+
},
|
|
86
|
+
React.createElement(
|
|
87
|
+
Text,
|
|
88
|
+
{ color: indicatorColor },
|
|
89
|
+
isChecked ? checkedIndicator : uncheckedIndicator
|
|
90
|
+
),
|
|
91
|
+
React.createElement(Text, null, " "),
|
|
92
|
+
React.createElement(
|
|
93
|
+
Text,
|
|
94
|
+
{ color: labelColor, bold: isFocused },
|
|
95
|
+
label
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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 CodeBlockProps {
|
|
7
|
+
/** Code content */
|
|
8
|
+
children: string;
|
|
9
|
+
/** Programming language for syntax highlighting */
|
|
10
|
+
language?: string;
|
|
11
|
+
/** Show line numbers */
|
|
12
|
+
showLineNumbers?: boolean;
|
|
13
|
+
/** Starting line number */
|
|
14
|
+
startLine?: number;
|
|
15
|
+
/** Highlight specific lines (1-indexed) */
|
|
16
|
+
highlightLines?: number[];
|
|
17
|
+
/** Title/filename to display */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Custom tokens override */
|
|
20
|
+
tokens?: Tokens;
|
|
21
|
+
/** Maximum width */
|
|
22
|
+
maxWidth?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Simple syntax highlighting patterns
|
|
26
|
+
const patterns: Record<string, { pattern: RegExp; color: string }[]> = {
|
|
27
|
+
javascript: [
|
|
28
|
+
{ pattern: /(\/\/.*$)/gm, color: "comment" },
|
|
29
|
+
{ pattern: /(\/\*[\s\S]*?\*\/)/g, color: "comment" },
|
|
30
|
+
{
|
|
31
|
+
pattern: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g,
|
|
32
|
+
color: "string",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern:
|
|
36
|
+
/\b(const|let|var|function|return|if|else|for|while|class|import|export|from|async|await|try|catch|throw|new|this|typeof|instanceof)\b/g,
|
|
37
|
+
color: "keyword",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
pattern: /\b(true|false|null|undefined|NaN|Infinity)\b/g,
|
|
41
|
+
color: "literal",
|
|
42
|
+
},
|
|
43
|
+
{ pattern: /\b(\d+\.?\d*)\b/g, color: "number" },
|
|
44
|
+
{ pattern: /\b([A-Z][a-zA-Z0-9]*)\b/g, color: "type" },
|
|
45
|
+
],
|
|
46
|
+
typescript: [
|
|
47
|
+
{ pattern: /(\/\/.*$)/gm, color: "comment" },
|
|
48
|
+
{ pattern: /(\/\*[\s\S]*?\*\/)/g, color: "comment" },
|
|
49
|
+
{
|
|
50
|
+
pattern: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g,
|
|
51
|
+
color: "string",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern:
|
|
55
|
+
/\b(const|let|var|function|return|if|else|for|while|class|import|export|from|async|await|try|catch|throw|new|this|typeof|instanceof|interface|type|extends|implements)\b/g,
|
|
56
|
+
color: "keyword",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
pattern: /\b(true|false|null|undefined|NaN|Infinity)\b/g,
|
|
60
|
+
color: "literal",
|
|
61
|
+
},
|
|
62
|
+
{ pattern: /\b(\d+\.?\d*)\b/g, color: "number" },
|
|
63
|
+
{ pattern: /\b([A-Z][a-zA-Z0-9]*)\b/g, color: "type" },
|
|
64
|
+
{ pattern: /:\s*([a-zA-Z]+)/g, color: "type" },
|
|
65
|
+
],
|
|
66
|
+
python: [
|
|
67
|
+
{ pattern: /(#.*$)/gm, color: "comment" },
|
|
68
|
+
{ pattern: /("""[\s\S]*?"""|'''[\s\S]*?''')/g, color: "string" },
|
|
69
|
+
{ pattern: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g, color: "string" },
|
|
70
|
+
{
|
|
71
|
+
pattern:
|
|
72
|
+
/\b(def|class|return|if|elif|else|for|while|import|from|as|try|except|raise|with|lambda|and|or|not|in|is|None|True|False)\b/g,
|
|
73
|
+
color: "keyword",
|
|
74
|
+
},
|
|
75
|
+
{ pattern: /\b(\d+\.?\d*)\b/g, color: "number" },
|
|
76
|
+
{ pattern: /@(\w+)/g, color: "decorator" },
|
|
77
|
+
],
|
|
78
|
+
bash: [
|
|
79
|
+
{ pattern: /(#.*$)/gm, color: "comment" },
|
|
80
|
+
{ pattern: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g, color: "string" },
|
|
81
|
+
{
|
|
82
|
+
pattern:
|
|
83
|
+
/\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|source)\b/g,
|
|
84
|
+
color: "keyword",
|
|
85
|
+
},
|
|
86
|
+
{ pattern: /(\$\{?[a-zA-Z_][a-zA-Z0-9_]*\}?)/g, color: "variable" },
|
|
87
|
+
],
|
|
88
|
+
json: [
|
|
89
|
+
{ pattern: /("(?:[^"\\]|\\.)*")\s*:/g, color: "key" },
|
|
90
|
+
{ pattern: /:\s*("(?:[^"\\]|\\.)*")/g, color: "string" },
|
|
91
|
+
{ pattern: /\b(true|false|null)\b/g, color: "literal" },
|
|
92
|
+
{ pattern: /\b(-?\d+\.?\d*)\b/g, color: "number" },
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function CodeBlock({
|
|
97
|
+
children,
|
|
98
|
+
language = "text",
|
|
99
|
+
showLineNumbers = true,
|
|
100
|
+
startLine = 1,
|
|
101
|
+
highlightLines = [],
|
|
102
|
+
title,
|
|
103
|
+
tokens: propTokens,
|
|
104
|
+
maxWidth,
|
|
105
|
+
}: CodeBlockProps): React.ReactElement {
|
|
106
|
+
const contextTokens = useTokens();
|
|
107
|
+
const tokens = propTokens ?? contextTokens;
|
|
108
|
+
|
|
109
|
+
const lines = children.split("\n");
|
|
110
|
+
const lineNumberWidth = String(startLine + lines.length - 1).length;
|
|
111
|
+
|
|
112
|
+
// Get color for syntax type
|
|
113
|
+
const getColor = (type: string): string => {
|
|
114
|
+
switch (type) {
|
|
115
|
+
case "comment":
|
|
116
|
+
return tokens.colors.muted;
|
|
117
|
+
case "string":
|
|
118
|
+
return tokens.colors.success;
|
|
119
|
+
case "keyword":
|
|
120
|
+
return tokens.colors.accent;
|
|
121
|
+
case "literal":
|
|
122
|
+
return tokens.colors.warning;
|
|
123
|
+
case "number":
|
|
124
|
+
return tokens.colors.info;
|
|
125
|
+
case "type":
|
|
126
|
+
return tokens.colors.accent;
|
|
127
|
+
case "variable":
|
|
128
|
+
return tokens.colors.warning;
|
|
129
|
+
case "key":
|
|
130
|
+
return tokens.colors.accent;
|
|
131
|
+
case "decorator":
|
|
132
|
+
return tokens.colors.warning;
|
|
133
|
+
default:
|
|
134
|
+
return tokens.colors.fg;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Simple tokenizer (basic highlighting without complex parsing)
|
|
139
|
+
const highlightCode = (code: string, lang: string): React.ReactNode[] => {
|
|
140
|
+
// For simplicity, return plain text with basic coloring
|
|
141
|
+
// A full implementation would tokenize and apply colors
|
|
142
|
+
return [
|
|
143
|
+
React.createElement(Text, { key: "code", color: tokens.colors.fg }, code),
|
|
144
|
+
];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return React.createElement(
|
|
148
|
+
Box,
|
|
149
|
+
{
|
|
150
|
+
flexDirection: "column",
|
|
151
|
+
borderStyle: "round",
|
|
152
|
+
borderColor: tokens.colors.border,
|
|
153
|
+
...(maxWidth ? { width: maxWidth } : {}),
|
|
154
|
+
},
|
|
155
|
+
// Title bar
|
|
156
|
+
title &&
|
|
157
|
+
React.createElement(
|
|
158
|
+
Box,
|
|
159
|
+
{
|
|
160
|
+
paddingX: 1,
|
|
161
|
+
borderStyle: "single",
|
|
162
|
+
borderTop: false,
|
|
163
|
+
borderLeft: false,
|
|
164
|
+
borderRight: false,
|
|
165
|
+
borderColor: tokens.colors.border,
|
|
166
|
+
},
|
|
167
|
+
React.createElement(
|
|
168
|
+
Text,
|
|
169
|
+
{ color: tokens.colors.muted, bold: true },
|
|
170
|
+
title
|
|
171
|
+
),
|
|
172
|
+
language !== "text" &&
|
|
173
|
+
React.createElement(
|
|
174
|
+
Text,
|
|
175
|
+
{ color: tokens.colors.accent },
|
|
176
|
+
` (${language})`
|
|
177
|
+
)
|
|
178
|
+
),
|
|
179
|
+
// Code content
|
|
180
|
+
React.createElement(
|
|
181
|
+
Box,
|
|
182
|
+
{ flexDirection: "column", paddingX: 1, paddingY: title ? 0 : 1 },
|
|
183
|
+
lines.map((line, index) => {
|
|
184
|
+
const lineNum = startLine + index;
|
|
185
|
+
const isHighlighted = highlightLines.includes(lineNum);
|
|
186
|
+
|
|
187
|
+
return React.createElement(
|
|
188
|
+
Box,
|
|
189
|
+
{ key: index },
|
|
190
|
+
// Line number
|
|
191
|
+
showLineNumbers &&
|
|
192
|
+
React.createElement(
|
|
193
|
+
Text,
|
|
194
|
+
{
|
|
195
|
+
color: isHighlighted
|
|
196
|
+
? tokens.colors.accent
|
|
197
|
+
: tokens.colors.muted,
|
|
198
|
+
},
|
|
199
|
+
String(lineNum).padStart(lineNumberWidth, " ") + " │ "
|
|
200
|
+
),
|
|
201
|
+
// Code line
|
|
202
|
+
React.createElement(
|
|
203
|
+
Text,
|
|
204
|
+
{
|
|
205
|
+
color: tokens.colors.fg,
|
|
206
|
+
backgroundColor: isHighlighted ? tokens.colors.border : undefined,
|
|
207
|
+
},
|
|
208
|
+
line || " "
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 CollapsibleProps {
|
|
8
|
+
/** Title/trigger for the collapsible */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Content when expanded */
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
/** Controlled open state */
|
|
13
|
+
open?: boolean;
|
|
14
|
+
/** Default open state for uncontrolled mode */
|
|
15
|
+
defaultOpen?: boolean;
|
|
16
|
+
/** Callback when open state changes */
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
/** Whether the collapsible is disabled */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Custom tokens override */
|
|
21
|
+
tokens?: Tokens;
|
|
22
|
+
/** Focus ID for focus management */
|
|
23
|
+
focusId?: string;
|
|
24
|
+
/** Icon when collapsed */
|
|
25
|
+
collapsedIcon?: string;
|
|
26
|
+
/** Icon when expanded */
|
|
27
|
+
expandedIcon?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Collapsible({
|
|
31
|
+
title,
|
|
32
|
+
children,
|
|
33
|
+
open: controlledOpen,
|
|
34
|
+
defaultOpen = false,
|
|
35
|
+
onOpenChange,
|
|
36
|
+
disabled = false,
|
|
37
|
+
tokens: propTokens,
|
|
38
|
+
focusId,
|
|
39
|
+
collapsedIcon = "▶",
|
|
40
|
+
expandedIcon = "▼",
|
|
41
|
+
}: CollapsibleProps): React.ReactElement {
|
|
42
|
+
const contextTokens = useTokens();
|
|
43
|
+
const tokens = propTokens ?? contextTokens;
|
|
44
|
+
const id = focusId ?? stableId("collapsible");
|
|
45
|
+
const { isFocused } = useFocusable(id);
|
|
46
|
+
|
|
47
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
48
|
+
const isControlled = controlledOpen !== undefined;
|
|
49
|
+
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
50
|
+
|
|
51
|
+
const toggle = useCallback(() => {
|
|
52
|
+
if (disabled) return;
|
|
53
|
+
|
|
54
|
+
const newOpen = !isOpen;
|
|
55
|
+
if (!isControlled) {
|
|
56
|
+
setInternalOpen(newOpen);
|
|
57
|
+
}
|
|
58
|
+
onOpenChange?.(newOpen);
|
|
59
|
+
}, [isOpen, isControlled, disabled, onOpenChange]);
|
|
60
|
+
|
|
61
|
+
useInput(
|
|
62
|
+
(input, key) => {
|
|
63
|
+
if (!isFocused || disabled) return;
|
|
64
|
+
|
|
65
|
+
if (key.return || input === " ") {
|
|
66
|
+
toggle();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{ isActive: isFocused }
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const icon = isOpen ? expandedIcon : collapsedIcon;
|
|
73
|
+
|
|
74
|
+
return React.createElement(
|
|
75
|
+
Box,
|
|
76
|
+
{ flexDirection: "column" },
|
|
77
|
+
// Trigger
|
|
78
|
+
React.createElement(
|
|
79
|
+
Box,
|
|
80
|
+
{
|
|
81
|
+
borderStyle: isFocused ? "round" : "single",
|
|
82
|
+
borderColor: disabled
|
|
83
|
+
? tokens.colors.disabled
|
|
84
|
+
: isFocused
|
|
85
|
+
? tokens.colors.focus
|
|
86
|
+
: tokens.colors.border,
|
|
87
|
+
paddingX: 1,
|
|
88
|
+
},
|
|
89
|
+
React.createElement(
|
|
90
|
+
Text,
|
|
91
|
+
{
|
|
92
|
+
color: disabled
|
|
93
|
+
? tokens.colors.disabled
|
|
94
|
+
: isFocused
|
|
95
|
+
? tokens.colors.accent
|
|
96
|
+
: tokens.colors.fg,
|
|
97
|
+
bold: isFocused,
|
|
98
|
+
dimColor: disabled,
|
|
99
|
+
},
|
|
100
|
+
icon,
|
|
101
|
+
" ",
|
|
102
|
+
title
|
|
103
|
+
)
|
|
104
|
+
),
|
|
105
|
+
// Content
|
|
106
|
+
isOpen
|
|
107
|
+
? React.createElement(
|
|
108
|
+
Box,
|
|
109
|
+
{
|
|
110
|
+
flexDirection: "column",
|
|
111
|
+
paddingLeft: 2,
|
|
112
|
+
borderStyle: "single",
|
|
113
|
+
borderColor: tokens.colors.border,
|
|
114
|
+
borderTop: false,
|
|
115
|
+
borderRight: false,
|
|
116
|
+
borderBottom: false,
|
|
117
|
+
marginLeft: 1,
|
|
118
|
+
},
|
|
119
|
+
children
|
|
120
|
+
)
|
|
121
|
+
: null
|
|
122
|
+
);
|
|
123
|
+
}
|