@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,251 @@
|
|
|
1
|
+
import { Tooltip } from "@sixthshift/design-system/tooltip";
|
|
2
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
|
|
5
|
+
export type LineChartDataPoint = {
|
|
6
|
+
label: string;
|
|
7
|
+
value: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type LineChartSeries = {
|
|
11
|
+
data: LineChartDataPoint[];
|
|
12
|
+
name?: string;
|
|
13
|
+
color?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type Interpolation = "linear" | "monotone" | "stepBefore" | "stepAfter";
|
|
17
|
+
|
|
18
|
+
export type LineChartProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
19
|
+
series: LineChartSeries[];
|
|
20
|
+
height?: number;
|
|
21
|
+
showGrid?: boolean;
|
|
22
|
+
showAxes?: boolean;
|
|
23
|
+
showDots?: boolean;
|
|
24
|
+
showLabels?: boolean;
|
|
25
|
+
showValues?: boolean;
|
|
26
|
+
interpolation?: Interpolation;
|
|
27
|
+
fillArea?: boolean;
|
|
28
|
+
yMin?: number;
|
|
29
|
+
yMax?: number;
|
|
30
|
+
yTicks?: number;
|
|
31
|
+
showTooltip?: boolean;
|
|
32
|
+
formatTooltip?: (point: LineChartDataPoint, seriesName?: string) => string;
|
|
33
|
+
formatValue?: (value: number) => string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const SERIES_COLORS = ["var(--fg-brand)", "var(--fg-success)", "var(--fg-warning)", "var(--fg-danger)", "var(--fg-subtle)"];
|
|
37
|
+
|
|
38
|
+
const PADDING = { top: 12, right: 16, bottom: 32, left: 48 };
|
|
39
|
+
const PADDING_NO_AXES = { top: 8, right: 8, bottom: 8, left: 8 };
|
|
40
|
+
|
|
41
|
+
function niceScale(min: number, max: number, ticks: number) {
|
|
42
|
+
// Defensive: a non-finite bound would make every derived tick NaN.
|
|
43
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
44
|
+
return { min: 0, max: 1, step: 1 / ticks };
|
|
45
|
+
}
|
|
46
|
+
if (min === max) {
|
|
47
|
+
const pad = min === 0 ? 1 : Math.abs(min) * 0.1;
|
|
48
|
+
return { min: min - pad, max: max + pad, step: (pad * 2) / ticks };
|
|
49
|
+
}
|
|
50
|
+
const range = max - min;
|
|
51
|
+
const roughStep = range / ticks;
|
|
52
|
+
const magnitude = 10 ** Math.floor(Math.log10(roughStep));
|
|
53
|
+
const residual = roughStep / magnitude;
|
|
54
|
+
const niceStep = residual <= 1.5 ? magnitude : residual <= 3 ? 2 * magnitude : residual <= 7 ? 5 * magnitude : 10 * magnitude;
|
|
55
|
+
const niceMin = Math.floor(min / niceStep) * niceStep;
|
|
56
|
+
const niceMax = Math.ceil(max / niceStep) * niceStep;
|
|
57
|
+
return { min: niceMin, max: niceMax, step: niceStep };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildPath(points: { x: number; y: number }[], interpolation: Interpolation): string {
|
|
61
|
+
if (points.length === 0) return "";
|
|
62
|
+
const parts = [`M ${points[0]!.x} ${points[0]!.y}`];
|
|
63
|
+
|
|
64
|
+
switch (interpolation) {
|
|
65
|
+
case "monotone":
|
|
66
|
+
for (let i = 1; i < points.length; i++) {
|
|
67
|
+
const prev = points[i - 1]!;
|
|
68
|
+
const curr = points[i]!;
|
|
69
|
+
const cpx = (prev.x + curr.x) / 2;
|
|
70
|
+
parts.push(`C ${cpx} ${prev.y}, ${cpx} ${curr.y}, ${curr.x} ${curr.y}`);
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "stepAfter":
|
|
74
|
+
for (let i = 1; i < points.length; i++) {
|
|
75
|
+
const prev = points[i - 1]!;
|
|
76
|
+
const curr = points[i]!;
|
|
77
|
+
parts.push(`L ${curr.x} ${prev.y}`);
|
|
78
|
+
parts.push(`L ${curr.x} ${curr.y}`);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "stepBefore":
|
|
82
|
+
for (let i = 1; i < points.length; i++) {
|
|
83
|
+
const prev = points[i - 1]!;
|
|
84
|
+
const curr = points[i]!;
|
|
85
|
+
parts.push(`L ${prev.x} ${curr.y}`);
|
|
86
|
+
parts.push(`L ${curr.x} ${curr.y}`);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
for (let i = 1; i < points.length; i++) {
|
|
91
|
+
parts.push(`L ${points[i]!.x} ${points[i]!.y}`);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return parts.join(" ");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const LineChart = ({
|
|
100
|
+
series,
|
|
101
|
+
height = 200,
|
|
102
|
+
showGrid = true,
|
|
103
|
+
showAxes = true,
|
|
104
|
+
showDots = true,
|
|
105
|
+
showLabels = true,
|
|
106
|
+
showValues = false,
|
|
107
|
+
interpolation = "monotone",
|
|
108
|
+
fillArea = false,
|
|
109
|
+
yMin: yMinProp,
|
|
110
|
+
yMax: yMaxProp,
|
|
111
|
+
yTicks = 4,
|
|
112
|
+
showTooltip = false,
|
|
113
|
+
formatTooltip,
|
|
114
|
+
formatValue = (v) => String(v),
|
|
115
|
+
className,
|
|
116
|
+
...rest
|
|
117
|
+
}: LineChartProps) => {
|
|
118
|
+
const allValues = series.flatMap((s) => s.data.map((d) => d.value));
|
|
119
|
+
// Math.min() of an empty list is Infinity, which propagates NaN through the
|
|
120
|
+
// whole scale. Fall back to a plain 0-1 axis when there is no data.
|
|
121
|
+
const rawMin = yMinProp ?? (allValues.length > 0 ? Math.min(...allValues) : 0);
|
|
122
|
+
const rawMax = yMaxProp ?? (allValues.length > 0 ? Math.max(...allValues) : 1);
|
|
123
|
+
const scale = niceScale(rawMin, rawMax, yTicks);
|
|
124
|
+
|
|
125
|
+
const labelSet = new Set<string>();
|
|
126
|
+
for (const s of series) {
|
|
127
|
+
for (const d of s.data) {
|
|
128
|
+
labelSet.add(d.label);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const labels = [...labelSet];
|
|
132
|
+
const labelCount = labels.length;
|
|
133
|
+
const labelIndex = new Map(labels.map((l, i) => [l, i]));
|
|
134
|
+
|
|
135
|
+
const padding = showAxes ? PADDING : PADDING_NO_AXES;
|
|
136
|
+
const viewWidth = 600;
|
|
137
|
+
const chartW = viewWidth - padding.left - padding.right;
|
|
138
|
+
const chartH = height - padding.top - padding.bottom;
|
|
139
|
+
|
|
140
|
+
const xStep = labelCount > 1 ? chartW / (labelCount - 1) : 0;
|
|
141
|
+
|
|
142
|
+
function toX(i: number) {
|
|
143
|
+
return padding.left + i * xStep;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toY(v: number) {
|
|
147
|
+
const ratio = (v - scale.min) / (scale.max - scale.min);
|
|
148
|
+
return padding.top + chartH - ratio * chartH;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const yTickValues: number[] = [];
|
|
152
|
+
for (let v = scale.min; v <= scale.max + scale.step * 0.01; v += scale.step) {
|
|
153
|
+
yTickValues.push(Math.round(v * 1e6) / 1e6);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function tooltipText(point: LineChartDataPoint, seriesName?: string) {
|
|
157
|
+
if (formatTooltip) return formatTooltip(point, seriesName);
|
|
158
|
+
const prefix = seriesName ? `${seriesName}: ` : `${point.label}: `;
|
|
159
|
+
return `${prefix}${formatValue(point.value)}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div {...rest} className={cn("relative w-full", className)}>
|
|
164
|
+
<svg viewBox={`0 0 ${viewWidth} ${height}`} className="w-full" preserveAspectRatio="xMidYMid meet" role="img">
|
|
165
|
+
<title>Line chart</title>
|
|
166
|
+
{showGrid &&
|
|
167
|
+
yTickValues.map((v) => (
|
|
168
|
+
<line
|
|
169
|
+
key={`grid-${v}`}
|
|
170
|
+
x1={padding.left}
|
|
171
|
+
y1={toY(v)}
|
|
172
|
+
x2={viewWidth - padding.right}
|
|
173
|
+
y2={toY(v)}
|
|
174
|
+
className="stroke-border-subtle"
|
|
175
|
+
strokeWidth={1}
|
|
176
|
+
strokeDasharray="4 4"
|
|
177
|
+
/>
|
|
178
|
+
))}
|
|
179
|
+
|
|
180
|
+
{showAxes && (
|
|
181
|
+
<>
|
|
182
|
+
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={padding.top + chartH} className="stroke-border-normal" strokeWidth={1} />
|
|
183
|
+
<line
|
|
184
|
+
x1={padding.left}
|
|
185
|
+
y1={padding.top + chartH}
|
|
186
|
+
x2={viewWidth - padding.right}
|
|
187
|
+
y2={padding.top + chartH}
|
|
188
|
+
className="stroke-border-normal"
|
|
189
|
+
strokeWidth={1}
|
|
190
|
+
/>
|
|
191
|
+
{yTickValues.map((v) => (
|
|
192
|
+
<text key={`ytick-${v}`} x={padding.left - 8} y={toY(v)} textAnchor="end" dominantBaseline="middle" className="fill-fg-subtle" fontSize={11}>
|
|
193
|
+
{formatValue(v)}
|
|
194
|
+
</text>
|
|
195
|
+
))}
|
|
196
|
+
</>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{showLabels &&
|
|
200
|
+
labels.map((label, i) => (
|
|
201
|
+
<text key={`xlabel-${label}`} x={toX(i)} y={height - 4} textAnchor="middle" className="fill-fg-subtle" fontSize={11}>
|
|
202
|
+
{label}
|
|
203
|
+
</text>
|
|
204
|
+
))}
|
|
205
|
+
|
|
206
|
+
{series.map((s, si) => {
|
|
207
|
+
const color = s.color ?? SERIES_COLORS[si % SERIES_COLORS.length];
|
|
208
|
+
const points = s.data.map((d) => ({ x: toX(labelIndex.get(d.label)!), y: toY(d.value) }));
|
|
209
|
+
const path = buildPath(points, interpolation);
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<g key={s.name ?? si}>
|
|
213
|
+
{fillArea && path && (
|
|
214
|
+
<path
|
|
215
|
+
d={`${path} L ${points[points.length - 1]!.x} ${padding.top + chartH} L ${points[0]!.x} ${padding.top + chartH} Z`}
|
|
216
|
+
fill={color}
|
|
217
|
+
opacity={0.1}
|
|
218
|
+
/>
|
|
219
|
+
)}
|
|
220
|
+
{path && <path d={path} fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />}
|
|
221
|
+
{showDots && points.map((p, pi) => <circle key={`dot-${s.data[pi]!.label}`} cx={p.x} cy={p.y} r={3} fill={color} />)}
|
|
222
|
+
{showValues &&
|
|
223
|
+
points.map((p, vi) => (
|
|
224
|
+
<text key={`val-${s.data[vi]!.label}`} x={p.x} y={p.y - 10} textAnchor="middle" className="fill-fg-normal" fontSize={10} fontWeight={600}>
|
|
225
|
+
{formatValue(s.data[vi]!.value)}
|
|
226
|
+
</text>
|
|
227
|
+
))}
|
|
228
|
+
</g>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
</svg>
|
|
232
|
+
|
|
233
|
+
{showTooltip &&
|
|
234
|
+
series.map((s, si) =>
|
|
235
|
+
s.data.map((d) => {
|
|
236
|
+
const xPct = (toX(labelIndex.get(d.label)!) / viewWidth) * 100;
|
|
237
|
+
const yPct = (toY(d.value) / height) * 100;
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<Tooltip key={`tip-${s.name ?? si}-${d.label}`} delayShow={0}>
|
|
241
|
+
<Tooltip.Trigger asChild>
|
|
242
|
+
<span className="absolute block -translate-x-1/2 -translate-y-1/2" style={{ left: `${xPct}%`, top: `${yPct}%`, width: 16, height: 16 }} />
|
|
243
|
+
</Tooltip.Trigger>
|
|
244
|
+
<Tooltip.Body>{tooltipText(d, s.name)}</Tooltip.Body>
|
|
245
|
+
</Tooltip>
|
|
246
|
+
);
|
|
247
|
+
})
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type Interpolation, LineChart, type LineChartDataPoint, type LineChartProps, type LineChartSeries } from "./LineChart";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Body } from "@sixthshift/design-system/body";
|
|
2
|
+
import { Emphasis } from "@sixthshift/design-system/emphasis";
|
|
3
|
+
import { Heading } from "@sixthshift/design-system/heading";
|
|
4
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
|
|
7
|
+
export type MarkdownProps = {
|
|
8
|
+
children: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Markdown = ({ children, className }: MarkdownProps) => (
|
|
13
|
+
<div className={cn("flex flex-col", className)}>
|
|
14
|
+
<ReactMarkdown
|
|
15
|
+
components={{
|
|
16
|
+
h1: ({ children }) => <Heading as="h3">{children}</Heading>,
|
|
17
|
+
h2: ({ children }) => <Heading as="h4">{children}</Heading>,
|
|
18
|
+
h3: ({ children }) => <Emphasis className="block">{children}</Emphasis>,
|
|
19
|
+
p: ({ children }) => (
|
|
20
|
+
<Body as="p" className="mt-2">
|
|
21
|
+
{children}
|
|
22
|
+
</Body>
|
|
23
|
+
),
|
|
24
|
+
a: ({ href, children }) => (
|
|
25
|
+
<a href={href} target="_blank" rel="noopener noreferrer" className="text-fg-brand underline">
|
|
26
|
+
{children}
|
|
27
|
+
</a>
|
|
28
|
+
),
|
|
29
|
+
ul: ({ children }) => <ul className="mt-2 flex list-disc flex-col gap-1 pl-5">{children}</ul>,
|
|
30
|
+
ol: ({ children }) => <ol className="mt-2 flex list-decimal flex-col gap-1 pl-5">{children}</ol>,
|
|
31
|
+
li: ({ children }) => <li className="text-fg-default text-sm">{children}</li>,
|
|
32
|
+
code: ({ children }) => <code className="rounded bg-bg-subtle px-1.5 py-0.5 font-mono text-fg-emphasis text-xs">{children}</code>,
|
|
33
|
+
pre: ({ children }) => <pre className="mt-2 overflow-x-auto rounded-md bg-bg-subtle p-3 font-mono text-xs">{children}</pre>,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</ReactMarkdown>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
Markdown.displayName = "Markdown";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Markdown, type MarkdownProps } from "./Markdown";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { MessageBody } from "./components/MessageBody";
|
|
5
|
+
import { MessageDescription } from "./components/MessageDescription";
|
|
6
|
+
import { MessageIcon } from "./components/MessageIcon";
|
|
7
|
+
import { MessageTitle } from "./components/MessageTitle";
|
|
8
|
+
|
|
9
|
+
export const messageVariants = cva("flex gap-3 rounded-lg border p-4 text-sm [&>svg]:shrink-0", {
|
|
10
|
+
variants: {
|
|
11
|
+
intent: {
|
|
12
|
+
neutral: "border-border-normal bg-bg-normal text-fg-normal",
|
|
13
|
+
success: "border-border-success bg-bg-success-subtle text-fg-on-success-subtle",
|
|
14
|
+
warning: "border-border-warning bg-bg-warning-subtle text-fg-on-warning-subtle",
|
|
15
|
+
danger: "border-border-danger bg-bg-danger-subtle text-fg-on-danger-subtle",
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: "p-4",
|
|
19
|
+
sm: "gap-2 p-3 text-xs",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
intent: "neutral",
|
|
24
|
+
size: "default",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type MessageProps = React.HTMLAttributes<HTMLDivElement> &
|
|
29
|
+
VariantProps<typeof messageVariants> & {
|
|
30
|
+
/** Simple API: Optional title displayed above children */
|
|
31
|
+
title?: React.ReactNode;
|
|
32
|
+
/** Simple API: Optional icon displayed on the left */
|
|
33
|
+
icon?: React.ReactNode;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Message = React.forwardRef<HTMLDivElement, MessageProps>(({ className, intent = "neutral", size, title, icon, children, ...props }, ref) => {
|
|
37
|
+
const hasCompoundChildren = React.Children.toArray(children).some(
|
|
38
|
+
(child) =>
|
|
39
|
+
React.isValidElement(child) &&
|
|
40
|
+
(child.type === MessageIcon || child.type === MessageBody || child.type === MessageTitle || child.type === MessageDescription)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div ref={ref} role="alert" className={cn(messageVariants({ intent, size, className }))} {...props}>
|
|
45
|
+
{hasCompoundChildren ? (
|
|
46
|
+
children
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
{icon && <MessageIcon>{icon}</MessageIcon>}
|
|
50
|
+
<MessageBody>
|
|
51
|
+
{title && <MessageTitle>{title}</MessageTitle>}
|
|
52
|
+
<MessageDescription>{children}</MessageDescription>
|
|
53
|
+
</MessageBody>
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
Message.displayName = "Message";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type MessageBodyProps = React.HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
export const MessageBody = React.forwardRef<HTMLDivElement, MessageBodyProps>(({ className, ...props }, ref) => {
|
|
7
|
+
return <div ref={ref} className={cn("flex min-w-0 flex-1 flex-col gap-1", className)} {...props} />;
|
|
8
|
+
});
|
|
9
|
+
MessageBody.displayName = "MessageBody";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type MessageDescriptionProps = React.HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
export const MessageDescription = React.forwardRef<HTMLDivElement, MessageDescriptionProps>(({ className, ...props }, ref) => {
|
|
7
|
+
return <div ref={ref} className={cn("leading-relaxed opacity-90", className)} {...props} />;
|
|
8
|
+
});
|
|
9
|
+
MessageDescription.displayName = "MessageDescription";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type MessageIconProps = React.HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
export const MessageIcon = React.forwardRef<HTMLDivElement, MessageIconProps>(({ className, children, ...props }, ref) => {
|
|
7
|
+
return (
|
|
8
|
+
<div ref={ref} className={cn("flex h-4 w-4 shrink-0", className)} {...props}>
|
|
9
|
+
{children}
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
MessageIcon.displayName = "MessageIcon";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type MessageTitleProps = React.HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
export const MessageTitle = React.forwardRef<HTMLDivElement, MessageTitleProps>(({ className, ...props }, ref) => {
|
|
7
|
+
return <div ref={ref} className={cn("font-medium leading-none", className)} {...props} />;
|
|
8
|
+
});
|
|
9
|
+
MessageTitle.displayName = "MessageTitle";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { MessageBodyProps } from "./components/MessageBody";
|
|
2
|
+
export { MessageBody } from "./components/MessageBody";
|
|
3
|
+
export type { MessageDescriptionProps } from "./components/MessageDescription";
|
|
4
|
+
export { MessageDescription } from "./components/MessageDescription";
|
|
5
|
+
export type { MessageIconProps } from "./components/MessageIcon";
|
|
6
|
+
export { MessageIcon } from "./components/MessageIcon";
|
|
7
|
+
export type { MessageTitleProps } from "./components/MessageTitle";
|
|
8
|
+
export { MessageTitle } from "./components/MessageTitle";
|
|
9
|
+
export type { MessageProps } from "./Message";
|
|
10
|
+
export { Message, messageVariants } from "./Message";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type MetricListProps = {
|
|
5
|
+
/** Metric rows */
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/** Additional CSS classes */
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const MetricList = ({ children, className }: MetricListProps) => {
|
|
12
|
+
return <div className={cn("flex flex-col gap-2 text-fg-subtle", className)}>{children}</div>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type MetricRowProps = {
|
|
5
|
+
/** Label displayed on the left */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Value displayed on the right, can be string or ReactNode for complex content */
|
|
8
|
+
value: ReactNode;
|
|
9
|
+
/** Optional variant for styling the value */
|
|
10
|
+
valueVariant?: "normal" | "success" | "warning" | "danger" | "info";
|
|
11
|
+
/** Additional CSS classes */
|
|
12
|
+
className?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const valueVariants = {
|
|
16
|
+
normal: "text-fg-normal",
|
|
17
|
+
success: "text-fg-success",
|
|
18
|
+
warning: "text-fg-warning",
|
|
19
|
+
danger: "text-fg-danger",
|
|
20
|
+
info: "text-fg-brand",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const MetricRow = ({ label, value, valueVariant = "normal", className }: MetricRowProps) => {
|
|
24
|
+
return (
|
|
25
|
+
<div className={cn("flex justify-between", className)}>
|
|
26
|
+
<span>{label}</span>
|
|
27
|
+
<span className={valueVariants[valueVariant]}>{value}</span>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { FloatingFocusManager, FloatingOverlay, useDismiss, useFloating, useInteractions, useRole } from "@floating-ui/react";
|
|
2
|
+
import { useMergedFloatingRef, usePresence } from "@sixthshift/design-system/hooks";
|
|
3
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
4
|
+
import { type HTMLAttributes, type ReactNode, useCallback, useContext, useEffect, useMemo } from "react";
|
|
5
|
+
import { ModalBody, ModalContext, ModalFooter, ModalHeader } from "./components";
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
export type ModalProps = Pick<HTMLAttributes<HTMLDivElement>, "className" | "style"> & {
|
|
12
|
+
/**
|
|
13
|
+
* Called when the modal's open state should change. Today Modal is mount-driven
|
|
14
|
+
* (parent conditionally renders), so this fires only with `false` on dismissal.
|
|
15
|
+
* The signature matches Popover/Tooltip/HoverCard/Tabs for API consistency and
|
|
16
|
+
* leaves room to evolve Modal into a state-driven primitive later.
|
|
17
|
+
*/
|
|
18
|
+
onOpenChange?: (open: boolean) => void;
|
|
19
|
+
/** Modal size */
|
|
20
|
+
size?: "sm" | "md" | "lg" | "full";
|
|
21
|
+
/** Children - use Modal.Header, Modal.Body, Modal.Footer */
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* When true (default), clicking outside closes the modal.
|
|
25
|
+
* When false, the modal can only be closed programmatically.
|
|
26
|
+
* Note: Escape key is handled by OverlayContext for proper stack ordering.
|
|
27
|
+
*/
|
|
28
|
+
dismissable?: boolean;
|
|
29
|
+
/** When true, shows a close (X) button in the modal header */
|
|
30
|
+
closable?: boolean;
|
|
31
|
+
/** Vertical alignment on desktop. "center" (default) or "top" (anchored near top, content grows down) */
|
|
32
|
+
align?: "center" | "top";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Size classes
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
const sizeClasses = {
|
|
40
|
+
sm: "sm:w-80",
|
|
41
|
+
md: "sm:w-[28rem]",
|
|
42
|
+
lg: "sm:w-[36rem]",
|
|
43
|
+
full: "sm:w-[95%] sm:h-[95%]",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Modal
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
const ModalRoot = ({ onOpenChange, size = "md", className, style, children, dismissable = true, closable, align = "center" }: ModalProps) => {
|
|
51
|
+
// Prefer explicit prop over context (context is set by useModal's overlay system)
|
|
52
|
+
const contextValue = useContext(ModalContext);
|
|
53
|
+
// Context provides a parameterless onClose for programmatic modals; adapt to the boolean shape.
|
|
54
|
+
const handleDismiss = useCallback(() => {
|
|
55
|
+
if (onOpenChange) onOpenChange(false);
|
|
56
|
+
else contextValue?.onClose();
|
|
57
|
+
}, [onOpenChange, contextValue]);
|
|
58
|
+
|
|
59
|
+
const { ref: presenceRef, state, isMounted, show, hide } = usePresence();
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
show();
|
|
63
|
+
}, [show]);
|
|
64
|
+
|
|
65
|
+
const handleClose = useCallback(() => {
|
|
66
|
+
hide(handleDismiss);
|
|
67
|
+
}, [handleDismiss, hide]);
|
|
68
|
+
|
|
69
|
+
const { refs, context } = useFloating({
|
|
70
|
+
open: true,
|
|
71
|
+
onOpenChange: (nextOpen) => {
|
|
72
|
+
if (!nextOpen) handleClose();
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const mergedRef = useMergedFloatingRef(refs, presenceRef);
|
|
77
|
+
|
|
78
|
+
const dismiss = useDismiss(context, {
|
|
79
|
+
enabled: dismissable,
|
|
80
|
+
// OverlayContext handles escape key for proper stack ordering
|
|
81
|
+
escapeKey: false,
|
|
82
|
+
outsidePress: true,
|
|
83
|
+
outsidePressEvent: "mousedown",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const role = useRole(context, {
|
|
87
|
+
role: "dialog",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { getFloatingProps } = useInteractions([dismiss, role]);
|
|
91
|
+
|
|
92
|
+
const modalContextValue = useMemo(() => ({ onClose: handleClose, closable }), [closable, handleClose]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
93
|
+
|
|
94
|
+
if (!isMounted) return null;
|
|
95
|
+
|
|
96
|
+
const isEntering = state === "entering";
|
|
97
|
+
const isExiting = state === "exiting";
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<FloatingFocusManager context={context} modal>
|
|
101
|
+
<FloatingOverlay lockScroll className={cn("fixed inset-0 z-modal bg-bg-overlay", isEntering && "animate-fade-in", isExiting && "animate-fade-out")}>
|
|
102
|
+
<div
|
|
103
|
+
ref={mergedRef}
|
|
104
|
+
role="dialog"
|
|
105
|
+
aria-modal="true"
|
|
106
|
+
tabIndex={-1}
|
|
107
|
+
data-state={state}
|
|
108
|
+
className={cn(
|
|
109
|
+
"fixed flex flex-col overflow-hidden outline-hidden",
|
|
110
|
+
"rounded-xl border border-border-normal bg-bg-normal",
|
|
111
|
+
// Size
|
|
112
|
+
sizeClasses[size],
|
|
113
|
+
// Cap height so ModalBody scrolls instead of overflowing the viewport
|
|
114
|
+
"max-h-[95%]",
|
|
115
|
+
// Mobile: slide up from bottom, full width
|
|
116
|
+
"max-sm:inset-x-0 max-sm:bottom-0 max-sm:rounded-b-none",
|
|
117
|
+
isEntering && "max-sm:animate-slide-up-in",
|
|
118
|
+
isExiting && "max-sm:animate-slide-up-out",
|
|
119
|
+
// Desktop: horizontal center + vertical alignment
|
|
120
|
+
"sm:left-1/2 sm:-translate-x-1/2",
|
|
121
|
+
align === "center" && "sm:top-1/2 sm:-translate-y-1/2",
|
|
122
|
+
align === "top" && "sm:top-[15vh]",
|
|
123
|
+
isEntering && "sm:animate-fade-in",
|
|
124
|
+
isExiting && "sm:animate-fade-out",
|
|
125
|
+
className
|
|
126
|
+
)}
|
|
127
|
+
style={style}
|
|
128
|
+
{...getFloatingProps({})}
|
|
129
|
+
>
|
|
130
|
+
<ModalContext.Provider value={modalContextValue}>{children}</ModalContext.Provider>
|
|
131
|
+
</div>
|
|
132
|
+
</FloatingOverlay>
|
|
133
|
+
</FloatingFocusManager>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
ModalRoot.displayName = "Modal";
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Compound Component Export
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
export const Modal = Object.assign(ModalRoot, {
|
|
143
|
+
Header: ModalHeader,
|
|
144
|
+
Body: ModalBody,
|
|
145
|
+
Footer: ModalFooter,
|
|
146
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type ModalBodyProps = React.HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
export const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>(({ className, ...props }, ref) => (
|
|
7
|
+
<div ref={ref} className={cn("flex-1 overflow-y-auto p-6 pt-0", className)} {...props} />
|
|
8
|
+
));
|
|
9
|
+
ModalBody.displayName = "ModalBody";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, type PropsWithChildren, useContext, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal close signal for a mounted modal. App-level callback — no event arg.
|
|
5
|
+
* Used by ModalHeader's X button and by `useModal`'s programmatic overlay system.
|
|
6
|
+
*/
|
|
7
|
+
export type OnClose = () => void;
|
|
8
|
+
|
|
9
|
+
interface ModalContextValue {
|
|
10
|
+
onClose: OnClose;
|
|
11
|
+
closable?: boolean | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ModalContext = createContext<ModalContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export function useModalContext() {
|
|
17
|
+
const context = useContext(ModalContext);
|
|
18
|
+
if (!context) {
|
|
19
|
+
throw new Error("useModalContext must be used within a Modal");
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ModalContextProvider = ({ onClose, closable, children }: PropsWithChildren<{ onClose: OnClose; closable?: boolean }>) => {
|
|
25
|
+
const value = useMemo(() => ({ onClose, closable }), [onClose, closable]);
|
|
26
|
+
return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>;
|
|
27
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type ModalFooterProps = React.HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
export const ModalFooter = React.forwardRef<HTMLDivElement, ModalFooterProps>(({ className, ...props }, ref) => (
|
|
7
|
+
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
8
|
+
));
|
|
9
|
+
ModalFooter.displayName = "ModalFooter";
|