@sixthshift/design-system 0.1.0

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 (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/styles.css +2 -0
  4. package/dist/theme.generated.css +438 -0
  5. package/package.json +238 -0
  6. package/src/components/Avatar/Avatar.tsx +19 -0
  7. package/src/components/Avatar/index.ts +1 -0
  8. package/src/components/Badge/Badge.tsx +59 -0
  9. package/src/components/Badge/index.ts +1 -0
  10. package/src/components/BarChart/BarChart.tsx +51 -0
  11. package/src/components/BarChart/index.ts +1 -0
  12. package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
  13. package/src/components/Breadcrumb/index.ts +5 -0
  14. package/src/components/Button/Button.tsx +157 -0
  15. package/src/components/Button/index.ts +1 -0
  16. package/src/components/Calendar/Calendar.tsx +433 -0
  17. package/src/components/Calendar/calendar.hooks.ts +329 -0
  18. package/src/components/Calendar/calendar.types.ts +117 -0
  19. package/src/components/Calendar/index.ts +28 -0
  20. package/src/components/Card/Card.tsx +47 -0
  21. package/src/components/Card/index.ts +1 -0
  22. package/src/components/Checkbox/Checkbox.tsx +144 -0
  23. package/src/components/Checkbox/index.ts +1 -0
  24. package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
  25. package/src/components/CheckboxGroup/index.ts +5 -0
  26. package/src/components/Code/Editor/Editor.tsx +161 -0
  27. package/src/components/Code/Editor/index.ts +2 -0
  28. package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
  29. package/src/components/Code/Editor/themes.ts +112 -0
  30. package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
  31. package/src/components/Code/ValidationStatus/index.ts +1 -0
  32. package/src/components/Code/Workspace/StatusBar.tsx +47 -0
  33. package/src/components/Code/Workspace/Toolbar.tsx +47 -0
  34. package/src/components/Code/Workspace/Workspace.tsx +109 -0
  35. package/src/components/Code/Workspace/index.ts +3 -0
  36. package/src/components/ColorDot/ColorDot.tsx +50 -0
  37. package/src/components/ColorDot/index.ts +1 -0
  38. package/src/components/DatePicker/DatePicker.tsx +236 -0
  39. package/src/components/DatePicker/datepicker.hooks.ts +381 -0
  40. package/src/components/DatePicker/datepicker.types.ts +140 -0
  41. package/src/components/DatePicker/index.ts +12 -0
  42. package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
  43. package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
  44. package/src/components/DateRangePicker/index.ts +2 -0
  45. package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
  46. package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
  47. package/src/components/DateTimePicker/index.ts +2 -0
  48. package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
  49. package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
  50. package/src/components/DateTimeRangePicker/index.ts +2 -0
  51. package/src/components/EmptyState/EmptyState.tsx +29 -0
  52. package/src/components/EmptyState/index.ts +1 -0
  53. package/src/components/Field/Field.tsx +60 -0
  54. package/src/components/Field/index.ts +1 -0
  55. package/src/components/FormField/FormField.tsx +55 -0
  56. package/src/components/FormField/index.ts +2 -0
  57. package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
  58. package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
  59. package/src/components/HeatMap/heat-map.utils.ts +15 -0
  60. package/src/components/HeatMap/index.ts +3 -0
  61. package/src/components/HoverCard/HoverCard.tsx +83 -0
  62. package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
  63. package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
  64. package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
  65. package/src/components/HoverCard/index.ts +3 -0
  66. package/src/components/Input/Input.tsx +48 -0
  67. package/src/components/Input/index.ts +1 -0
  68. package/src/components/LineChart/LineChart.tsx +251 -0
  69. package/src/components/LineChart/index.ts +1 -0
  70. package/src/components/Markdown/Markdown.tsx +40 -0
  71. package/src/components/Markdown/index.ts +1 -0
  72. package/src/components/Message/Message.tsx +59 -0
  73. package/src/components/Message/components/MessageBody.tsx +9 -0
  74. package/src/components/Message/components/MessageDescription.tsx +9 -0
  75. package/src/components/Message/components/MessageIcon.tsx +13 -0
  76. package/src/components/Message/components/MessageTitle.tsx +9 -0
  77. package/src/components/Message/index.ts +10 -0
  78. package/src/components/MetricList/MetricList.tsx +13 -0
  79. package/src/components/MetricList/MetricRow.tsx +30 -0
  80. package/src/components/MetricList/index.ts +2 -0
  81. package/src/components/Modal/Modal.tsx +146 -0
  82. package/src/components/Modal/components/ModalBody.tsx +9 -0
  83. package/src/components/Modal/components/ModalContext.tsx +27 -0
  84. package/src/components/Modal/components/ModalFooter.tsx +9 -0
  85. package/src/components/Modal/components/ModalHeader.tsx +29 -0
  86. package/src/components/Modal/components/index.ts +9 -0
  87. package/src/components/Modal/index.ts +13 -0
  88. package/src/components/NavBottom/NavBottom.tsx +53 -0
  89. package/src/components/NavBottom/index.ts +3 -0
  90. package/src/components/NavSide/NavItem.tsx +39 -0
  91. package/src/components/NavSide/NavSide.tsx +40 -0
  92. package/src/components/NavSide/index.ts +2 -0
  93. package/src/components/NavSide/types.ts +36 -0
  94. package/src/components/Pagination/Pagination.tsx +86 -0
  95. package/src/components/Pagination/index.ts +2 -0
  96. package/src/components/Popover/Popover.tsx +68 -0
  97. package/src/components/Popover/components/PopoverBody.tsx +32 -0
  98. package/src/components/Popover/components/PopoverClose.tsx +26 -0
  99. package/src/components/Popover/components/PopoverContext.tsx +24 -0
  100. package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
  101. package/src/components/Popover/index.ts +10 -0
  102. package/src/components/ProgressBar/ProgressBar.tsx +42 -0
  103. package/src/components/ProgressBar/index.ts +1 -0
  104. package/src/components/RadioButton/RadioButton.tsx +89 -0
  105. package/src/components/RadioButton/index.ts +1 -0
  106. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
  107. package/src/components/RadioButtonGroup/index.ts +5 -0
  108. package/src/components/SearchInput/SearchInput.tsx +45 -0
  109. package/src/components/SearchInput/index.ts +2 -0
  110. package/src/components/Select/Select.tsx +289 -0
  111. package/src/components/Select/SelectDropdown.tsx +90 -0
  112. package/src/components/Select/SelectTrigger.tsx +130 -0
  113. package/src/components/Select/index.ts +3 -0
  114. package/src/components/Select/useSelectKeyboard.ts +111 -0
  115. package/src/components/Separator/Separator.tsx +20 -0
  116. package/src/components/Separator/index.ts +1 -0
  117. package/src/components/Sheet/Sheet.tsx +136 -0
  118. package/src/components/Sheet/components/SheetBody.tsx +9 -0
  119. package/src/components/Sheet/components/SheetContext.tsx +23 -0
  120. package/src/components/Sheet/components/SheetFooter.tsx +9 -0
  121. package/src/components/Sheet/components/SheetHeader.tsx +29 -0
  122. package/src/components/Sheet/components/index.ts +9 -0
  123. package/src/components/Sheet/index.ts +13 -0
  124. package/src/components/Skeleton/Skeleton.tsx +7 -0
  125. package/src/components/Skeleton/index.ts +1 -0
  126. package/src/components/Sparkline/Sparkline.tsx +93 -0
  127. package/src/components/Sparkline/index.ts +1 -0
  128. package/src/components/Spinner/Spinner.tsx +41 -0
  129. package/src/components/Spinner/index.ts +1 -0
  130. package/src/components/StatsCard/StatsCard.tsx +40 -0
  131. package/src/components/StatsCard/index.ts +1 -0
  132. package/src/components/Switch/Switch.tsx +128 -0
  133. package/src/components/Switch/index.ts +1 -0
  134. package/src/components/Tabs/Tabs.tsx +49 -0
  135. package/src/components/Tabs/components/TabsContext.tsx +35 -0
  136. package/src/components/Tabs/components/TabsList.tsx +98 -0
  137. package/src/components/Tabs/components/TabsPanels.tsx +26 -0
  138. package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
  139. package/src/components/Tabs/index.ts +3 -0
  140. package/src/components/TagChip/TagChip.tsx +46 -0
  141. package/src/components/TagChip/index.ts +1 -0
  142. package/src/components/TagInput/TagInput.tsx +60 -0
  143. package/src/components/TagInput/index.ts +1 -0
  144. package/src/components/Text/Text.tsx +35 -0
  145. package/src/components/Text/index.ts +1 -0
  146. package/src/components/TextInline/TextInline.tsx +35 -0
  147. package/src/components/TextInline/index.ts +1 -0
  148. package/src/components/Textarea/Textarea.tsx +20 -0
  149. package/src/components/Textarea/index.ts +1 -0
  150. package/src/components/TimePicker/PeriodSelector.tsx +28 -0
  151. package/src/components/TimePicker/TimeColumn.tsx +62 -0
  152. package/src/components/TimePicker/TimePicker.tsx +323 -0
  153. package/src/components/TimePicker/index.ts +7 -0
  154. package/src/components/TimePicker/timepicker.hooks.ts +173 -0
  155. package/src/components/TimePicker/timepicker.types.ts +101 -0
  156. package/src/components/Toast/Toast.tsx +111 -0
  157. package/src/components/Toast/index.ts +1 -0
  158. package/src/components/Toggle/Toggle.tsx +72 -0
  159. package/src/components/Toggle/index.ts +1 -0
  160. package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
  161. package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
  162. package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
  163. package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
  164. package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
  165. package/src/components/ToggleGroup/index.ts +2 -0
  166. package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
  167. package/src/components/Tooltip/Tooltip.tsx +66 -0
  168. package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
  169. package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
  170. package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
  171. package/src/components/Tooltip/index.ts +9 -0
  172. package/src/date-time/arithmetic.ts +101 -0
  173. package/src/date-time/boundaries.ts +91 -0
  174. package/src/date-time/compare.ts +85 -0
  175. package/src/date-time/duration.ts +63 -0
  176. package/src/date-time/elapsed.ts +18 -0
  177. package/src/date-time/format.ts +722 -0
  178. package/src/date-time/index.ts +279 -0
  179. package/src/date-time/now.ts +70 -0
  180. package/src/date-time/parse.ts +170 -0
  181. package/src/date-time/serialize.ts +90 -0
  182. package/src/date-time/timezone.ts +47 -0
  183. package/src/date-time/validate.ts +278 -0
  184. package/src/hooks/index.ts +10 -0
  185. package/src/hooks/useCollapsible.ts +16 -0
  186. package/src/hooks/useControllableState.ts +54 -0
  187. package/src/hooks/useDebouncedCallback.ts +21 -0
  188. package/src/hooks/useDebouncedState.ts +20 -0
  189. package/src/hooks/useDeviceFormFactor.ts +49 -0
  190. package/src/hooks/useLocalStorage.ts +67 -0
  191. package/src/hooks/useMergedFloatingRef.ts +29 -0
  192. package/src/hooks/usePresence.ts +127 -0
  193. package/src/hooks/useStack.ts +99 -0
  194. package/src/hooks/useTheme.ts +105 -0
  195. package/src/internal/Slot.tsx +79 -0
  196. package/src/internal/index.ts +2 -0
  197. package/src/internal/rovingFocus.ts +39 -0
  198. package/src/internal/types.ts +10 -0
  199. package/src/lib/EmptyBoundary.tsx +12 -0
  200. package/src/lib/ErrorBoundary.tsx +48 -0
  201. package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
  202. package/src/lib/contexts/ComponentsContext/index.ts +1 -0
  203. package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
  204. package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
  205. package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
  206. package/src/lib/contexts/OverlayContext/index.ts +6 -0
  207. package/src/lib/format.ts +51 -0
  208. package/src/lib/utils.ts +10 -0
  209. package/src/lib/withEmpty.tsx +14 -0
  210. package/src/lib/withErrorBoundary.tsx +16 -0
  211. package/src/lib/withSuspense.tsx +16 -0
  212. package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
  213. package/src/styles/base.css +41 -0
  214. package/src/theme/index.ts +37 -0
  215. package/src/theme/palette.json +100 -0
  216. package/src/theme/palette.schema.json +40 -0
  217. package/src/theme/schema.ts +122 -0
  218. package/src/theme/theme.json +349 -0
  219. package/src/theme/theme.schema.json +876 -0
  220. package/src/theme/typography.json +7 -0
  221. package/src/theme/typography.schema.json +36 -0
  222. package/src/typography/Body.tsx +8 -0
  223. package/src/typography/Caption.tsx +8 -0
  224. package/src/typography/Code.tsx +8 -0
  225. package/src/typography/Display.tsx +8 -0
  226. package/src/typography/Emphasis.tsx +8 -0
  227. package/src/typography/Heading.tsx +23 -0
  228. package/src/typography/Label.tsx +10 -0
  229. package/src/typography/Lead.tsx +8 -0
  230. package/src/typography/Mono.tsx +8 -0
  231. package/src/typography/Muted.tsx +8 -0
  232. package/src/typography/SectionTitle.tsx +8 -0
  233. package/src/typography/Subtitle.tsx +8 -0
  234. package/src/typography/Timestamp.tsx +8 -0
  235. package/src/typography/index.ts +10 -0
  236. package/tailwind.config.ts +261 -0
@@ -0,0 +1,47 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { Button } from "../../Button";
3
+
4
+ export type Action = {
5
+ id: string;
6
+ label: string;
7
+ icon?: ReactNode;
8
+ onClick: () => void;
9
+ disabled?: boolean;
10
+ variant?: "solid" | "outline" | "ghost" | "link";
11
+ };
12
+
13
+ export type ToolbarProps = HTMLAttributes<HTMLDivElement> & {
14
+ actions?: Action[] | undefined;
15
+ primaryAction?: Action | undefined;
16
+ leftContent?: ReactNode;
17
+ rightContent?: ReactNode;
18
+ };
19
+
20
+ /**
21
+ * Toolbar component for code editor with actions and custom content areas
22
+ */
23
+ export const Toolbar = ({ actions = [], primaryAction, leftContent, rightContent, className, ...props }: ToolbarProps) => {
24
+ return (
25
+ <div {...props} className={`flex items-center justify-between border-border-normal border-b bg-bg-normal px-4 py-2 ${className ?? ""}`}>
26
+ {/* Left content */}
27
+ <div className="flex items-center gap-2">{leftContent}</div>
28
+
29
+ {/* Actions */}
30
+ <div className="flex items-center gap-2">
31
+ {actions.map((action) => (
32
+ <Button key={action.id} variant={action.variant || "ghost"} size="sm" onClick={action.onClick} disabled={action.disabled}>
33
+ {action.icon && <span className="mr-1">{action.icon}</span>}
34
+ {action.label}
35
+ </Button>
36
+ ))}
37
+ {primaryAction && (
38
+ <Button variant={primaryAction.variant || "solid"} size="sm" onClick={primaryAction.onClick} disabled={primaryAction.disabled}>
39
+ {primaryAction.icon && <span className="mr-1">{primaryAction.icon}</span>}
40
+ {primaryAction.label}
41
+ </Button>
42
+ )}
43
+ {rightContent}
44
+ </div>
45
+ </div>
46
+ );
47
+ };
@@ -0,0 +1,109 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { useState } from "react";
3
+ import { Editor } from "../Editor";
4
+ import type { ValidationError } from "../ValidationStatus";
5
+ import { ValidationStatus } from "../ValidationStatus";
6
+ import type { StatusBarItem } from "./StatusBar";
7
+ import { StatusBar } from "./StatusBar";
8
+ import type { Action } from "./Toolbar";
9
+ import { Toolbar } from "./Toolbar";
10
+
11
+ export type WorkspaceProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange"> & {
12
+ // Core editor props
13
+ value: string;
14
+ onChange: (value: string) => void;
15
+ readOnly?: boolean;
16
+ typeDefinitions?: string;
17
+ compilerOptions?: Record<string, unknown>;
18
+
19
+ // Toolbar
20
+ toolbar?: {
21
+ actions?: Action[];
22
+ primaryAction?: Action;
23
+ leftContent?: ReactNode;
24
+ rightContent?: ReactNode;
25
+ };
26
+
27
+ // Validation
28
+ onValidate?: (errors: ValidationError[]) => void;
29
+
30
+ // Status bar
31
+ statusBar?: {
32
+ items?: StatusBarItem[];
33
+ leftContent?: ReactNode;
34
+ rightContent?: ReactNode;
35
+ };
36
+ };
37
+
38
+ /**
39
+ * Comprehensive code editor workspace with toolbar, validation, and status bar
40
+ *
41
+ * Features:
42
+ * - Full Monaco editor with IntelliSense
43
+ * - Optional toolbar with actions
44
+ * - Built-in validation display
45
+ * - Status bar with metadata
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <Workspace
50
+ * value={code}
51
+ * onChange={setCode}
52
+ * toolbar={{
53
+ * actions: [{ id: "format", label: "Format", onClick: format }],
54
+ * primaryAction: { id: "save", label: "Save", onClick: save }
55
+ * }}
56
+ * />
57
+ * ```
58
+ */
59
+ export const Workspace = ({
60
+ value,
61
+ onChange,
62
+ readOnly = false,
63
+ typeDefinitions,
64
+ compilerOptions,
65
+ toolbar,
66
+ onValidate,
67
+ statusBar,
68
+ className,
69
+ ...props
70
+ }: WorkspaceProps) => {
71
+ const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
72
+
73
+ const handleValidate = (errors: ValidationError[]) => {
74
+ setValidationErrors(errors);
75
+ onValidate?.(errors);
76
+ };
77
+
78
+ return (
79
+ <div {...props} className={`flex flex-col overflow-hidden rounded-lg border border-border-normal bg-bg-normal ${className ?? ""}`}>
80
+ {/* Toolbar */}
81
+ {toolbar && (
82
+ <Toolbar actions={toolbar.actions} primaryAction={toolbar.primaryAction} leftContent={toolbar.leftContent} rightContent={toolbar.rightContent} />
83
+ )}
84
+
85
+ {/* Editor and validation */}
86
+ <div className="flex flex-1 flex-col overflow-hidden">
87
+ <Editor
88
+ value={value}
89
+ onChange={onChange}
90
+ onValidate={handleValidate}
91
+ readOnly={readOnly}
92
+ typeDefinitions={typeDefinitions}
93
+ compilerOptions={compilerOptions}
94
+ className="flex-1"
95
+ />
96
+
97
+ {/* Validation panel */}
98
+ {validationErrors.length > 0 && (
99
+ <div className="border-border-normal border-t bg-bg-normal p-4">
100
+ <ValidationStatus errors={validationErrors} />
101
+ </div>
102
+ )}
103
+ </div>
104
+
105
+ {/* Status bar */}
106
+ <StatusBar items={statusBar?.items} leftContent={statusBar?.leftContent} rightContent={statusBar?.rightContent} />
107
+ </div>
108
+ );
109
+ };
@@ -0,0 +1,3 @@
1
+ export { StatusBar, type StatusBarItem, type StatusBarProps } from "./StatusBar";
2
+ export { type Action, Toolbar, type ToolbarProps } from "./Toolbar";
3
+ export { Workspace, type WorkspaceProps } from "./Workspace";
@@ -0,0 +1,50 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import type { HTMLAttributes } from "react";
4
+
5
+ const INTENT_CLASSES: Record<string, string> = {
6
+ neutral: "bg-fg-subtle",
7
+ brand: "bg-bg-brand",
8
+ primary: "bg-bg-brand",
9
+ success: "bg-bg-success",
10
+ warning: "bg-bg-warning",
11
+ danger: "bg-bg-danger",
12
+ };
13
+
14
+ const colorDotVariants = cva("inline-block shrink-0 rounded-full", {
15
+ variants: {
16
+ size: {
17
+ sm: "h-1.5 w-1.5",
18
+ md: "h-2 w-2",
19
+ lg: "h-2.5 w-2.5",
20
+ },
21
+ pulse: {
22
+ true: "animate-pulse",
23
+ false: "",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ size: "md",
28
+ pulse: false,
29
+ },
30
+ });
31
+
32
+ export type ColorDotProps = HTMLAttributes<HTMLSpanElement> &
33
+ VariantProps<typeof colorDotVariants> & {
34
+ /** Intent name (neutral, brand, success, warning, danger) or arbitrary CSS color */
35
+ color: string;
36
+ };
37
+
38
+ export const ColorDot = ({ color, size, pulse, className, style, ...props }: ColorDotProps) => {
39
+ const intentClass = INTENT_CLASSES[color];
40
+
41
+ return (
42
+ <span
43
+ className={cn(colorDotVariants({ size, pulse }), intentClass, className)}
44
+ style={intentClass ? style : { ...style, backgroundColor: color }}
45
+ {...props}
46
+ />
47
+ );
48
+ };
49
+
50
+ export { colorDotVariants };
@@ -0,0 +1 @@
1
+ export { ColorDot, type ColorDotProps, colorDotVariants } from "./ColorDot";
@@ -0,0 +1,236 @@
1
+ import { autoUpdate, FloatingPortal, flip, offset, shift, useClick, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
2
+ import { useControllableState } from "@sixthshift/design-system/hooks";
3
+ import { cn } from "@sixthshift/design-system/utils";
4
+ import { Calendar as CalendarIcon, X } from "lucide-react";
5
+ import type * as React from "react";
6
+ import { useCallback, useId, useState } from "react";
7
+ import { isPlainDate, type Temporal, today } from "../../date-time";
8
+ import { Calendar } from "../Calendar";
9
+ import { defaultFormatDisplay, getDisplayValue, temporalToISO } from "./datepicker.hooks";
10
+ import type { DatePickerProps, DateRangeValue } from "./datepicker.types";
11
+
12
+ export type { DatePickerProps };
13
+
14
+ export const DatePicker = (props: DatePickerProps) => {
15
+ const {
16
+ mode = "single",
17
+ weekStartsOn = 0,
18
+ minDate,
19
+ maxDate,
20
+ disabled,
21
+ presets,
22
+ placeholder = "Select date",
23
+ isDisabled = false,
24
+ isInvalid = false,
25
+ name,
26
+ className,
27
+ align = "end",
28
+ clearable = true,
29
+ } = props;
30
+
31
+ // Get mode-specific props
32
+ const value = "value" in props ? props.value : undefined;
33
+ const defaultValue = "defaultValue" in props ? props.defaultValue : undefined;
34
+ const onChange = "onChange" in props ? props.onChange : undefined;
35
+ const max = mode === "multiple" && "max" in props ? props.max : undefined;
36
+
37
+ // Open state
38
+ const [open, setOpen] = useState(false);
39
+
40
+ // Controllable value state (external/committed value)
41
+ const [committedValue, setCommittedValue] = useControllableState({
42
+ value: value as Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined,
43
+ defaultValue: defaultValue as Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined,
44
+ onChange: onChange as ((value: Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined) => void) | undefined,
45
+ });
46
+
47
+ // Draft value (internal state while popup is open)
48
+ const [draftValue, setDraftValue] = useState<Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined>(committedValue);
49
+
50
+ // Sync draft when popup opens
51
+ const handleOpenChange = useCallback(
52
+ (newOpen: boolean) => {
53
+ if (newOpen) {
54
+ setDraftValue(committedValue);
55
+ }
56
+ setOpen(newOpen);
57
+ },
58
+ [committedValue]
59
+ );
60
+
61
+ // Month navigation state (uses Temporal.PlainDate for calendar grid)
62
+ const [month, setMonth] = useState<Temporal.PlainDate>(() => {
63
+ if (mode === "single" && isPlainDate(committedValue)) {
64
+ return committedValue;
65
+ }
66
+ if (mode === "range" && committedValue && typeof committedValue === "object" && "from" in committedValue && committedValue.from) {
67
+ return committedValue.from;
68
+ }
69
+ if (mode === "multiple" && Array.isArray(committedValue) && committedValue.length > 0 && committedValue[0]) {
70
+ return committedValue[0];
71
+ }
72
+ return today();
73
+ });
74
+
75
+ // Apply changes
76
+ const handleApply = useCallback(() => {
77
+ setCommittedValue(draftValue as Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[]);
78
+ setOpen(false);
79
+ }, [draftValue, setCommittedValue]);
80
+
81
+ // Cancel changes
82
+ const handleCancel = useCallback(() => {
83
+ setDraftValue(committedValue);
84
+ setOpen(false);
85
+ }, [committedValue]);
86
+
87
+ // Clear value
88
+ const handleClear = useCallback(
89
+ (e: React.MouseEvent) => {
90
+ e.stopPropagation();
91
+ e.preventDefault();
92
+ if (mode === "multiple") {
93
+ setCommittedValue([] as unknown as Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[]);
94
+ } else {
95
+ setCommittedValue(undefined);
96
+ }
97
+ },
98
+ [mode, setCommittedValue]
99
+ );
100
+
101
+ // Floating UI setup
102
+ const { refs, floatingStyles, context } = useFloating({
103
+ open,
104
+ onOpenChange: handleOpenChange,
105
+ placement: align === "start" ? "bottom-start" : "bottom-end",
106
+ middleware: [offset(8), flip(), shift({ padding: 8 })],
107
+ whileElementsMounted: autoUpdate,
108
+ });
109
+
110
+ const click = useClick(context);
111
+ const dismiss = useDismiss(context);
112
+ const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
113
+
114
+ // IDs for accessibility
115
+ const id = useId();
116
+ const contentId = `datepicker-content-${id}`;
117
+ const inputId = `datepicker-input-${id}`;
118
+
119
+ // Display value (from committed value)
120
+ const displayValue = getDisplayValue(mode, committedValue, defaultFormatDisplay);
121
+ const hasValue = mode === "multiple" ? Array.isArray(committedValue) && committedValue.length > 0 : Boolean(committedValue);
122
+
123
+ // Helper to get ISO string for form submission
124
+ const getFormValue = (): string | undefined => {
125
+ if (mode === "single" && isPlainDate(committedValue)) {
126
+ return temporalToISO(committedValue);
127
+ }
128
+ return undefined;
129
+ };
130
+
131
+ const getRangeFormValues = (): {
132
+ from: string | undefined;
133
+ to: string | undefined;
134
+ } => {
135
+ if (mode === "range" && committedValue) {
136
+ const range = committedValue as DateRangeValue;
137
+ return {
138
+ from: range.from ? temporalToISO(range.from) : undefined,
139
+ to: range.to ? temporalToISO(range.to) : undefined,
140
+ };
141
+ }
142
+ return { from: undefined, to: undefined };
143
+ };
144
+
145
+ const rangeFormValues = getRangeFormValues();
146
+
147
+ return (
148
+ <>
149
+ {/* Trigger Input */}
150
+ <div className={cn("relative flex items-center", mode === "range" && "min-w-91.25", mode !== "range" && "min-w-65")}>
151
+ <CalendarIcon className="pointer-events-none absolute left-3 h-4 w-4 text-fg-subtle" />
152
+ <input
153
+ ref={refs.setReference as React.Ref<HTMLInputElement>}
154
+ id={inputId}
155
+ type="text"
156
+ readOnly
157
+ value={displayValue}
158
+ placeholder={placeholder}
159
+ disabled={isDisabled}
160
+ role="combobox"
161
+ aria-invalid={isInvalid}
162
+ aria-haspopup="dialog"
163
+ aria-expanded={open}
164
+ aria-controls={open ? contentId : undefined}
165
+ className={cn(
166
+ `flex h-9 w-full cursor-pointer rounded-md border border-border-normal bg-transparent py-1 pl-9 text-sm shadow-xs transition-colors placeholder:text-fg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50`,
167
+ mode === "range" && "min-w-91.25",
168
+ mode !== "range" && "min-w-65",
169
+ clearable && hasValue ? "pr-9" : "pr-3",
170
+ isInvalid && "border-border-danger",
171
+ className
172
+ )}
173
+ {...getReferenceProps()}
174
+ />
175
+ {clearable && hasValue && !isDisabled && (
176
+ <button
177
+ type="button"
178
+ onClick={handleClear}
179
+ className="absolute right-3 rounded-sm p-0.5 text-fg-subtle hover:bg-bg-subtle hover:text-fg-normal focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring"
180
+ aria-label="Clear date"
181
+ >
182
+ <X className="h-3.5 w-3.5" />
183
+ </button>
184
+ )}
185
+
186
+ {/* Hidden inputs for form submission */}
187
+ {name && mode === "single" && committedValue && <input type="hidden" name={name} value={getFormValue()} />}
188
+ {name && mode === "range" && committedValue && (
189
+ <>
190
+ {rangeFormValues.from && <input type="hidden" name={`${name}.from`} value={rangeFormValues.from} />}
191
+ {rangeFormValues.to && <input type="hidden" name={`${name}.to`} value={rangeFormValues.to} />}
192
+ </>
193
+ )}
194
+ {name &&
195
+ mode === "multiple" &&
196
+ Array.isArray(committedValue) &&
197
+ // biome-ignore lint/suspicious/noArrayIndexKey: Hidden form inputs for stable date array
198
+ committedValue.map((date, i) => <input key={i} type="hidden" name={`${name}[]`} value={temporalToISO(date)} />)}
199
+ </div>
200
+
201
+ {/* Popover Content */}
202
+ {open && (
203
+ <FloatingPortal>
204
+ <div
205
+ ref={refs.setFloating}
206
+ id={contentId}
207
+ role="dialog"
208
+ aria-modal="true"
209
+ style={floatingStyles}
210
+ className="z-popover rounded-xl border border-border-normal bg-bg-normal p-4 shadow-lg"
211
+ {...getFloatingProps()}
212
+ >
213
+ {/* @ts-expect-error - TypeScript can't properly narrow discriminated union props across component boundaries */}
214
+ <Calendar
215
+ mode={mode}
216
+ value={draftValue}
217
+ onSelect={setDraftValue}
218
+ month={month}
219
+ onMonthChange={setMonth}
220
+ minDate={minDate}
221
+ maxDate={maxDate}
222
+ disabled={disabled}
223
+ weekStartsOn={weekStartsOn}
224
+ presets={presets}
225
+ showFooter
226
+ showToday={mode !== "range"}
227
+ onApply={handleApply}
228
+ onCancel={handleCancel}
229
+ {...(mode === "multiple" && max ? { max } : {})}
230
+ />
231
+ </div>
232
+ </FloatingPortal>
233
+ )}
234
+ </>
235
+ );
236
+ };