@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,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,2 @@
1
+ export { Chart } from "./chart.js";
2
+ export type { ChartProps, ChartDataPoint } from "./chart.js";
@@ -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,2 @@
1
+ export { Checkbox } from "./checkbox.js";
2
+ export type { CheckboxProps } from "./checkbox.js";
@@ -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,2 @@
1
+ export { CodeBlock } from "./code-block.js";
2
+ export type { CodeBlockProps } from "./code-block.js";
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { Collapsible } from "./collapsible.js";
2
+ export type { CollapsibleProps } from "./collapsible.js";