@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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/styles.css +2 -0
- package/dist/theme.generated.css +438 -0
- package/package.json +238 -0
- package/src/components/Avatar/Avatar.tsx +19 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.tsx +59 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BarChart/BarChart.tsx +51 -0
- package/src/components/BarChart/index.ts +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
- package/src/components/Breadcrumb/index.ts +5 -0
- package/src/components/Button/Button.tsx +157 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Calendar/Calendar.tsx +433 -0
- package/src/components/Calendar/calendar.hooks.ts +329 -0
- package/src/components/Calendar/calendar.types.ts +117 -0
- package/src/components/Calendar/index.ts +28 -0
- package/src/components/Card/Card.tsx +47 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.tsx +144 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
- package/src/components/CheckboxGroup/index.ts +5 -0
- package/src/components/Code/Editor/Editor.tsx +161 -0
- package/src/components/Code/Editor/index.ts +2 -0
- package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
- package/src/components/Code/Editor/themes.ts +112 -0
- package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
- package/src/components/Code/ValidationStatus/index.ts +1 -0
- package/src/components/Code/Workspace/StatusBar.tsx +47 -0
- package/src/components/Code/Workspace/Toolbar.tsx +47 -0
- package/src/components/Code/Workspace/Workspace.tsx +109 -0
- package/src/components/Code/Workspace/index.ts +3 -0
- package/src/components/ColorDot/ColorDot.tsx +50 -0
- package/src/components/ColorDot/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.tsx +236 -0
- package/src/components/DatePicker/datepicker.hooks.ts +381 -0
- package/src/components/DatePicker/datepicker.types.ts +140 -0
- package/src/components/DatePicker/index.ts +12 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
- package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
- package/src/components/DateRangePicker/index.ts +2 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
- package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
- package/src/components/DateTimePicker/index.ts +2 -0
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
- package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
- package/src/components/DateTimeRangePicker/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +29 -0
- package/src/components/EmptyState/index.ts +1 -0
- package/src/components/Field/Field.tsx +60 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +55 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
- package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
- package/src/components/HeatMap/heat-map.utils.ts +15 -0
- package/src/components/HeatMap/index.ts +3 -0
- package/src/components/HoverCard/HoverCard.tsx +83 -0
- package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
- package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
- package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
- package/src/components/HoverCard/index.ts +3 -0
- package/src/components/Input/Input.tsx +48 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/LineChart/LineChart.tsx +251 -0
- package/src/components/LineChart/index.ts +1 -0
- package/src/components/Markdown/Markdown.tsx +40 -0
- package/src/components/Markdown/index.ts +1 -0
- package/src/components/Message/Message.tsx +59 -0
- package/src/components/Message/components/MessageBody.tsx +9 -0
- package/src/components/Message/components/MessageDescription.tsx +9 -0
- package/src/components/Message/components/MessageIcon.tsx +13 -0
- package/src/components/Message/components/MessageTitle.tsx +9 -0
- package/src/components/Message/index.ts +10 -0
- package/src/components/MetricList/MetricList.tsx +13 -0
- package/src/components/MetricList/MetricRow.tsx +30 -0
- package/src/components/MetricList/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +146 -0
- package/src/components/Modal/components/ModalBody.tsx +9 -0
- package/src/components/Modal/components/ModalContext.tsx +27 -0
- package/src/components/Modal/components/ModalFooter.tsx +9 -0
- package/src/components/Modal/components/ModalHeader.tsx +29 -0
- package/src/components/Modal/components/index.ts +9 -0
- package/src/components/Modal/index.ts +13 -0
- package/src/components/NavBottom/NavBottom.tsx +53 -0
- package/src/components/NavBottom/index.ts +3 -0
- package/src/components/NavSide/NavItem.tsx +39 -0
- package/src/components/NavSide/NavSide.tsx +40 -0
- package/src/components/NavSide/index.ts +2 -0
- package/src/components/NavSide/types.ts +36 -0
- package/src/components/Pagination/Pagination.tsx +86 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Popover/Popover.tsx +68 -0
- package/src/components/Popover/components/PopoverBody.tsx +32 -0
- package/src/components/Popover/components/PopoverClose.tsx +26 -0
- package/src/components/Popover/components/PopoverContext.tsx +24 -0
- package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
- package/src/components/Popover/index.ts +10 -0
- package/src/components/ProgressBar/ProgressBar.tsx +42 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.tsx +89 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
- package/src/components/RadioButtonGroup/index.ts +5 -0
- package/src/components/SearchInput/SearchInput.tsx +45 -0
- package/src/components/SearchInput/index.ts +2 -0
- package/src/components/Select/Select.tsx +289 -0
- package/src/components/Select/SelectDropdown.tsx +90 -0
- package/src/components/Select/SelectTrigger.tsx +130 -0
- package/src/components/Select/index.ts +3 -0
- package/src/components/Select/useSelectKeyboard.ts +111 -0
- package/src/components/Separator/Separator.tsx +20 -0
- package/src/components/Separator/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +136 -0
- package/src/components/Sheet/components/SheetBody.tsx +9 -0
- package/src/components/Sheet/components/SheetContext.tsx +23 -0
- package/src/components/Sheet/components/SheetFooter.tsx +9 -0
- package/src/components/Sheet/components/SheetHeader.tsx +29 -0
- package/src/components/Sheet/components/index.ts +9 -0
- package/src/components/Sheet/index.ts +13 -0
- package/src/components/Skeleton/Skeleton.tsx +7 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Sparkline/Sparkline.tsx +93 -0
- package/src/components/Sparkline/index.ts +1 -0
- package/src/components/Spinner/Spinner.tsx +41 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/StatsCard/StatsCard.tsx +40 -0
- package/src/components/StatsCard/index.ts +1 -0
- package/src/components/Switch/Switch.tsx +128 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +49 -0
- package/src/components/Tabs/components/TabsContext.tsx +35 -0
- package/src/components/Tabs/components/TabsList.tsx +98 -0
- package/src/components/Tabs/components/TabsPanels.tsx +26 -0
- package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/TagChip/TagChip.tsx +46 -0
- package/src/components/TagChip/index.ts +1 -0
- package/src/components/TagInput/TagInput.tsx +60 -0
- package/src/components/TagInput/index.ts +1 -0
- package/src/components/Text/Text.tsx +35 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/TextInline/TextInline.tsx +35 -0
- package/src/components/TextInline/index.ts +1 -0
- package/src/components/Textarea/Textarea.tsx +20 -0
- package/src/components/Textarea/index.ts +1 -0
- package/src/components/TimePicker/PeriodSelector.tsx +28 -0
- package/src/components/TimePicker/TimeColumn.tsx +62 -0
- package/src/components/TimePicker/TimePicker.tsx +323 -0
- package/src/components/TimePicker/index.ts +7 -0
- package/src/components/TimePicker/timepicker.hooks.ts +173 -0
- package/src/components/TimePicker/timepicker.types.ts +101 -0
- package/src/components/Toast/Toast.tsx +111 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Toggle/Toggle.tsx +72 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
- package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
- package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
- package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
- package/src/components/Tooltip/Tooltip.tsx +66 -0
- package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
- package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
- package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
- package/src/components/Tooltip/index.ts +9 -0
- package/src/date-time/arithmetic.ts +101 -0
- package/src/date-time/boundaries.ts +91 -0
- package/src/date-time/compare.ts +85 -0
- package/src/date-time/duration.ts +63 -0
- package/src/date-time/elapsed.ts +18 -0
- package/src/date-time/format.ts +722 -0
- package/src/date-time/index.ts +279 -0
- package/src/date-time/now.ts +70 -0
- package/src/date-time/parse.ts +170 -0
- package/src/date-time/serialize.ts +90 -0
- package/src/date-time/timezone.ts +47 -0
- package/src/date-time/validate.ts +278 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCollapsible.ts +16 -0
- package/src/hooks/useControllableState.ts +54 -0
- package/src/hooks/useDebouncedCallback.ts +21 -0
- package/src/hooks/useDebouncedState.ts +20 -0
- package/src/hooks/useDeviceFormFactor.ts +49 -0
- package/src/hooks/useLocalStorage.ts +67 -0
- package/src/hooks/useMergedFloatingRef.ts +29 -0
- package/src/hooks/usePresence.ts +127 -0
- package/src/hooks/useStack.ts +99 -0
- package/src/hooks/useTheme.ts +105 -0
- package/src/internal/Slot.tsx +79 -0
- package/src/internal/index.ts +2 -0
- package/src/internal/rovingFocus.ts +39 -0
- package/src/internal/types.ts +10 -0
- package/src/lib/EmptyBoundary.tsx +12 -0
- package/src/lib/ErrorBoundary.tsx +48 -0
- package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
- package/src/lib/contexts/ComponentsContext/index.ts +1 -0
- package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
- package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
- package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
- package/src/lib/contexts/OverlayContext/index.ts +6 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/utils.ts +10 -0
- package/src/lib/withEmpty.tsx +14 -0
- package/src/lib/withErrorBoundary.tsx +16 -0
- package/src/lib/withSuspense.tsx +16 -0
- package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
- package/src/styles/base.css +41 -0
- package/src/theme/index.ts +37 -0
- package/src/theme/palette.json +100 -0
- package/src/theme/palette.schema.json +40 -0
- package/src/theme/schema.ts +122 -0
- package/src/theme/theme.json +349 -0
- package/src/theme/theme.schema.json +876 -0
- package/src/theme/typography.json +7 -0
- package/src/theme/typography.schema.json +36 -0
- package/src/typography/Body.tsx +8 -0
- package/src/typography/Caption.tsx +8 -0
- package/src/typography/Code.tsx +8 -0
- package/src/typography/Display.tsx +8 -0
- package/src/typography/Emphasis.tsx +8 -0
- package/src/typography/Heading.tsx +23 -0
- package/src/typography/Label.tsx +10 -0
- package/src/typography/Lead.tsx +8 -0
- package/src/typography/Mono.tsx +8 -0
- package/src/typography/Muted.tsx +8 -0
- package/src/typography/SectionTitle.tsx +8 -0
- package/src/typography/Subtitle.tsx +8 -0
- package/src/typography/Timestamp.tsx +8 -0
- package/src/typography/index.ts +10 -0
- 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,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
|
+
};
|