@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,111 @@
|
|
|
1
|
+
import { FloatingPortal, type FloatingPortalProps } from "@floating-ui/react";
|
|
2
|
+
import { Button } from "@sixthshift/design-system/button";
|
|
3
|
+
import { usePresence } from "@sixthshift/design-system/hooks";
|
|
4
|
+
import { Message, MessageBody, MessageDescription, MessageIcon, type MessageProps, MessageTitle } from "@sixthshift/design-system/message";
|
|
5
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
6
|
+
import { X } from "lucide-react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export type ToastProps = Omit<MessageProps, "size"> & {
|
|
14
|
+
/** Called when toast closes (after exit animation) */
|
|
15
|
+
onClose?: () => void;
|
|
16
|
+
/** Optional action button label */
|
|
17
|
+
action?: string;
|
|
18
|
+
/** Called when action button is clicked */
|
|
19
|
+
onAction?: () => void;
|
|
20
|
+
/** Portal root element (default: document.body) */
|
|
21
|
+
root?: FloatingPortalProps["root"];
|
|
22
|
+
/**
|
|
23
|
+
* Whether Toast should render in a portal and position itself.
|
|
24
|
+
* Set to false when Toast is rendered by a parent that handles positioning (e.g., OverlayContext).
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
standalone?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Toast
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
|
|
35
|
+
({ className, intent = "neutral", title, icon, action, onAction, onClose, root, standalone = true, children, ...props }, ref) => {
|
|
36
|
+
const { ref: presenceRef, state, isMounted, show, hide } = usePresence();
|
|
37
|
+
|
|
38
|
+
// Start enter animation on mount
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
show();
|
|
41
|
+
}, [show]);
|
|
42
|
+
|
|
43
|
+
// Handle close with exit animation
|
|
44
|
+
const handleClose = React.useCallback(() => {
|
|
45
|
+
hide(() => {
|
|
46
|
+
onClose?.();
|
|
47
|
+
});
|
|
48
|
+
}, [hide, onClose]);
|
|
49
|
+
|
|
50
|
+
// Merge refs
|
|
51
|
+
const mergedRef = React.useCallback(
|
|
52
|
+
(node: HTMLDivElement | null) => {
|
|
53
|
+
presenceRef(node);
|
|
54
|
+
if (typeof ref === "function") {
|
|
55
|
+
ref(node);
|
|
56
|
+
} else if (ref) {
|
|
57
|
+
ref.current = node;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[presenceRef, ref]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!isMounted) return null;
|
|
64
|
+
|
|
65
|
+
const isEntering = state === "entering";
|
|
66
|
+
const isExiting = state === "exiting";
|
|
67
|
+
|
|
68
|
+
const content = (
|
|
69
|
+
<div
|
|
70
|
+
ref={mergedRef}
|
|
71
|
+
className={cn(
|
|
72
|
+
"max-w-sm shadow-lg",
|
|
73
|
+
standalone && "fixed bottom-6 left-1/2 z-toast -translate-x-1/2",
|
|
74
|
+
isEntering && "animate-fade-in",
|
|
75
|
+
isExiting && "animate-fade-out",
|
|
76
|
+
className
|
|
77
|
+
)}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<Message intent={intent}>
|
|
81
|
+
{icon && <MessageIcon>{icon}</MessageIcon>}
|
|
82
|
+
<MessageBody>
|
|
83
|
+
{title && <MessageTitle>{title}</MessageTitle>}
|
|
84
|
+
<MessageDescription>
|
|
85
|
+
{children}
|
|
86
|
+
{action && (
|
|
87
|
+
<div className="mt-2">
|
|
88
|
+
<Button variant="link" size="sm" className="h-auto p-0 font-medium" onClick={onAction}>
|
|
89
|
+
{action}
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</MessageDescription>
|
|
94
|
+
</MessageBody>
|
|
95
|
+
{onClose && (
|
|
96
|
+
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 opacity-70 hover:opacity-100" onClick={handleClose} aria-label="Dismiss">
|
|
97
|
+
<X className="h-4 w-4" />
|
|
98
|
+
</Button>
|
|
99
|
+
)}
|
|
100
|
+
</Message>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (standalone) {
|
|
105
|
+
return <FloatingPortal {...(root ? { root } : {})}>{content}</FloatingPortal>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return content;
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
Toast.displayName = "Toast";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toast, type ToastProps } from "./Toast";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
2
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { buttonVariants } from "../Button/Button";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pressed-state backgrounds matching Button's active: states.
|
|
9
|
+
* When a toggle is "on", its resting background becomes the pressed token.
|
|
10
|
+
*/
|
|
11
|
+
const togglePressedVariants = cva("", {
|
|
12
|
+
variants: {
|
|
13
|
+
variant: { solid: "", outline: "", ghost: "", link: "" },
|
|
14
|
+
intent: { neutral: "", danger: "", success: "", warning: "" },
|
|
15
|
+
},
|
|
16
|
+
compoundVariants: [
|
|
17
|
+
// Solid — use the main -pressed background
|
|
18
|
+
{ variant: "solid", intent: "neutral", className: "bg-bg-brand-pressed hover:bg-bg-brand-pressed" },
|
|
19
|
+
{ variant: "solid", intent: "danger", className: "bg-bg-danger-pressed hover:bg-bg-danger-pressed" },
|
|
20
|
+
{ variant: "solid", intent: "success", className: "bg-bg-success-pressed hover:bg-bg-success-pressed" },
|
|
21
|
+
{ variant: "solid", intent: "warning", className: "bg-bg-warning-pressed hover:bg-bg-warning-pressed" },
|
|
22
|
+
// Outline — use the subtle -pressed background
|
|
23
|
+
{ variant: "outline", intent: "neutral", className: "bg-bg-subtle-pressed text-fg-normal hover:bg-bg-subtle-pressed" },
|
|
24
|
+
{ variant: "outline", intent: "danger", className: "bg-bg-danger-subtle-pressed text-fg-on-danger-subtle-pressed hover:bg-bg-danger-subtle-pressed" },
|
|
25
|
+
{ variant: "outline", intent: "success", className: "bg-bg-success-subtle-pressed text-fg-on-success-subtle-pressed hover:bg-bg-success-subtle-pressed" },
|
|
26
|
+
{ variant: "outline", intent: "warning", className: "bg-bg-warning-subtle-pressed text-fg-on-warning-subtle-pressed hover:bg-bg-warning-subtle-pressed" },
|
|
27
|
+
// Ghost — same as outline
|
|
28
|
+
{ variant: "ghost", intent: "neutral", className: "bg-bg-subtle-pressed text-fg-normal hover:bg-bg-subtle-pressed" },
|
|
29
|
+
{ variant: "ghost", intent: "danger", className: "bg-bg-danger-subtle-pressed text-fg-on-danger-subtle-pressed hover:bg-bg-danger-subtle-pressed" },
|
|
30
|
+
{ variant: "ghost", intent: "success", className: "bg-bg-success-subtle-pressed text-fg-on-success-subtle-pressed hover:bg-bg-success-subtle-pressed" },
|
|
31
|
+
{ variant: "ghost", intent: "warning", className: "bg-bg-warning-subtle-pressed text-fg-on-warning-subtle-pressed hover:bg-bg-warning-subtle-pressed" },
|
|
32
|
+
],
|
|
33
|
+
defaultVariants: { variant: "solid", intent: "neutral" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export type ToggleProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange" | "value"> &
|
|
37
|
+
VariantProps<typeof buttonVariants> & {
|
|
38
|
+
/** Controlled pressed state */
|
|
39
|
+
pressed?: boolean;
|
|
40
|
+
/** Default pressed state for uncontrolled mode */
|
|
41
|
+
defaultPressed?: boolean;
|
|
42
|
+
/** Called when pressed state changes */
|
|
43
|
+
onPressedChange?: (pressed: boolean) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
47
|
+
({ className, variant, intent, size, pressed: controlledPressed, defaultPressed = false, onPressedChange, disabled, children, ...props }, ref) => {
|
|
48
|
+
const [pressed, setPressed] = useControllableState({
|
|
49
|
+
value: controlledPressed,
|
|
50
|
+
defaultValue: defaultPressed,
|
|
51
|
+
onChange: onPressedChange,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
aria-pressed={pressed}
|
|
58
|
+
data-state={pressed ? "on" : "off"}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
ref={ref}
|
|
61
|
+
onClick={() => setPressed(!pressed)}
|
|
62
|
+
className={cn(buttonVariants({ variant, intent, size }), pressed && togglePressedVariants({ variant, intent }), className)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
Toggle.displayName = "Toggle";
|
|
71
|
+
|
|
72
|
+
export { Toggle, togglePressedVariants };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toggle, type ToggleProps, togglePressedVariants } from "./Toggle";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ToggleGroupMultiple } from "./ToggleGroupMultiple";
|
|
3
|
+
import { ToggleGroupSingle } from "./ToggleGroupSingle";
|
|
4
|
+
import type { ToggleGroupProps } from "./toggleGroup.types";
|
|
5
|
+
|
|
6
|
+
const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>((props, ref) => {
|
|
7
|
+
if (props.type === "multiple") {
|
|
8
|
+
return <ToggleGroupMultiple ref={ref} {...props} />;
|
|
9
|
+
}
|
|
10
|
+
return <ToggleGroupSingle ref={ref} {...props} />;
|
|
11
|
+
});
|
|
12
|
+
ToggleGroup.displayName = "ToggleGroup";
|
|
13
|
+
|
|
14
|
+
export { ToggleGroup };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import type { ToggleGroupBaseProps } from "./toggleGroup.types";
|
|
4
|
+
|
|
5
|
+
type ContainerProps = React.HTMLAttributes<HTMLDivElement> & Required<Pick<ToggleGroupBaseProps, "appearance" | "orientation">>;
|
|
6
|
+
|
|
7
|
+
const ToggleGroupContainer = React.forwardRef<HTMLDivElement, ContainerProps>(({ appearance, orientation, className, children, ...props }, ref) => {
|
|
8
|
+
const isVertical = orientation === "vertical";
|
|
9
|
+
const isSegmented = appearance === "segmented";
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div ref={ref} className={cn("inline-flex", isVertical ? "flex-col" : "flex-row", !isSegmented && "gap-2", className)} {...props}>
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
ToggleGroupContainer.displayName = "ToggleGroupContainer";
|
|
18
|
+
|
|
19
|
+
export { ToggleGroupContainer };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { buttonVariants } from "../Button/Button";
|
|
4
|
+
import { togglePressedVariants } from "../Toggle/Toggle";
|
|
5
|
+
import type { ToggleGroupBaseProps, ToggleGroupOption } from "./toggleGroup.types";
|
|
6
|
+
|
|
7
|
+
type ItemProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
8
|
+
Required<Pick<ToggleGroupBaseProps, "appearance" | "orientation" | "variant" | "intent" | "size">> & {
|
|
9
|
+
option: ToggleGroupOption;
|
|
10
|
+
selected: boolean;
|
|
11
|
+
groupDisabled?: boolean | undefined;
|
|
12
|
+
index: number;
|
|
13
|
+
total: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ToggleGroupItem = React.forwardRef<HTMLButtonElement, ItemProps>(
|
|
17
|
+
({ option, selected, groupDisabled, appearance, orientation, variant, intent, size, index, total, ...props }, ref) => {
|
|
18
|
+
const isDisabled = groupDisabled || option.disabled;
|
|
19
|
+
const isFirst = index === 0;
|
|
20
|
+
const isLast = index === total - 1;
|
|
21
|
+
const isVertical = orientation === "vertical";
|
|
22
|
+
const isSegmented = appearance === "segmented";
|
|
23
|
+
|
|
24
|
+
// Both modes share the same base: buttonVariants + togglePressedVariants.
|
|
25
|
+
// Segmented just overrides border/shadow/rounding so items join cleanly.
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
ref={ref}
|
|
29
|
+
type="button"
|
|
30
|
+
disabled={isDisabled}
|
|
31
|
+
aria-label={option.ariaLabel}
|
|
32
|
+
className={cn(
|
|
33
|
+
buttonVariants({ variant, intent, size }),
|
|
34
|
+
selected && togglePressedVariants({ variant, intent }),
|
|
35
|
+
// Segmented: only override rounding (let buttonVariants handle border/color).
|
|
36
|
+
// Negative margin collapses double borders between outline items.
|
|
37
|
+
isSegmented &&
|
|
38
|
+
(isVertical
|
|
39
|
+
? cn("rounded-none", isFirst && "rounded-t-md", isLast && "rounded-b-md", !isFirst && "-mt-px")
|
|
40
|
+
: cn("rounded-none", isFirst && "rounded-l-md", isLast && "rounded-r-md", !isFirst && "-ml-px"))
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{option.label}
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
ToggleGroupItem.displayName = "ToggleGroupItem";
|
|
50
|
+
|
|
51
|
+
export { ToggleGroupItem };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { ToggleGroupContainer } from "./ToggleGroupContainer";
|
|
4
|
+
import { ToggleGroupItem } from "./ToggleGroupItem";
|
|
5
|
+
import type { ToggleGroupMultipleProps } from "./toggleGroup.types";
|
|
6
|
+
|
|
7
|
+
const ToggleGroupMultiple = React.forwardRef<HTMLDivElement, ToggleGroupMultipleProps>(
|
|
8
|
+
(
|
|
9
|
+
{
|
|
10
|
+
options,
|
|
11
|
+
value: controlledValue,
|
|
12
|
+
defaultValue = [],
|
|
13
|
+
onValueChange,
|
|
14
|
+
appearance = "segmented",
|
|
15
|
+
orientation = "horizontal",
|
|
16
|
+
variant = "solid",
|
|
17
|
+
intent = "neutral",
|
|
18
|
+
size = "default",
|
|
19
|
+
disabled,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
},
|
|
23
|
+
ref
|
|
24
|
+
) => {
|
|
25
|
+
const [value, setValue] = useControllableState({
|
|
26
|
+
value: controlledValue,
|
|
27
|
+
defaultValue,
|
|
28
|
+
onChange: onValueChange,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const handleToggle = (optionValue: string) => {
|
|
32
|
+
const next = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue];
|
|
33
|
+
setValue(next);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ToggleGroupContainer ref={ref} role="group" appearance={appearance} orientation={orientation} className={className} {...props}>
|
|
38
|
+
{options.map((option, index) => (
|
|
39
|
+
<ToggleGroupItem
|
|
40
|
+
key={option.value}
|
|
41
|
+
option={option}
|
|
42
|
+
selected={value.includes(option.value)}
|
|
43
|
+
groupDisabled={disabled}
|
|
44
|
+
appearance={appearance}
|
|
45
|
+
orientation={orientation}
|
|
46
|
+
variant={variant}
|
|
47
|
+
intent={intent}
|
|
48
|
+
size={size}
|
|
49
|
+
index={index}
|
|
50
|
+
total={options.length}
|
|
51
|
+
aria-pressed={value.includes(option.value)}
|
|
52
|
+
onClick={() => handleToggle(option.value)}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</ToggleGroupContainer>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
ToggleGroupMultiple.displayName = "ToggleGroupMultiple";
|
|
60
|
+
|
|
61
|
+
export { ToggleGroupMultiple };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { getRovingTargetIndex } from "../../internal/rovingFocus";
|
|
4
|
+
import type { WritableRefObject } from "../../internal/types";
|
|
5
|
+
import { ToggleGroupContainer } from "./ToggleGroupContainer";
|
|
6
|
+
import { ToggleGroupItem } from "./ToggleGroupItem";
|
|
7
|
+
import type { ToggleGroupSingleProps } from "./toggleGroup.types";
|
|
8
|
+
|
|
9
|
+
const ToggleGroupSingle = React.forwardRef<HTMLDivElement, ToggleGroupSingleProps>(
|
|
10
|
+
(
|
|
11
|
+
{
|
|
12
|
+
options,
|
|
13
|
+
value: controlledValue,
|
|
14
|
+
defaultValue = "",
|
|
15
|
+
onValueChange,
|
|
16
|
+
appearance = "segmented",
|
|
17
|
+
orientation = "horizontal",
|
|
18
|
+
variant = "solid",
|
|
19
|
+
intent = "neutral",
|
|
20
|
+
size = "default",
|
|
21
|
+
disabled,
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
},
|
|
25
|
+
ref
|
|
26
|
+
) => {
|
|
27
|
+
const [value, setValue] = useControllableState({
|
|
28
|
+
value: controlledValue,
|
|
29
|
+
defaultValue,
|
|
30
|
+
onChange: onValueChange,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const handleSelect = (optionValue: string) => {
|
|
34
|
+
// Single select: clicking the selected item does nothing (no deselect)
|
|
35
|
+
if (optionValue !== value) {
|
|
36
|
+
setValue(optionValue);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const groupRef = React.useRef<HTMLDivElement | null>(null);
|
|
41
|
+
const mergedRef = React.useCallback(
|
|
42
|
+
(node: HTMLDivElement | null) => {
|
|
43
|
+
groupRef.current = node;
|
|
44
|
+
if (typeof ref === "function") ref(node);
|
|
45
|
+
else if (ref) (ref as WritableRefObject<HTMLDivElement | null>).current = node;
|
|
46
|
+
},
|
|
47
|
+
[ref]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// role="radiogroup" means one tab stop with arrow-key navigation.
|
|
51
|
+
const handleKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
52
|
+
const enabled = Array.from(groupRef.current?.querySelectorAll<HTMLButtonElement>('[role="radio"]') ?? []).filter((radio) => !radio.disabled);
|
|
53
|
+
const focused = (event.target as HTMLElement | null)?.closest?.('[role="radio"]') as HTMLButtonElement | null;
|
|
54
|
+
const target = getRovingTargetIndex(event.key, focused ? enabled.indexOf(focused) : -1, enabled.length);
|
|
55
|
+
if (target === null) return;
|
|
56
|
+
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
const next = enabled[target];
|
|
59
|
+
next?.focus();
|
|
60
|
+
// In the radio pattern, moving focus also moves the selection.
|
|
61
|
+
next?.click();
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
// Tab stop: the selected option, or the first enabled one when nothing is
|
|
65
|
+
// selected, so the group is always keyboard-reachable.
|
|
66
|
+
const firstEnabledValue = options.find((option) => !(disabled || option.disabled))?.value;
|
|
67
|
+
const hasSelected = options.some((option) => option.value === value);
|
|
68
|
+
const tabStopValue = hasSelected ? value : firstEnabledValue;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<ToggleGroupContainer
|
|
72
|
+
ref={mergedRef}
|
|
73
|
+
role="radiogroup"
|
|
74
|
+
onKeyDown={handleKeyDown}
|
|
75
|
+
appearance={appearance}
|
|
76
|
+
orientation={orientation}
|
|
77
|
+
className={className}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
{options.map((option, index) => (
|
|
81
|
+
<ToggleGroupItem
|
|
82
|
+
key={option.value}
|
|
83
|
+
option={option}
|
|
84
|
+
selected={value === option.value}
|
|
85
|
+
groupDisabled={disabled}
|
|
86
|
+
appearance={appearance}
|
|
87
|
+
orientation={orientation}
|
|
88
|
+
variant={variant}
|
|
89
|
+
intent={intent}
|
|
90
|
+
size={size}
|
|
91
|
+
index={index}
|
|
92
|
+
total={options.length}
|
|
93
|
+
role="radio"
|
|
94
|
+
aria-checked={value === option.value}
|
|
95
|
+
tabIndex={option.value === tabStopValue ? 0 : -1}
|
|
96
|
+
onClick={() => handleSelect(option.value)}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</ToggleGroupContainer>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
ToggleGroupSingle.displayName = "ToggleGroupSingle";
|
|
104
|
+
|
|
105
|
+
export { ToggleGroupSingle };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { ToggleProps } from "../Toggle/Toggle";
|
|
3
|
+
|
|
4
|
+
/** Extract a prop's union type from ToggleProps, stripping null added by CVA */
|
|
5
|
+
type ToggleProp<K extends keyof ToggleProps> = NonNullable<ToggleProps[K]>;
|
|
6
|
+
|
|
7
|
+
export type ToggleGroupOption = {
|
|
8
|
+
/** Unique value for the option */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Display label (text, icons, or any ReactNode) */
|
|
11
|
+
label: React.ReactNode;
|
|
12
|
+
/** Accessible label when label is non-text (e.g. icon-only) */
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
/** Whether this option is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ToggleGroupBaseProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> & {
|
|
19
|
+
/** Available options */
|
|
20
|
+
options: readonly ToggleGroupOption[];
|
|
21
|
+
/** Visual appearance */
|
|
22
|
+
appearance?: "segmented" | "separate";
|
|
23
|
+
/** Layout orientation */
|
|
24
|
+
orientation?: "vertical" | "horizontal";
|
|
25
|
+
/** Button variant (link excluded as it has no clear pressed state in groups) */
|
|
26
|
+
variant?: Exclude<ToggleProp<"variant">, "link">;
|
|
27
|
+
/** Color intent */
|
|
28
|
+
intent?: ToggleProp<"intent">;
|
|
29
|
+
/** Button size (xl excluded — too large for grouped toggles) */
|
|
30
|
+
size?: Exclude<ToggleProp<"size">, "xl">;
|
|
31
|
+
/** Disable all options */
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ToggleGroupSingleProps = ToggleGroupBaseProps & {
|
|
36
|
+
type: "single";
|
|
37
|
+
/** Controlled selected value */
|
|
38
|
+
value?: string;
|
|
39
|
+
/** Default selected value for uncontrolled mode */
|
|
40
|
+
defaultValue?: string;
|
|
41
|
+
/** Called when selection changes */
|
|
42
|
+
onValueChange?: (value: string) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ToggleGroupMultipleProps = ToggleGroupBaseProps & {
|
|
46
|
+
type: "multiple";
|
|
47
|
+
/** Controlled selected values */
|
|
48
|
+
value?: string[];
|
|
49
|
+
/** Default selected values for uncontrolled mode */
|
|
50
|
+
defaultValue?: string[];
|
|
51
|
+
/** Called when selection changes */
|
|
52
|
+
onValueChange?: (value: string[]) => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ToggleGroupProps = ToggleGroupSingleProps | ToggleGroupMultipleProps;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { autoUpdate, flip, offset, type Placement, shift, useDismiss, useFloating, useFocus, useHover, useInteractions, useRole } from "@floating-ui/react";
|
|
2
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
3
|
+
import { TooltipBody } from "./components/TooltipBody";
|
|
4
|
+
import { TooltipContext } from "./components/TooltipContext";
|
|
5
|
+
import { TooltipTrigger } from "./components/TooltipTrigger";
|
|
6
|
+
|
|
7
|
+
export type TooltipProps = {
|
|
8
|
+
/** Controlled open state */
|
|
9
|
+
open?: boolean;
|
|
10
|
+
/** Callback when open state changes */
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
/** Placement of the tooltip relative to trigger */
|
|
13
|
+
placement?: Placement;
|
|
14
|
+
/** Delay before showing tooltip (ms) */
|
|
15
|
+
delayShow?: number;
|
|
16
|
+
/** Delay before hiding tooltip (ms) */
|
|
17
|
+
delayHide?: number;
|
|
18
|
+
/** Offset from the trigger in pixels */
|
|
19
|
+
offsetPx?: number;
|
|
20
|
+
/** Children - compound components */
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const TooltipRoot = ({ open: controlledOpen, onOpenChange, placement = "top", delayShow = 300, delayHide = 0, offsetPx = 8, children }: TooltipProps) => {
|
|
25
|
+
const [open, setOpen] = useControllableState({
|
|
26
|
+
value: controlledOpen,
|
|
27
|
+
defaultValue: false,
|
|
28
|
+
onChange: onOpenChange,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
32
|
+
open,
|
|
33
|
+
onOpenChange: setOpen,
|
|
34
|
+
placement,
|
|
35
|
+
middleware: [offset(offsetPx), flip(), shift({ padding: 8 })],
|
|
36
|
+
whileElementsMounted: autoUpdate,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const hover = useHover(context, {
|
|
40
|
+
delay: { open: delayShow, close: delayHide },
|
|
41
|
+
move: false,
|
|
42
|
+
});
|
|
43
|
+
const focus = useFocus(context);
|
|
44
|
+
const dismiss = useDismiss(context);
|
|
45
|
+
const role = useRole(context, { role: "tooltip" });
|
|
46
|
+
|
|
47
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);
|
|
48
|
+
|
|
49
|
+
const contextValue = {
|
|
50
|
+
open,
|
|
51
|
+
refs: {
|
|
52
|
+
setReference: refs.setReference,
|
|
53
|
+
setFloating: refs.setFloating,
|
|
54
|
+
},
|
|
55
|
+
floatingStyles,
|
|
56
|
+
getReferenceProps,
|
|
57
|
+
getFloatingProps,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return <TooltipContext.Provider value={contextValue}>{children}</TooltipContext.Provider>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Tooltip = Object.assign(TooltipRoot, {
|
|
64
|
+
Trigger: TooltipTrigger,
|
|
65
|
+
Body: TooltipBody,
|
|
66
|
+
});
|
|
@@ -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 { useTooltipContext } from "./TooltipContext";
|
|
5
|
+
|
|
6
|
+
export type TooltipBodyProps = React.HTMLAttributes<HTMLDivElement>;
|
|
7
|
+
|
|
8
|
+
export const TooltipBody = React.forwardRef<HTMLDivElement, TooltipBodyProps>(({ className, children, ...props }, forwardedRef) => {
|
|
9
|
+
const { open, refs, floatingStyles, getFloatingProps } = useTooltipContext();
|
|
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 rounded-lg border border-border-normal bg-bg-normal px-2.5 py-1.5 text-fg-normal text-xs shadow-lg", className)}
|
|
23
|
+
{...getFloatingProps(props)}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
</FloatingPortal>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
TooltipBody.displayName = "TooltipBody";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type TooltipContextValue = {
|
|
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 TooltipContext = createContext<TooltipContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export function useTooltipContext() {
|
|
17
|
+
const context = useContext(TooltipContext);
|
|
18
|
+
if (!context) {
|
|
19
|
+
throw new Error("Tooltip compound components must be used within <Tooltip>");
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "../../../internal/Slot";
|
|
3
|
+
import { useTooltipContext } from "./TooltipContext";
|
|
4
|
+
|
|
5
|
+
export type TooltipTriggerProps = React.HTMLAttributes<HTMLElement> & {
|
|
6
|
+
/** Render as child element */
|
|
7
|
+
asChild?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const TooltipTrigger = React.forwardRef<HTMLElement, TooltipTriggerProps>(({ asChild = false, children, ...props }, forwardedRef) => {
|
|
11
|
+
const { refs, getReferenceProps } = useTooltipContext();
|
|
12
|
+
const Comp = asChild ? Slot : "span";
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Comp
|
|
16
|
+
ref={(node: HTMLElement | null) => {
|
|
17
|
+
refs.setReference(node);
|
|
18
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
19
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
20
|
+
}}
|
|
21
|
+
{...getReferenceProps(props)}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</Comp>
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
TooltipTrigger.displayName = "TooltipTrigger";
|