@sixthshift/design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/styles.css +2 -0
  4. package/dist/theme.generated.css +438 -0
  5. package/package.json +238 -0
  6. package/src/components/Avatar/Avatar.tsx +19 -0
  7. package/src/components/Avatar/index.ts +1 -0
  8. package/src/components/Badge/Badge.tsx +59 -0
  9. package/src/components/Badge/index.ts +1 -0
  10. package/src/components/BarChart/BarChart.tsx +51 -0
  11. package/src/components/BarChart/index.ts +1 -0
  12. package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
  13. package/src/components/Breadcrumb/index.ts +5 -0
  14. package/src/components/Button/Button.tsx +157 -0
  15. package/src/components/Button/index.ts +1 -0
  16. package/src/components/Calendar/Calendar.tsx +433 -0
  17. package/src/components/Calendar/calendar.hooks.ts +329 -0
  18. package/src/components/Calendar/calendar.types.ts +117 -0
  19. package/src/components/Calendar/index.ts +28 -0
  20. package/src/components/Card/Card.tsx +47 -0
  21. package/src/components/Card/index.ts +1 -0
  22. package/src/components/Checkbox/Checkbox.tsx +144 -0
  23. package/src/components/Checkbox/index.ts +1 -0
  24. package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
  25. package/src/components/CheckboxGroup/index.ts +5 -0
  26. package/src/components/Code/Editor/Editor.tsx +161 -0
  27. package/src/components/Code/Editor/index.ts +2 -0
  28. package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
  29. package/src/components/Code/Editor/themes.ts +112 -0
  30. package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
  31. package/src/components/Code/ValidationStatus/index.ts +1 -0
  32. package/src/components/Code/Workspace/StatusBar.tsx +47 -0
  33. package/src/components/Code/Workspace/Toolbar.tsx +47 -0
  34. package/src/components/Code/Workspace/Workspace.tsx +109 -0
  35. package/src/components/Code/Workspace/index.ts +3 -0
  36. package/src/components/ColorDot/ColorDot.tsx +50 -0
  37. package/src/components/ColorDot/index.ts +1 -0
  38. package/src/components/DatePicker/DatePicker.tsx +236 -0
  39. package/src/components/DatePicker/datepicker.hooks.ts +381 -0
  40. package/src/components/DatePicker/datepicker.types.ts +140 -0
  41. package/src/components/DatePicker/index.ts +12 -0
  42. package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
  43. package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
  44. package/src/components/DateRangePicker/index.ts +2 -0
  45. package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
  46. package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
  47. package/src/components/DateTimePicker/index.ts +2 -0
  48. package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
  49. package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
  50. package/src/components/DateTimeRangePicker/index.ts +2 -0
  51. package/src/components/EmptyState/EmptyState.tsx +29 -0
  52. package/src/components/EmptyState/index.ts +1 -0
  53. package/src/components/Field/Field.tsx +60 -0
  54. package/src/components/Field/index.ts +1 -0
  55. package/src/components/FormField/FormField.tsx +55 -0
  56. package/src/components/FormField/index.ts +2 -0
  57. package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
  58. package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
  59. package/src/components/HeatMap/heat-map.utils.ts +15 -0
  60. package/src/components/HeatMap/index.ts +3 -0
  61. package/src/components/HoverCard/HoverCard.tsx +83 -0
  62. package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
  63. package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
  64. package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
  65. package/src/components/HoverCard/index.ts +3 -0
  66. package/src/components/Input/Input.tsx +48 -0
  67. package/src/components/Input/index.ts +1 -0
  68. package/src/components/LineChart/LineChart.tsx +251 -0
  69. package/src/components/LineChart/index.ts +1 -0
  70. package/src/components/Markdown/Markdown.tsx +40 -0
  71. package/src/components/Markdown/index.ts +1 -0
  72. package/src/components/Message/Message.tsx +59 -0
  73. package/src/components/Message/components/MessageBody.tsx +9 -0
  74. package/src/components/Message/components/MessageDescription.tsx +9 -0
  75. package/src/components/Message/components/MessageIcon.tsx +13 -0
  76. package/src/components/Message/components/MessageTitle.tsx +9 -0
  77. package/src/components/Message/index.ts +10 -0
  78. package/src/components/MetricList/MetricList.tsx +13 -0
  79. package/src/components/MetricList/MetricRow.tsx +30 -0
  80. package/src/components/MetricList/index.ts +2 -0
  81. package/src/components/Modal/Modal.tsx +146 -0
  82. package/src/components/Modal/components/ModalBody.tsx +9 -0
  83. package/src/components/Modal/components/ModalContext.tsx +27 -0
  84. package/src/components/Modal/components/ModalFooter.tsx +9 -0
  85. package/src/components/Modal/components/ModalHeader.tsx +29 -0
  86. package/src/components/Modal/components/index.ts +9 -0
  87. package/src/components/Modal/index.ts +13 -0
  88. package/src/components/NavBottom/NavBottom.tsx +53 -0
  89. package/src/components/NavBottom/index.ts +3 -0
  90. package/src/components/NavSide/NavItem.tsx +39 -0
  91. package/src/components/NavSide/NavSide.tsx +40 -0
  92. package/src/components/NavSide/index.ts +2 -0
  93. package/src/components/NavSide/types.ts +36 -0
  94. package/src/components/Pagination/Pagination.tsx +86 -0
  95. package/src/components/Pagination/index.ts +2 -0
  96. package/src/components/Popover/Popover.tsx +68 -0
  97. package/src/components/Popover/components/PopoverBody.tsx +32 -0
  98. package/src/components/Popover/components/PopoverClose.tsx +26 -0
  99. package/src/components/Popover/components/PopoverContext.tsx +24 -0
  100. package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
  101. package/src/components/Popover/index.ts +10 -0
  102. package/src/components/ProgressBar/ProgressBar.tsx +42 -0
  103. package/src/components/ProgressBar/index.ts +1 -0
  104. package/src/components/RadioButton/RadioButton.tsx +89 -0
  105. package/src/components/RadioButton/index.ts +1 -0
  106. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
  107. package/src/components/RadioButtonGroup/index.ts +5 -0
  108. package/src/components/SearchInput/SearchInput.tsx +45 -0
  109. package/src/components/SearchInput/index.ts +2 -0
  110. package/src/components/Select/Select.tsx +289 -0
  111. package/src/components/Select/SelectDropdown.tsx +90 -0
  112. package/src/components/Select/SelectTrigger.tsx +130 -0
  113. package/src/components/Select/index.ts +3 -0
  114. package/src/components/Select/useSelectKeyboard.ts +111 -0
  115. package/src/components/Separator/Separator.tsx +20 -0
  116. package/src/components/Separator/index.ts +1 -0
  117. package/src/components/Sheet/Sheet.tsx +136 -0
  118. package/src/components/Sheet/components/SheetBody.tsx +9 -0
  119. package/src/components/Sheet/components/SheetContext.tsx +23 -0
  120. package/src/components/Sheet/components/SheetFooter.tsx +9 -0
  121. package/src/components/Sheet/components/SheetHeader.tsx +29 -0
  122. package/src/components/Sheet/components/index.ts +9 -0
  123. package/src/components/Sheet/index.ts +13 -0
  124. package/src/components/Skeleton/Skeleton.tsx +7 -0
  125. package/src/components/Skeleton/index.ts +1 -0
  126. package/src/components/Sparkline/Sparkline.tsx +93 -0
  127. package/src/components/Sparkline/index.ts +1 -0
  128. package/src/components/Spinner/Spinner.tsx +41 -0
  129. package/src/components/Spinner/index.ts +1 -0
  130. package/src/components/StatsCard/StatsCard.tsx +40 -0
  131. package/src/components/StatsCard/index.ts +1 -0
  132. package/src/components/Switch/Switch.tsx +128 -0
  133. package/src/components/Switch/index.ts +1 -0
  134. package/src/components/Tabs/Tabs.tsx +49 -0
  135. package/src/components/Tabs/components/TabsContext.tsx +35 -0
  136. package/src/components/Tabs/components/TabsList.tsx +98 -0
  137. package/src/components/Tabs/components/TabsPanels.tsx +26 -0
  138. package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
  139. package/src/components/Tabs/index.ts +3 -0
  140. package/src/components/TagChip/TagChip.tsx +46 -0
  141. package/src/components/TagChip/index.ts +1 -0
  142. package/src/components/TagInput/TagInput.tsx +60 -0
  143. package/src/components/TagInput/index.ts +1 -0
  144. package/src/components/Text/Text.tsx +35 -0
  145. package/src/components/Text/index.ts +1 -0
  146. package/src/components/TextInline/TextInline.tsx +35 -0
  147. package/src/components/TextInline/index.ts +1 -0
  148. package/src/components/Textarea/Textarea.tsx +20 -0
  149. package/src/components/Textarea/index.ts +1 -0
  150. package/src/components/TimePicker/PeriodSelector.tsx +28 -0
  151. package/src/components/TimePicker/TimeColumn.tsx +62 -0
  152. package/src/components/TimePicker/TimePicker.tsx +323 -0
  153. package/src/components/TimePicker/index.ts +7 -0
  154. package/src/components/TimePicker/timepicker.hooks.ts +173 -0
  155. package/src/components/TimePicker/timepicker.types.ts +101 -0
  156. package/src/components/Toast/Toast.tsx +111 -0
  157. package/src/components/Toast/index.ts +1 -0
  158. package/src/components/Toggle/Toggle.tsx +72 -0
  159. package/src/components/Toggle/index.ts +1 -0
  160. package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
  161. package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
  162. package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
  163. package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
  164. package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
  165. package/src/components/ToggleGroup/index.ts +2 -0
  166. package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
  167. package/src/components/Tooltip/Tooltip.tsx +66 -0
  168. package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
  169. package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
  170. package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
  171. package/src/components/Tooltip/index.ts +9 -0
  172. package/src/date-time/arithmetic.ts +101 -0
  173. package/src/date-time/boundaries.ts +91 -0
  174. package/src/date-time/compare.ts +85 -0
  175. package/src/date-time/duration.ts +63 -0
  176. package/src/date-time/elapsed.ts +18 -0
  177. package/src/date-time/format.ts +722 -0
  178. package/src/date-time/index.ts +279 -0
  179. package/src/date-time/now.ts +70 -0
  180. package/src/date-time/parse.ts +170 -0
  181. package/src/date-time/serialize.ts +90 -0
  182. package/src/date-time/timezone.ts +47 -0
  183. package/src/date-time/validate.ts +278 -0
  184. package/src/hooks/index.ts +10 -0
  185. package/src/hooks/useCollapsible.ts +16 -0
  186. package/src/hooks/useControllableState.ts +54 -0
  187. package/src/hooks/useDebouncedCallback.ts +21 -0
  188. package/src/hooks/useDebouncedState.ts +20 -0
  189. package/src/hooks/useDeviceFormFactor.ts +49 -0
  190. package/src/hooks/useLocalStorage.ts +67 -0
  191. package/src/hooks/useMergedFloatingRef.ts +29 -0
  192. package/src/hooks/usePresence.ts +127 -0
  193. package/src/hooks/useStack.ts +99 -0
  194. package/src/hooks/useTheme.ts +105 -0
  195. package/src/internal/Slot.tsx +79 -0
  196. package/src/internal/index.ts +2 -0
  197. package/src/internal/rovingFocus.ts +39 -0
  198. package/src/internal/types.ts +10 -0
  199. package/src/lib/EmptyBoundary.tsx +12 -0
  200. package/src/lib/ErrorBoundary.tsx +48 -0
  201. package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
  202. package/src/lib/contexts/ComponentsContext/index.ts +1 -0
  203. package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
  204. package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
  205. package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
  206. package/src/lib/contexts/OverlayContext/index.ts +6 -0
  207. package/src/lib/format.ts +51 -0
  208. package/src/lib/utils.ts +10 -0
  209. package/src/lib/withEmpty.tsx +14 -0
  210. package/src/lib/withErrorBoundary.tsx +16 -0
  211. package/src/lib/withSuspense.tsx +16 -0
  212. package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
  213. package/src/styles/base.css +41 -0
  214. package/src/theme/index.ts +37 -0
  215. package/src/theme/palette.json +100 -0
  216. package/src/theme/palette.schema.json +40 -0
  217. package/src/theme/schema.ts +122 -0
  218. package/src/theme/theme.json +349 -0
  219. package/src/theme/theme.schema.json +876 -0
  220. package/src/theme/typography.json +7 -0
  221. package/src/theme/typography.schema.json +36 -0
  222. package/src/typography/Body.tsx +8 -0
  223. package/src/typography/Caption.tsx +8 -0
  224. package/src/typography/Code.tsx +8 -0
  225. package/src/typography/Display.tsx +8 -0
  226. package/src/typography/Emphasis.tsx +8 -0
  227. package/src/typography/Heading.tsx +23 -0
  228. package/src/typography/Label.tsx +10 -0
  229. package/src/typography/Lead.tsx +8 -0
  230. package/src/typography/Mono.tsx +8 -0
  231. package/src/typography/Muted.tsx +8 -0
  232. package/src/typography/SectionTitle.tsx +8 -0
  233. package/src/typography/Subtitle.tsx +8 -0
  234. package/src/typography/Timestamp.tsx +8 -0
  235. package/src/typography/index.ts +10 -0
  236. package/tailwind.config.ts +261 -0
@@ -0,0 +1,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,2 @@
1
+ export { ToggleGroup } from "./ToggleGroup";
2
+ export type { ToggleGroupOption, ToggleGroupProps } from "./toggleGroup.types";
@@ -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";
@@ -0,0 +1,9 @@
1
+ export {
2
+ TooltipBody,
3
+ type TooltipBodyProps,
4
+ } from "./components/TooltipBody";
5
+ export {
6
+ TooltipTrigger,
7
+ type TooltipTriggerProps,
8
+ } from "./components/TooltipTrigger";
9
+ export { Tooltip, type TooltipProps } from "./Tooltip";