@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,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,2 @@
1
+ export { Stepper } from "./stepper.js";
2
+ export type { StepperProps, Step } from "./stepper.js";
@@ -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,2 @@
1
+ export { Switch } from "./switch.js";
2
+ export type { SwitchProps } from "./switch.js";
@@ -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,2 @@
1
+ export { Table } from "./table.js";
2
+ export type { TableProps, TableColumn } from "./table.js";
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { Tabs } from "./tabs.js";
2
+ export type { TabsProps, Tab } from "./tabs.js";