@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,62 @@
|
|
|
1
|
+
import type { Temporal, WeekStartsOn } from "../../date-time";
|
|
2
|
+
|
|
3
|
+
export type DisabledDateMatcher =
|
|
4
|
+
| Temporal.PlainDate
|
|
5
|
+
| Temporal.PlainDate[]
|
|
6
|
+
| ((date: Temporal.PlainDate) => boolean)
|
|
7
|
+
| { before: Temporal.PlainDate }
|
|
8
|
+
| { after: Temporal.PlainDate }
|
|
9
|
+
| { from: Temporal.PlainDate; to: Temporal.PlainDate };
|
|
10
|
+
|
|
11
|
+
export type DateTimeRangeValue = {
|
|
12
|
+
from: Temporal.Instant | undefined;
|
|
13
|
+
to: Temporal.Instant | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type DateTimeRangePresetOption = {
|
|
17
|
+
label: string;
|
|
18
|
+
value: () => DateTimeRangeValue;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type DateTimeRangePickerProps = {
|
|
22
|
+
// Value (controlled/uncontrolled) - NOW USES INSTANT
|
|
23
|
+
value?: DateTimeRangeValue | undefined;
|
|
24
|
+
defaultValue?: DateTimeRangeValue | undefined;
|
|
25
|
+
onChange?: (range: DateTimeRangeValue | undefined) => void;
|
|
26
|
+
|
|
27
|
+
// Presets
|
|
28
|
+
presets?: DateTimeRangePresetOption[];
|
|
29
|
+
showPresets?: boolean; // Default: true
|
|
30
|
+
|
|
31
|
+
// Date constraints
|
|
32
|
+
minDate?: Temporal.PlainDate;
|
|
33
|
+
maxDate?: Temporal.PlainDate;
|
|
34
|
+
disabledDates?: DisabledDateMatcher | DisabledDateMatcher[];
|
|
35
|
+
|
|
36
|
+
// Time constraints (apply to both start and end times)
|
|
37
|
+
minTime?: Temporal.PlainTime;
|
|
38
|
+
maxTime?: Temporal.PlainTime;
|
|
39
|
+
minuteStep?: 1 | 5 | 10 | 15 | 30;
|
|
40
|
+
|
|
41
|
+
// Time format
|
|
42
|
+
clockFormat?: "12h" | "24h"; // Default: "12h"
|
|
43
|
+
showSeconds?: boolean; // Default: false
|
|
44
|
+
|
|
45
|
+
// Display
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
weekStartsOn?: WeekStartsOn;
|
|
48
|
+
|
|
49
|
+
// Form
|
|
50
|
+
name?: string;
|
|
51
|
+
|
|
52
|
+
// State
|
|
53
|
+
isDisabled?: boolean;
|
|
54
|
+
isInvalid?: boolean;
|
|
55
|
+
|
|
56
|
+
// Style
|
|
57
|
+
className?: string;
|
|
58
|
+
align?: "start" | "end";
|
|
59
|
+
|
|
60
|
+
/** Whether the clear button is shown when a value is present (default: true) */
|
|
61
|
+
clearable?: boolean;
|
|
62
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Muted } from "@sixthshift/design-system/muted";
|
|
2
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export type EmptyStateProps = HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
/** Optional icon to display */
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
/** Message to display */
|
|
9
|
+
message: string;
|
|
10
|
+
/** Optional description for more context */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Optional action button or element */
|
|
13
|
+
action?: ReactNode;
|
|
14
|
+
/** Additional CSS classes */
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const EmptyState = ({ icon, message, description, action, className, ...props }: EmptyStateProps) => {
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn("flex flex-col items-center justify-center gap-3 p-8 text-center", className)} {...props}>
|
|
21
|
+
{icon && <div className="text-fg-subtle">{icon}</div>}
|
|
22
|
+
<div className="flex flex-col gap-1">
|
|
23
|
+
<Muted>{message}</Muted>
|
|
24
|
+
{description && <span className="text-fg-subtle text-xs">{description}</span>}
|
|
25
|
+
</div>
|
|
26
|
+
{action && <div className="mt-2">{action}</div>}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { EmptyState, type EmptyStateProps } from "./EmptyState";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The labelled identifier carve-out used by identity-disclosure surfaces
|
|
6
|
+
* (Person Detail's Identifiers card, Calendar Event's per-source provenance).
|
|
7
|
+
*
|
|
8
|
+
* Field exists because identifiers (JIDs, external IDs, OAuth subs) are
|
|
9
|
+
* *content* on these surfaces — they answer "what does PA know about this
|
|
10
|
+
* contact?" The default value rendering is monospaced with `break-all`
|
|
11
|
+
* because a raw JID is the canonical case and it must wrap inside the row.
|
|
12
|
+
*
|
|
13
|
+
* Sibling primitive — not a replacement: `MetricRow` (in `@sixthshift/design-system/metric-list`)
|
|
14
|
+
* carries runtime metrics ("CPU: 12.5%"). Both are label/value pairs but the
|
|
15
|
+
* value semantics differ: a metric is a short readable number; an identifier
|
|
16
|
+
* is a long opaque string. Forcing `MetricRow` to do both would conflate the
|
|
17
|
+
* two and break Identifiers-card wrapping.
|
|
18
|
+
*
|
|
19
|
+
* See: docs/engineering/explanation/concepts/identity-rendering.md
|
|
20
|
+
*/
|
|
21
|
+
export type FieldProps = {
|
|
22
|
+
/** The humane label for what the identifier identifies ("WhatsApp", "Email"). */
|
|
23
|
+
label: string;
|
|
24
|
+
/** The identifier itself. Rendered monospaced + breakable by default. */
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* `stacked` (default): label above, value below — best for long identifiers
|
|
28
|
+
* that need horizontal room to wrap.
|
|
29
|
+
* `row`: label and value on one line with the value right-aligned and
|
|
30
|
+
* wrapping — best for short identifiers (phone, short ID).
|
|
31
|
+
*/
|
|
32
|
+
layout?: "stacked" | "row";
|
|
33
|
+
/**
|
|
34
|
+
* Set `false` to render the value in proportional text — e.g. when the
|
|
35
|
+
* value is itself a human-readable name (rare on identity-disclosure
|
|
36
|
+
* surfaces, but valid).
|
|
37
|
+
*/
|
|
38
|
+
mono?: boolean;
|
|
39
|
+
className?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Field = ({ label, children, layout = "stacked", mono = true, className }: FieldProps) => {
|
|
43
|
+
const valueClass = cn("text-fg-subtle text-xs", mono && "break-all font-mono");
|
|
44
|
+
|
|
45
|
+
if (layout === "row") {
|
|
46
|
+
return (
|
|
47
|
+
<div className={cn("flex items-baseline justify-between gap-3", className)}>
|
|
48
|
+
<span className="shrink-0 text-fg-subtle text-sm">{label}</span>
|
|
49
|
+
<span className={cn(valueClass, "min-w-0 text-right")}>{children}</span>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn("flex flex-col gap-1", className)}>
|
|
56
|
+
<span className="text-fg-subtle text-xs uppercase tracking-wide">{label}</span>
|
|
57
|
+
<span className={valueClass}>{children}</span>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Field, type FieldProps } from "./Field";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Label } from "@sixthshift/design-system/label";
|
|
2
|
+
import { Message } from "@sixthshift/design-system/message";
|
|
3
|
+
import { Muted } from "@sixthshift/design-system/muted";
|
|
4
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
export type FormFieldFeedback = {
|
|
8
|
+
message: string;
|
|
9
|
+
intent: "danger" | "success" | "warning";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type FormFieldProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
13
|
+
label: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
feedback?: FormFieldFeedback;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(({ className, label, description, feedback, required, children, id, ...props }, ref) => {
|
|
20
|
+
const generatedId = React.useId();
|
|
21
|
+
const fieldId = id ?? generatedId;
|
|
22
|
+
const descriptionId = description ? `${fieldId}-description` : undefined;
|
|
23
|
+
const feedbackId = feedback ? `${fieldId}-feedback` : undefined;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div ref={ref} className={cn("flex flex-col gap-1.5", className)} {...props}>
|
|
27
|
+
<Label htmlFor={fieldId}>
|
|
28
|
+
{label}
|
|
29
|
+
{required && <span className="ml-0.5 text-fg-danger">*</span>}
|
|
30
|
+
</Label>
|
|
31
|
+
|
|
32
|
+
{description && <Muted id={descriptionId}>{description}</Muted>}
|
|
33
|
+
|
|
34
|
+
{React.Children.map(children, (child) => {
|
|
35
|
+
if (React.isValidElement(child)) {
|
|
36
|
+
return React.cloneElement(child, {
|
|
37
|
+
id: fieldId,
|
|
38
|
+
"aria-describedby": cn(descriptionId, feedbackId) || undefined,
|
|
39
|
+
"aria-invalid": feedback?.intent === "danger" ? true : undefined,
|
|
40
|
+
} as React.HTMLAttributes<HTMLElement>);
|
|
41
|
+
}
|
|
42
|
+
return child;
|
|
43
|
+
})}
|
|
44
|
+
|
|
45
|
+
{feedback && (
|
|
46
|
+
<Message id={feedbackId} intent={feedback.intent} size="sm">
|
|
47
|
+
{feedback.message}
|
|
48
|
+
</Message>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
FormField.displayName = "FormField";
|
|
54
|
+
|
|
55
|
+
export { FormField };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
import { Temporal } from "../../date-time";
|
|
4
|
+
import { DAY_KEYS, DAY_LABELS, DEFAULT_COLOR_SCALE, getIntensity, type HeatMapCell, MONTH_NAMES } from "./heat-map.utils";
|
|
5
|
+
|
|
6
|
+
export type HeatMapCalendarProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
7
|
+
data: HeatMapCell[];
|
|
8
|
+
colorScale?: string[];
|
|
9
|
+
cellSize?: number;
|
|
10
|
+
cellGap?: number;
|
|
11
|
+
formatTooltip?: (cell: HeatMapCell) => string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type MonthGrid = {
|
|
15
|
+
year: number;
|
|
16
|
+
month: number;
|
|
17
|
+
label: string;
|
|
18
|
+
weeks: ({ date: string; value: number; col: number } | null)[][];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function buildMonthGrids(data: HeatMapCell[]): MonthGrid[] {
|
|
22
|
+
const dataMap = new Map(data.map((d) => [d.date, d.value]));
|
|
23
|
+
|
|
24
|
+
const dates = data.map((d) => Temporal.PlainDate.from(d.date)).sort(Temporal.PlainDate.compare);
|
|
25
|
+
const minDate = dates[0]!;
|
|
26
|
+
const maxDate = dates[dates.length - 1]!;
|
|
27
|
+
|
|
28
|
+
const months: MonthGrid[] = [];
|
|
29
|
+
let cursor = Temporal.PlainDate.from({ year: minDate.year, month: minDate.month, day: 1 });
|
|
30
|
+
const endMonth = Temporal.PlainDate.from({ year: maxDate.year, month: maxDate.month, day: 1 });
|
|
31
|
+
|
|
32
|
+
while (Temporal.PlainDate.compare(cursor, endMonth) <= 0) {
|
|
33
|
+
const year = cursor.year;
|
|
34
|
+
const month = cursor.month;
|
|
35
|
+
const daysInMonth = cursor.daysInMonth;
|
|
36
|
+
|
|
37
|
+
const weeks: ({ date: string; value: number; col: number } | null)[][] = [];
|
|
38
|
+
let currentWeek: ({ date: string; value: number; col: number } | null)[] = [];
|
|
39
|
+
|
|
40
|
+
const firstDow = cursor.dayOfWeek - 1;
|
|
41
|
+
for (let i = 0; i < firstDow; i++) {
|
|
42
|
+
currentWeek.push(null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
46
|
+
const date = Temporal.PlainDate.from({ year, month, day });
|
|
47
|
+
const dateStr = date.toString();
|
|
48
|
+
const col = date.dayOfWeek - 1;
|
|
49
|
+
|
|
50
|
+
currentWeek.push({ date: dateStr, value: dataMap.get(dateStr) ?? 0, col });
|
|
51
|
+
|
|
52
|
+
if (col === 6 || day === daysInMonth) {
|
|
53
|
+
if (day === daysInMonth && col < 6) {
|
|
54
|
+
for (let i = col + 1; i <= 6; i++) {
|
|
55
|
+
currentWeek.push(null);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
weeks.push(currentWeek);
|
|
59
|
+
currentWeek = [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
months.push({
|
|
64
|
+
year,
|
|
65
|
+
month,
|
|
66
|
+
label: `${MONTH_NAMES[month - 1]} ${year}`,
|
|
67
|
+
weeks,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
cursor = month === 12 ? Temporal.PlainDate.from({ year: year + 1, month: 1, day: 1 }) : Temporal.PlainDate.from({ year, month: month + 1, day: 1 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return months;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const HeatMapCalendar = ({
|
|
77
|
+
data,
|
|
78
|
+
colorScale = DEFAULT_COLOR_SCALE,
|
|
79
|
+
cellSize = 12,
|
|
80
|
+
cellGap = 2,
|
|
81
|
+
formatTooltip,
|
|
82
|
+
className,
|
|
83
|
+
...rest
|
|
84
|
+
}: HeatMapCalendarProps) => {
|
|
85
|
+
if (data.length === 0) return null;
|
|
86
|
+
|
|
87
|
+
const maxValue = Math.max(...data.map((d) => d.value), 1);
|
|
88
|
+
const months = buildMonthGrids(data);
|
|
89
|
+
const step = cellSize + cellGap;
|
|
90
|
+
const gridWidth = 7 * step;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div {...rest} className={cn("flex flex-wrap gap-4", className)}>
|
|
94
|
+
{months.map((month) => {
|
|
95
|
+
const headerHeight = 24;
|
|
96
|
+
const dayLabelHeight = 14;
|
|
97
|
+
const svgHeight = headerHeight + dayLabelHeight + month.weeks.length * step;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<svg key={month.label} width={gridWidth} height={svgHeight} className="overflow-visible" role="img">
|
|
101
|
+
<title>{month.label}</title>
|
|
102
|
+
<text x={0} y={14} className="fill-fg-normal" fontSize={12} fontWeight={500}>
|
|
103
|
+
{month.label}
|
|
104
|
+
</text>
|
|
105
|
+
{DAY_LABELS.map((label, i) => (
|
|
106
|
+
<text key={`day-${DAY_KEYS[i]}`} x={i * step + cellSize / 2} y={headerHeight + 10} textAnchor="middle" className="fill-fg-subtle" fontSize={9}>
|
|
107
|
+
{label}
|
|
108
|
+
</text>
|
|
109
|
+
))}
|
|
110
|
+
{month.weeks.map((week, wi) =>
|
|
111
|
+
week.map((cell) => {
|
|
112
|
+
if (!cell) return null;
|
|
113
|
+
const intensity = getIntensity(cell.value, maxValue, colorScale.length);
|
|
114
|
+
const fill = cell.value > 0 ? colorScale[intensity]! : colorScale[0]!;
|
|
115
|
+
return (
|
|
116
|
+
<rect key={cell.date} x={cell.col * step} y={headerHeight + dayLabelHeight + wi * step} width={cellSize} height={cellSize} rx={2} fill={fill}>
|
|
117
|
+
{formatTooltip && <title>{formatTooltip({ date: cell.date, value: cell.value })}</title>}
|
|
118
|
+
</rect>
|
|
119
|
+
);
|
|
120
|
+
})
|
|
121
|
+
)}
|
|
122
|
+
</svg>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
import { Temporal } from "../../date-time";
|
|
4
|
+
import { DAY_KEYS, DAY_LABELS, DEFAULT_COLOR_SCALE, getIntensity, type HeatMapCell } from "./heat-map.utils";
|
|
5
|
+
|
|
6
|
+
export type HeatMapMatrixProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
7
|
+
data: HeatMapCell[];
|
|
8
|
+
colorScale?: string[];
|
|
9
|
+
cellSize?: number;
|
|
10
|
+
cellGap?: number;
|
|
11
|
+
showDayLabels?: boolean;
|
|
12
|
+
formatTooltip?: (cell: HeatMapCell) => string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const HeatMapMatrix = ({
|
|
16
|
+
data,
|
|
17
|
+
colorScale = DEFAULT_COLOR_SCALE,
|
|
18
|
+
cellSize = 12,
|
|
19
|
+
cellGap = 2,
|
|
20
|
+
showDayLabels = true,
|
|
21
|
+
formatTooltip,
|
|
22
|
+
className,
|
|
23
|
+
...rest
|
|
24
|
+
}: HeatMapMatrixProps) => {
|
|
25
|
+
if (data.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const maxValue = Math.max(...data.map((d) => d.value), 1);
|
|
28
|
+
|
|
29
|
+
const sorted = [...data].map((d) => ({ ...d, plain: Temporal.PlainDate.from(d.date) })).sort((a, b) => Temporal.PlainDate.compare(a.plain, b.plain));
|
|
30
|
+
|
|
31
|
+
const rows: { date: string; value: number; col: number }[][] = [];
|
|
32
|
+
let currentRow: { date: string; value: number; col: number }[] = [];
|
|
33
|
+
let lastDow = -1;
|
|
34
|
+
|
|
35
|
+
for (const cell of sorted) {
|
|
36
|
+
const dow = cell.plain.dayOfWeek - 1;
|
|
37
|
+
if (dow <= lastDow && currentRow.length > 0) {
|
|
38
|
+
rows.push(currentRow);
|
|
39
|
+
currentRow = [];
|
|
40
|
+
}
|
|
41
|
+
currentRow.push({ date: cell.date, value: cell.value, col: dow });
|
|
42
|
+
lastDow = dow;
|
|
43
|
+
}
|
|
44
|
+
if (currentRow.length > 0) rows.push(currentRow);
|
|
45
|
+
|
|
46
|
+
const step = cellSize + cellGap;
|
|
47
|
+
const labelHeight = showDayLabels ? 14 : 0;
|
|
48
|
+
const gridWidth = 7 * step;
|
|
49
|
+
const gridHeight = rows.length * step;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div {...rest} className={cn("inline-block", className)}>
|
|
53
|
+
<svg width={gridWidth} height={gridHeight + labelHeight} className="overflow-visible" role="img">
|
|
54
|
+
<title>Heat map</title>
|
|
55
|
+
{showDayLabels &&
|
|
56
|
+
DAY_LABELS.map((label, i) => (
|
|
57
|
+
<text key={`col-${DAY_KEYS[i]}`} x={i * step + cellSize / 2} y={10} textAnchor="middle" className="fill-fg-subtle" fontSize={9}>
|
|
58
|
+
{label}
|
|
59
|
+
</text>
|
|
60
|
+
))}
|
|
61
|
+
{rows.map((row, ri) =>
|
|
62
|
+
row.map((cell) => {
|
|
63
|
+
const intensity = getIntensity(cell.value, maxValue, colorScale.length);
|
|
64
|
+
const fill = cell.value > 0 ? colorScale[intensity]! : colorScale[0]!;
|
|
65
|
+
return (
|
|
66
|
+
<rect key={cell.date} x={cell.col * step} y={labelHeight + ri * step} width={cellSize} height={cellSize} rx={2} fill={fill}>
|
|
67
|
+
{formatTooltip && <title>{formatTooltip({ date: cell.date, value: cell.value })}</title>}
|
|
68
|
+
</rect>
|
|
69
|
+
);
|
|
70
|
+
})
|
|
71
|
+
)}
|
|
72
|
+
</svg>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type HeatMapCell = {
|
|
2
|
+
date: string;
|
|
3
|
+
value: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const DAY_LABELS = ["M", "T", "W", "T", "F", "S", "S"];
|
|
7
|
+
export const DAY_KEYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
8
|
+
export const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_COLOR_SCALE = ["var(--bg-subtle)", "var(--bg-brand-subtle)", "var(--bg-brand)", "var(--bg-brand-strong)"];
|
|
11
|
+
|
|
12
|
+
export function getIntensity(value: number, max: number, levels: number): number {
|
|
13
|
+
if (value <= 0 || max <= 0) return 0;
|
|
14
|
+
return Math.ceil(Math.min(value / max, 1) * (levels - 1));
|
|
15
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { autoUpdate, flip, offset, type Placement, safePolygon, shift, useDismiss, useFloating, useFocus, useHover, useInteractions } from "@floating-ui/react";
|
|
2
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import { HoverCardContent } from "./components/HoverCardContent";
|
|
5
|
+
import { HoverCardContext } from "./components/HoverCardContext";
|
|
6
|
+
import { HoverCardTrigger } from "./components/HoverCardTrigger";
|
|
7
|
+
|
|
8
|
+
export type HoverCardProps = {
|
|
9
|
+
/** Controlled open state */
|
|
10
|
+
open?: boolean;
|
|
11
|
+
/** Callback when open state changes */
|
|
12
|
+
onOpenChange?: (open: boolean) => void;
|
|
13
|
+
/** Default open state for uncontrolled mode */
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
/** Placement relative to trigger */
|
|
16
|
+
placement?: Placement;
|
|
17
|
+
/** Delay before showing (ms). Defaults to 500ms — longer than a tooltip so hover-cards don't pop on incidental hover. */
|
|
18
|
+
delayShow?: number;
|
|
19
|
+
/** Delay before hiding (ms). Defaults to 200ms. */
|
|
20
|
+
delayHide?: number;
|
|
21
|
+
/** Offset from trigger in pixels */
|
|
22
|
+
offsetPx?: number;
|
|
23
|
+
/** Children — compound components */
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const HoverCardRoot = ({
|
|
28
|
+
open: controlledOpen,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
defaultOpen = false,
|
|
31
|
+
placement = "bottom-start",
|
|
32
|
+
delayShow = 500,
|
|
33
|
+
delayHide = 200,
|
|
34
|
+
offsetPx = 8,
|
|
35
|
+
children,
|
|
36
|
+
}: HoverCardProps) => {
|
|
37
|
+
const [open, setOpen] = useControllableState({
|
|
38
|
+
value: controlledOpen,
|
|
39
|
+
defaultValue: defaultOpen,
|
|
40
|
+
onChange: onOpenChange,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
44
|
+
open,
|
|
45
|
+
onOpenChange: setOpen,
|
|
46
|
+
placement,
|
|
47
|
+
strategy: "fixed",
|
|
48
|
+
middleware: [offset(offsetPx), flip(), shift({ padding: 8 })],
|
|
49
|
+
whileElementsMounted: autoUpdate,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const hover = useHover(context, {
|
|
53
|
+
delay: { open: delayShow, close: delayHide },
|
|
54
|
+
handleClose: safePolygon(),
|
|
55
|
+
});
|
|
56
|
+
const focus = useFocus(context);
|
|
57
|
+
const dismiss = useDismiss(context);
|
|
58
|
+
|
|
59
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss]);
|
|
60
|
+
|
|
61
|
+
const contextValue = {
|
|
62
|
+
open,
|
|
63
|
+
refs: {
|
|
64
|
+
setReference: refs.setReference,
|
|
65
|
+
setFloating: refs.setFloating,
|
|
66
|
+
},
|
|
67
|
+
floatingStyles,
|
|
68
|
+
getReferenceProps,
|
|
69
|
+
getFloatingProps,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return <HoverCardContext.Provider value={contextValue}>{children}</HoverCardContext.Provider>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type HoverCardComponent = typeof HoverCardRoot & {
|
|
76
|
+
Trigger: typeof HoverCardTrigger;
|
|
77
|
+
Content: typeof HoverCardContent;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const HoverCard: HoverCardComponent = Object.assign(HoverCardRoot, {
|
|
81
|
+
Trigger: HoverCardTrigger,
|
|
82
|
+
Content: HoverCardContent,
|
|
83
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FloatingPortal } from "@floating-ui/react";
|
|
2
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useHoverCardContext } from "./HoverCardContext";
|
|
5
|
+
|
|
6
|
+
export type HoverCardContentProps = React.HTMLAttributes<HTMLDivElement>;
|
|
7
|
+
|
|
8
|
+
export const HoverCardContent = React.forwardRef<HTMLDivElement, HoverCardContentProps>(({ className, children, ...props }, forwardedRef) => {
|
|
9
|
+
const { open, refs, floatingStyles, getFloatingProps } = useHoverCardContext();
|
|
10
|
+
|
|
11
|
+
if (!open) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<FloatingPortal>
|
|
15
|
+
<div
|
|
16
|
+
ref={(node) => {
|
|
17
|
+
refs.setFloating(node);
|
|
18
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
19
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
20
|
+
}}
|
|
21
|
+
style={floatingStyles}
|
|
22
|
+
className={cn("z-popover w-80 rounded-lg border border-border-normal bg-bg-normal p-4 shadow-lg", className)}
|
|
23
|
+
{...getFloatingProps(props)}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
</FloatingPortal>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
HoverCardContent.displayName = "HoverCardContent";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type HoverCardContextValue = {
|
|
4
|
+
open: boolean;
|
|
5
|
+
refs: {
|
|
6
|
+
setReference: (node: HTMLElement | null) => void;
|
|
7
|
+
setFloating: (node: HTMLElement | null) => void;
|
|
8
|
+
};
|
|
9
|
+
floatingStyles: React.CSSProperties;
|
|
10
|
+
getReferenceProps: (props?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
11
|
+
getFloatingProps: (props?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const HoverCardContext = createContext<HoverCardContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export function useHoverCardContext() {
|
|
17
|
+
const context = useContext(HoverCardContext);
|
|
18
|
+
if (!context) {
|
|
19
|
+
throw new Error("HoverCard compound components must be used within <HoverCard>");
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "../../../internal/Slot";
|
|
3
|
+
import type { WritableRefObject } from "../../../internal/types";
|
|
4
|
+
import { useHoverCardContext } from "./HoverCardContext";
|
|
5
|
+
|
|
6
|
+
export type HoverCardTriggerProps = React.HTMLAttributes<HTMLElement> & {
|
|
7
|
+
/** Render as child element (default true — hover-cards typically wrap an existing inline element). */
|
|
8
|
+
asChild?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const HoverCardTrigger = React.forwardRef<HTMLElement, HoverCardTriggerProps>(({ asChild = true, children, className, ...props }, forwardedRef) => {
|
|
12
|
+
const { refs, getReferenceProps } = useHoverCardContext();
|
|
13
|
+
const Comp = asChild ? Slot : "span";
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Comp
|
|
17
|
+
ref={(node: HTMLElement | null) => {
|
|
18
|
+
refs.setReference(node);
|
|
19
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
20
|
+
else if (forwardedRef) (forwardedRef as WritableRefObject<HTMLElement | null>).current = node;
|
|
21
|
+
}}
|
|
22
|
+
// When rendering as a span wrapper (not asChild), use block layout so the
|
|
23
|
+
// bounding box matches the wrapped element. Without this, the span has
|
|
24
|
+
// inline layout and floating-ui anchors to the wrong position.
|
|
25
|
+
className={asChild ? className : `block ${className ?? ""}`.trim()}
|
|
26
|
+
{...getReferenceProps(props)}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</Comp>
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
HoverCardTrigger.displayName = "HoverCardTrigger";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
5
|
+
iconLeft?: React.ReactNode;
|
|
6
|
+
iconRight?: React.ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const iconSlotStyles = `absolute top-1/2 -translate-y-1/2 text-fg-subtle
|
|
10
|
+
[&_svg]:h-4 [&_svg]:w-4
|
|
11
|
+
[&_button]:flex [&_button]:cursor-pointer [&_button]:appearance-none
|
|
12
|
+
[&_button]:border-0 [&_button]:bg-transparent`;
|
|
13
|
+
|
|
14
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, iconLeft, iconRight, ...props }, ref) => {
|
|
15
|
+
if (iconLeft || iconRight) {
|
|
16
|
+
return (
|
|
17
|
+
<div className={cn("relative h-9 w-full text-sm", className)}>
|
|
18
|
+
{iconLeft && <span className={cn(iconSlotStyles, "left-3")}>{iconLeft}</span>}
|
|
19
|
+
<input
|
|
20
|
+
type={type}
|
|
21
|
+
className={cn(
|
|
22
|
+
`flex h-full w-full rounded-md border border-border-normal bg-bg-normal py-1 text-[length:inherit] shadow-xs transition-colors file:border-0 file:bg-bg-normal file:font-medium file:text-fg-normal file:text-sm placeholder:text-fg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50`,
|
|
23
|
+
iconLeft ? "pl-9" : "pl-3",
|
|
24
|
+
iconRight ? "pr-9" : "pr-3"
|
|
25
|
+
)}
|
|
26
|
+
ref={ref}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
{iconRight && <span className={cn(iconSlotStyles, "right-3")}>{iconRight}</span>}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<input
|
|
36
|
+
type={type}
|
|
37
|
+
className={cn(
|
|
38
|
+
`flex h-9 w-full rounded-md border border-border-normal bg-bg-normal px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-bg-normal file:font-medium file:text-fg-normal file:text-sm placeholder:text-fg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50`,
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
ref={ref}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
Input.displayName = "Input";
|
|
47
|
+
|
|
48
|
+
export { Input };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Input, type InputProps } from "./Input";
|