@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,165 @@
1
+ import React, { useState, useEffect } 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 CountdownProps {
7
+ /** Target date/time */
8
+ target: Date;
9
+ /** Callback when countdown reaches zero */
10
+ onComplete?: () => void;
11
+ /** Format to display */
12
+ format?: "full" | "compact" | "minimal";
13
+ /** Show labels */
14
+ showLabels?: boolean;
15
+ /** Custom tokens override */
16
+ tokens?: Tokens;
17
+ /** Update interval in ms */
18
+ interval?: number;
19
+ }
20
+
21
+ interface TimeLeft {
22
+ days: number;
23
+ hours: number;
24
+ minutes: number;
25
+ seconds: number;
26
+ total: number;
27
+ }
28
+
29
+ export function Countdown({
30
+ target,
31
+ onComplete,
32
+ format = "full",
33
+ showLabels = true,
34
+ tokens: propTokens,
35
+ interval = 1000,
36
+ }: CountdownProps): React.ReactElement {
37
+ const contextTokens = useTokens();
38
+ const tokens = propTokens ?? contextTokens;
39
+
40
+ const calculateTimeLeft = (): TimeLeft => {
41
+ const difference = target.getTime() - Date.now();
42
+
43
+ if (difference <= 0) {
44
+ return { days: 0, hours: 0, minutes: 0, seconds: 0, total: 0 };
45
+ }
46
+
47
+ return {
48
+ days: Math.floor(difference / (1000 * 60 * 60 * 24)),
49
+ hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
50
+ minutes: Math.floor((difference / (1000 * 60)) % 60),
51
+ seconds: Math.floor((difference / 1000) % 60),
52
+ total: difference,
53
+ };
54
+ };
55
+
56
+ const [timeLeft, setTimeLeft] = useState<TimeLeft>(calculateTimeLeft);
57
+ const [completed, setCompleted] = useState(false);
58
+
59
+ useEffect(() => {
60
+ if (completed) return;
61
+
62
+ const timer = setInterval(() => {
63
+ const newTimeLeft = calculateTimeLeft();
64
+ setTimeLeft(newTimeLeft);
65
+
66
+ if (newTimeLeft.total <= 0 && !completed) {
67
+ setCompleted(true);
68
+ onComplete?.();
69
+ clearInterval(timer);
70
+ }
71
+ }, interval);
72
+
73
+ return () => clearInterval(timer);
74
+ }, [target, completed, interval]);
75
+
76
+ // Pad number with zeros
77
+ const pad = (n: number, digits = 2): string => {
78
+ return String(n).padStart(digits, "0");
79
+ };
80
+
81
+ // Render based on format
82
+ const renderCountdown = () => {
83
+ if (completed) {
84
+ return React.createElement(
85
+ Text,
86
+ { color: tokens.colors.success, bold: true },
87
+ "Completed!"
88
+ );
89
+ }
90
+
91
+ switch (format) {
92
+ case "minimal":
93
+ return React.createElement(
94
+ Text,
95
+ { color: tokens.colors.fg },
96
+ `${pad(timeLeft.hours + timeLeft.days * 24)}:${pad(timeLeft.minutes)}:${pad(timeLeft.seconds)}`
97
+ );
98
+
99
+ case "compact":
100
+ return React.createElement(
101
+ Box,
102
+ { gap: 1 },
103
+ timeLeft.days > 0 &&
104
+ React.createElement(
105
+ Text,
106
+ { color: tokens.colors.accent, bold: true },
107
+ `${timeLeft.days}d`
108
+ ),
109
+ React.createElement(
110
+ Text,
111
+ { color: tokens.colors.accent, bold: true },
112
+ `${pad(timeLeft.hours)}:${pad(timeLeft.minutes)}:${pad(timeLeft.seconds)}`
113
+ )
114
+ );
115
+
116
+ case "full":
117
+ default:
118
+ const units = [
119
+ { value: timeLeft.days, label: "days", show: timeLeft.days > 0 },
120
+ { value: timeLeft.hours, label: "hours", show: true },
121
+ { value: timeLeft.minutes, label: "mins", show: true },
122
+ { value: timeLeft.seconds, label: "secs", show: true },
123
+ ].filter((u) => u.show);
124
+
125
+ return React.createElement(
126
+ Box,
127
+ { gap: 2 },
128
+ ...units.map((unit, index) =>
129
+ React.createElement(
130
+ Box,
131
+ {
132
+ key: index,
133
+ flexDirection: "column",
134
+ alignItems: "center",
135
+ },
136
+ React.createElement(
137
+ Text,
138
+ {
139
+ color: tokens.colors.accent,
140
+ bold: true,
141
+ },
142
+ pad(unit.value)
143
+ ),
144
+ showLabels &&
145
+ React.createElement(
146
+ Text,
147
+ { color: tokens.colors.muted, dimColor: true },
148
+ unit.label
149
+ )
150
+ )
151
+ )
152
+ );
153
+ }
154
+ };
155
+
156
+ return React.createElement(
157
+ Box,
158
+ {
159
+ borderStyle: "round",
160
+ borderColor: completed ? tokens.colors.success : tokens.colors.border,
161
+ paddingX: 1,
162
+ },
163
+ renderCountdown()
164
+ );
165
+ }
@@ -0,0 +1,2 @@
1
+ export { Countdown } from "./countdown.js";
2
+ export type { CountdownProps } from "./countdown.js";
@@ -0,0 +1,256 @@
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 } from "@hauktui/core";
6
+
7
+ export interface DataTableColumn<T> {
8
+ /** Column header */
9
+ header: string;
10
+ /** Key to access data */
11
+ accessor: keyof T;
12
+ /** Column width */
13
+ width?: number;
14
+ /** Alignment */
15
+ align?: "left" | "center" | "right";
16
+ /** Custom render function */
17
+ render?: (value: T[keyof T], row: T) => React.ReactNode;
18
+ }
19
+
20
+ export interface DataTableProps<T extends Record<string, unknown>> {
21
+ /** Column definitions */
22
+ columns: DataTableColumn<T>[];
23
+ /** Data rows */
24
+ data: T[];
25
+ /** Callback when row is selected */
26
+ onSelect?: (row: T, index: number) => void;
27
+ /** Maximum visible rows */
28
+ maxRows?: number;
29
+ /** Whether rows are selectable */
30
+ selectable?: boolean;
31
+ /** Show row numbers */
32
+ showRowNumbers?: boolean;
33
+ /** Enable sorting */
34
+ sortable?: boolean;
35
+ /** Custom tokens override */
36
+ tokens?: Tokens;
37
+ /** Focus ID for focus management */
38
+ focusId?: string;
39
+ /** Striped rows */
40
+ striped?: boolean;
41
+ }
42
+
43
+ export function DataTable<T extends Record<string, unknown>>({
44
+ columns,
45
+ data,
46
+ onSelect,
47
+ maxRows = 10,
48
+ selectable = true,
49
+ showRowNumbers = false,
50
+ sortable = true,
51
+ tokens: propTokens,
52
+ focusId,
53
+ striped = true,
54
+ }: DataTableProps<T>): React.ReactElement {
55
+ const contextTokens = useTokens();
56
+ const tokens = propTokens ?? contextTokens;
57
+ const id = focusId ?? stableId("data-table");
58
+ const { isFocused } = useFocusable(id);
59
+
60
+ const [selectedIndex, setSelectedIndex] = useState(0);
61
+ const [scrollOffset, setScrollOffset] = useState(0);
62
+ const [sortColumn, setSortColumn] = useState<keyof T | null>(null);
63
+ const [sortAsc, setSortAsc] = useState(true);
64
+
65
+ const sortedData = React.useMemo(() => {
66
+ if (!sortColumn) return data;
67
+ return [...data].sort((a, b) => {
68
+ const aVal = a[sortColumn];
69
+ const bVal = b[sortColumn];
70
+ if (aVal < bVal) return sortAsc ? -1 : 1;
71
+ if (aVal > bVal) return sortAsc ? 1 : -1;
72
+ return 0;
73
+ });
74
+ }, [data, sortColumn, sortAsc]);
75
+
76
+ const visibleData = sortedData.slice(scrollOffset, scrollOffset + maxRows);
77
+
78
+ useInput(
79
+ (input, key) => {
80
+ if (!isFocused) return;
81
+
82
+ if (key.upArrow || input === "k") {
83
+ if (selectedIndex > 0) {
84
+ setSelectedIndex(selectedIndex - 1);
85
+ if (selectedIndex - 1 < scrollOffset) {
86
+ setScrollOffset(scrollOffset - 1);
87
+ }
88
+ }
89
+ } else if (key.downArrow || input === "j") {
90
+ if (selectedIndex < sortedData.length - 1) {
91
+ setSelectedIndex(selectedIndex + 1);
92
+ if (selectedIndex + 1 >= scrollOffset + maxRows) {
93
+ setScrollOffset(scrollOffset + 1);
94
+ }
95
+ }
96
+ } else if (input === "g") {
97
+ setSelectedIndex(0);
98
+ setScrollOffset(0);
99
+ } else if (input === "G") {
100
+ setSelectedIndex(sortedData.length - 1);
101
+ setScrollOffset(Math.max(0, sortedData.length - maxRows));
102
+ } else if (key.return && selectable) {
103
+ onSelect?.(sortedData[selectedIndex]!, selectedIndex);
104
+ } else if (sortable && input === "s") {
105
+ // Cycle through columns for sorting
106
+ const currentColIndex = sortColumn
107
+ ? columns.findIndex((c) => c.accessor === sortColumn)
108
+ : -1;
109
+ const nextColIndex = (currentColIndex + 1) % columns.length;
110
+ const nextCol = columns[nextColIndex]!.accessor;
111
+ if (nextCol === sortColumn) {
112
+ setSortAsc(!sortAsc);
113
+ } else {
114
+ setSortColumn(nextCol);
115
+ setSortAsc(true);
116
+ }
117
+ }
118
+ },
119
+ { isActive: isFocused }
120
+ );
121
+
122
+ const getColumnWidth = (col: DataTableColumn<T>): number => {
123
+ if (col.width) return col.width;
124
+ const headerLen = col.header.length;
125
+ const maxDataLen = Math.max(
126
+ ...data.map((row) => String(row[col.accessor] ?? "").length)
127
+ );
128
+ return Math.max(headerLen, maxDataLen) + 2;
129
+ };
130
+
131
+ // Header row
132
+ const headerRow = React.createElement(
133
+ Box,
134
+ { gap: 0 },
135
+ showRowNumbers
136
+ ? React.createElement(
137
+ Text,
138
+ { color: tokens.colors.muted, bold: true },
139
+ "#".padEnd(4)
140
+ )
141
+ : null,
142
+ ...columns.map((col) => {
143
+ const width = getColumnWidth(col);
144
+ const isSorted = sortColumn === col.accessor;
145
+ const sortIndicator = isSorted ? (sortAsc ? " ▲" : " ▼") : "";
146
+
147
+ return React.createElement(
148
+ Text,
149
+ {
150
+ key: String(col.accessor),
151
+ color: tokens.colors.fg,
152
+ bold: true,
153
+ },
154
+ (col.header + sortIndicator).padEnd(width)
155
+ );
156
+ })
157
+ );
158
+
159
+ // Separator
160
+ const totalWidth =
161
+ columns.reduce((sum, col) => sum + getColumnWidth(col), 0) +
162
+ (showRowNumbers ? 4 : 0);
163
+ const separator = React.createElement(
164
+ Text,
165
+ { color: tokens.colors.border },
166
+ "─".repeat(totalWidth)
167
+ );
168
+
169
+ // Data rows
170
+ const dataRows = visibleData.map((row, visibleIdx) => {
171
+ const actualIndex = scrollOffset + visibleIdx;
172
+ const isSelected = actualIndex === selectedIndex;
173
+ const isStriped = striped && visibleIdx % 2 === 1;
174
+
175
+ return React.createElement(
176
+ Box,
177
+ { key: actualIndex, gap: 0 },
178
+ showRowNumbers
179
+ ? React.createElement(
180
+ Text,
181
+ { color: tokens.colors.muted, dimColor: true },
182
+ (actualIndex + 1).toString().padEnd(4)
183
+ )
184
+ : null,
185
+ ...columns.map((col) => {
186
+ const width = getColumnWidth(col);
187
+ const value = row[col.accessor];
188
+ const displayValue = col.render
189
+ ? col.render(value, row)
190
+ : String(value ?? "");
191
+
192
+ let text = String(displayValue);
193
+ if (col.align === "right") {
194
+ text = text.padStart(width);
195
+ } else if (col.align === "center") {
196
+ const pad = Math.floor((width - text.length) / 2);
197
+ text = " ".repeat(pad) + text + " ".repeat(width - pad - text.length);
198
+ } else {
199
+ text = text.padEnd(width);
200
+ }
201
+
202
+ return React.createElement(
203
+ Text,
204
+ {
205
+ key: String(col.accessor),
206
+ color:
207
+ isSelected && isFocused ? tokens.colors.accent : tokens.colors.fg,
208
+ bold: isSelected && isFocused,
209
+ backgroundColor:
210
+ isSelected && isFocused
211
+ ? undefined
212
+ : isStriped
213
+ ? tokens.colors.muted + "20"
214
+ : undefined,
215
+ },
216
+ text
217
+ );
218
+ })
219
+ );
220
+ });
221
+
222
+ // Scroll indicator
223
+ const showScrollIndicator = sortedData.length > maxRows;
224
+ const scrollInfo = showScrollIndicator
225
+ ? React.createElement(
226
+ Text,
227
+ { color: tokens.colors.muted, dimColor: true },
228
+ `${selectedIndex + 1}/${sortedData.length}`
229
+ )
230
+ : null;
231
+
232
+ return React.createElement(
233
+ Box,
234
+ {
235
+ flexDirection: "column",
236
+ borderStyle: "single",
237
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
238
+ paddingX: 1,
239
+ },
240
+ headerRow,
241
+ separator,
242
+ ...dataRows,
243
+ showScrollIndicator
244
+ ? React.createElement(
245
+ Box,
246
+ { justifyContent: "space-between", marginTop: 0 },
247
+ React.createElement(
248
+ Text,
249
+ { color: tokens.colors.muted, dimColor: true },
250
+ "j/k navigate g/G top/bottom s sort"
251
+ ),
252
+ scrollInfo
253
+ )
254
+ : null
255
+ );
256
+ }
@@ -0,0 +1,2 @@
1
+ export { DataTable } from "./data-table.js";
2
+ export type { DataTableProps, DataTableColumn } from "./data-table.js";
@@ -0,0 +1,280 @@
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 DatePickerProps {
8
+ /** Selected date */
9
+ value?: Date;
10
+ /** Callback when date is selected */
11
+ onChange?: (date: Date) => void;
12
+ /** Placeholder text */
13
+ placeholder?: string;
14
+ /** Date format */
15
+ format?: "short" | "medium" | "long";
16
+ /** Minimum selectable date */
17
+ minDate?: Date;
18
+ /** Maximum selectable date */
19
+ maxDate?: Date;
20
+ /** Custom tokens override */
21
+ tokens?: Tokens;
22
+ /** Focus ID for focus management */
23
+ focusId?: string;
24
+ /** Label */
25
+ label?: string;
26
+ }
27
+
28
+ const MONTHS = [
29
+ "Jan",
30
+ "Feb",
31
+ "Mar",
32
+ "Apr",
33
+ "May",
34
+ "Jun",
35
+ "Jul",
36
+ "Aug",
37
+ "Sep",
38
+ "Oct",
39
+ "Nov",
40
+ "Dec",
41
+ ];
42
+
43
+ function formatDate(date: Date, format: "short" | "medium" | "long"): string {
44
+ const day = date.getDate();
45
+ const month = date.getMonth();
46
+ const year = date.getFullYear();
47
+
48
+ switch (format) {
49
+ case "short":
50
+ return `${month + 1}/${day}/${year}`;
51
+ case "medium":
52
+ return `${MONTHS[month]} ${day}, ${year}`;
53
+ case "long":
54
+ return date.toLocaleDateString("en-US", {
55
+ weekday: "long",
56
+ year: "numeric",
57
+ month: "long",
58
+ day: "numeric",
59
+ });
60
+ default:
61
+ return `${month + 1}/${day}/${year}`;
62
+ }
63
+ }
64
+
65
+ function getDaysInMonth(year: number, month: number): number {
66
+ return new Date(year, month + 1, 0).getDate();
67
+ }
68
+
69
+ export function DatePicker({
70
+ value,
71
+ onChange,
72
+ placeholder = "Select date...",
73
+ format = "medium",
74
+ minDate,
75
+ maxDate,
76
+ tokens: propTokens,
77
+ focusId,
78
+ label,
79
+ }: DatePickerProps): React.ReactElement {
80
+ const contextTokens = useTokens();
81
+ const tokens = propTokens ?? contextTokens;
82
+ const id = focusId ?? stableId("date-picker");
83
+ const { isFocused } = useFocusable(id);
84
+
85
+ const [isOpen, setIsOpen] = useState(false);
86
+ const [tempDate, setTempDate] = useState(value || new Date());
87
+ const [editField, setEditField] = useState<"month" | "day" | "year">("month");
88
+
89
+ useInput(
90
+ (input, key) => {
91
+ if (!isFocused) return;
92
+
93
+ if (key.return && !isOpen) {
94
+ setIsOpen(true);
95
+ setTempDate(value || new Date());
96
+ return;
97
+ }
98
+
99
+ if (key.escape && isOpen) {
100
+ setIsOpen(false);
101
+ return;
102
+ }
103
+
104
+ if (isOpen) {
105
+ const year = tempDate.getFullYear();
106
+ const month = tempDate.getMonth();
107
+ const day = tempDate.getDate();
108
+
109
+ if (key.tab) {
110
+ if (editField === "month") setEditField("day");
111
+ else if (editField === "day") setEditField("year");
112
+ else setEditField("month");
113
+ return;
114
+ }
115
+
116
+ if (key.upArrow || input === "k") {
117
+ if (editField === "month") {
118
+ setTempDate(
119
+ new Date(
120
+ year,
121
+ month + 1,
122
+ Math.min(day, getDaysInMonth(year, month + 1))
123
+ )
124
+ );
125
+ } else if (editField === "day") {
126
+ const maxDay = getDaysInMonth(year, month);
127
+ setTempDate(new Date(year, month, day < maxDay ? day + 1 : 1));
128
+ } else {
129
+ setTempDate(new Date(year + 1, month, day));
130
+ }
131
+ } else if (key.downArrow || input === "j") {
132
+ if (editField === "month") {
133
+ setTempDate(
134
+ new Date(
135
+ year,
136
+ month - 1,
137
+ Math.min(day, getDaysInMonth(year, month - 1))
138
+ )
139
+ );
140
+ } else if (editField === "day") {
141
+ const maxDay = getDaysInMonth(year, month);
142
+ setTempDate(new Date(year, month, day > 1 ? day - 1 : maxDay));
143
+ } else {
144
+ setTempDate(new Date(year - 1, month, day));
145
+ }
146
+ } else if (key.return) {
147
+ if (minDate && tempDate < minDate) return;
148
+ if (maxDate && tempDate > maxDate) return;
149
+ onChange?.(tempDate);
150
+ setIsOpen(false);
151
+ }
152
+ }
153
+ },
154
+ { isActive: isFocused }
155
+ );
156
+
157
+ const displayValue = value ? formatDate(value, format) : placeholder;
158
+
159
+ const picker = React.createElement(
160
+ Box,
161
+ {
162
+ borderStyle: "round",
163
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
164
+ paddingX: 1,
165
+ gap: 1,
166
+ },
167
+ React.createElement(
168
+ Text,
169
+ { color: value ? tokens.colors.fg : tokens.colors.muted },
170
+ displayValue
171
+ ),
172
+ React.createElement(
173
+ Text,
174
+ { color: tokens.colors.muted },
175
+ isOpen ? "▲" : "▼"
176
+ )
177
+ );
178
+
179
+ if (!isOpen) {
180
+ if (label) {
181
+ return React.createElement(
182
+ Box,
183
+ { flexDirection: "column" },
184
+ React.createElement(
185
+ Text,
186
+ { color: tokens.colors.fg, bold: true },
187
+ label
188
+ ),
189
+ picker
190
+ );
191
+ }
192
+ return picker;
193
+ }
194
+
195
+ // Expanded date picker
196
+ return React.createElement(
197
+ Box,
198
+ { flexDirection: "column" },
199
+ label
200
+ ? React.createElement(
201
+ Text,
202
+ { color: tokens.colors.fg, bold: true },
203
+ label
204
+ )
205
+ : null,
206
+ picker,
207
+ React.createElement(
208
+ Box,
209
+ {
210
+ borderStyle: "round",
211
+ borderColor: tokens.colors.border,
212
+ paddingX: 1,
213
+ gap: 2,
214
+ marginTop: 0,
215
+ },
216
+ React.createElement(
217
+ Box,
218
+ { flexDirection: "column", alignItems: "center" },
219
+ React.createElement(
220
+ Text,
221
+ { color: tokens.colors.muted, dimColor: true },
222
+ "Month"
223
+ ),
224
+ React.createElement(
225
+ Text,
226
+ {
227
+ color:
228
+ editField === "month" ? tokens.colors.accent : tokens.colors.fg,
229
+ bold: editField === "month",
230
+ inverse: editField === "month",
231
+ },
232
+ ` ${MONTHS[tempDate.getMonth()]} `
233
+ )
234
+ ),
235
+ React.createElement(
236
+ Box,
237
+ { flexDirection: "column", alignItems: "center" },
238
+ React.createElement(
239
+ Text,
240
+ { color: tokens.colors.muted, dimColor: true },
241
+ "Day"
242
+ ),
243
+ React.createElement(
244
+ Text,
245
+ {
246
+ color:
247
+ editField === "day" ? tokens.colors.accent : tokens.colors.fg,
248
+ bold: editField === "day",
249
+ inverse: editField === "day",
250
+ },
251
+ ` ${tempDate.getDate().toString().padStart(2, "0")} `
252
+ )
253
+ ),
254
+ React.createElement(
255
+ Box,
256
+ { flexDirection: "column", alignItems: "center" },
257
+ React.createElement(
258
+ Text,
259
+ { color: tokens.colors.muted, dimColor: true },
260
+ "Year"
261
+ ),
262
+ React.createElement(
263
+ Text,
264
+ {
265
+ color:
266
+ editField === "year" ? tokens.colors.accent : tokens.colors.fg,
267
+ bold: editField === "year",
268
+ inverse: editField === "year",
269
+ },
270
+ ` ${tempDate.getFullYear()} `
271
+ )
272
+ )
273
+ ),
274
+ React.createElement(
275
+ Text,
276
+ { color: tokens.colors.muted, dimColor: true },
277
+ "Tab switch field ↑↓ adjust Enter confirm Esc cancel"
278
+ )
279
+ );
280
+ }
@@ -0,0 +1,2 @@
1
+ export { DatePicker } from "./date-picker.js";
2
+ export type { DatePickerProps } from "./date-picker.js";