@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,130 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import { ChevronDown, X } from "lucide-react";
3
+ import type { HTMLAttributes } from "react";
4
+
5
+ interface SelectTriggerButtonProps {
6
+ setReference: (node: HTMLElement | null) => void;
7
+ listboxId: string;
8
+ open: boolean;
9
+ disabled: boolean;
10
+ collapsed: boolean;
11
+ displayLabel: string;
12
+ showClearButton: boolean;
13
+ className: string | undefined;
14
+ onToggle: () => void;
15
+ onKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
16
+ onClear: (e: React.MouseEvent) => void;
17
+ props: HTMLAttributes<HTMLElement>;
18
+ }
19
+
20
+ export const SelectTriggerButton = ({
21
+ setReference,
22
+ listboxId,
23
+ open,
24
+ disabled,
25
+ collapsed,
26
+ displayLabel,
27
+ showClearButton,
28
+ className,
29
+ onToggle,
30
+ onKeyDown,
31
+ onClear,
32
+ props,
33
+ }: SelectTriggerButtonProps) => {
34
+ // The clear button must be a sibling of the trigger, not a descendant: nesting
35
+ // one interactive control inside another is invalid HTML and leaves assistive
36
+ // tech unable to reach the inner control (axe: nested-interactive).
37
+ const showClear = showClearButton && !collapsed;
38
+
39
+ return (
40
+ <div className="relative w-full">
41
+ <button
42
+ ref={setReference}
43
+ type="button"
44
+ onClick={onToggle}
45
+ onKeyDown={onKeyDown}
46
+ disabled={disabled}
47
+ className={cn(
48
+ `flex w-full cursor-pointer items-center gap-2 rounded-md border border-border-normal bg-bg-normal px-3 py-2 font-medium text-fg-normal text-sm transition-colors hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50`,
49
+ collapsed ? "justify-center" : "justify-between",
50
+ showClear && "pr-9",
51
+ className
52
+ )}
53
+ title={collapsed ? displayLabel : undefined}
54
+ aria-haspopup="listbox"
55
+ aria-expanded={open}
56
+ // The listbox unmounts when closed, so only reference it while it exists.
57
+ aria-controls={open ? listboxId : undefined}
58
+ {...props}
59
+ >
60
+ {collapsed ? (
61
+ <span className="font-bold text-xs">{displayLabel.charAt(0)}</span>
62
+ ) : (
63
+ <>
64
+ <span className="truncate">{displayLabel}</span>
65
+ <span className="flex shrink-0 items-center gap-1">
66
+ <ChevronDown className={cn("h-4 w-4 text-fg-subtle transition-transform", open && "rotate-180")} />
67
+ </span>
68
+ </>
69
+ )}
70
+ </button>
71
+ {showClear && (
72
+ <button
73
+ type="button"
74
+ onClick={onClear}
75
+ disabled={disabled}
76
+ className="absolute top-1/2 right-8 -translate-y-1/2 rounded-sm p-0.5 text-fg-subtle hover:bg-bg-subtle hover:text-fg-normal focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
77
+ aria-label="Clear selection"
78
+ >
79
+ <X className="h-3.5 w-3.5" />
80
+ </button>
81
+ )}
82
+ </div>
83
+ );
84
+ };
85
+
86
+ interface SelectTriggerSearchProps {
87
+ setReference: (node: HTMLElement | null) => void;
88
+ listboxId: string;
89
+ inputRef: React.RefObject<HTMLInputElement | null>;
90
+ searchValue: string;
91
+ disabled: boolean;
92
+ displayLabel: string;
93
+ open: boolean;
94
+ className: string | undefined;
95
+ onSearchChange: (value: string) => void;
96
+ props: React.InputHTMLAttributes<HTMLInputElement>;
97
+ }
98
+
99
+ export const SelectTriggerSearch = ({
100
+ setReference,
101
+ listboxId,
102
+ inputRef,
103
+ searchValue,
104
+ disabled,
105
+ displayLabel,
106
+ open,
107
+ className,
108
+ onSearchChange,
109
+ props,
110
+ }: SelectTriggerSearchProps) => (
111
+ <div ref={setReference} className={cn("relative flex w-full items-center", className)}>
112
+ <input
113
+ ref={inputRef as React.RefObject<HTMLInputElement>}
114
+ type="text"
115
+ value={searchValue}
116
+ onChange={(e) => onSearchChange(e.target.value)}
117
+ placeholder={displayLabel}
118
+ disabled={disabled}
119
+ className={cn(
120
+ `flex-1 rounded-md border border-border-normal bg-bg-normal px-3 py-2 font-medium text-fg-normal text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50`
121
+ )}
122
+ role="combobox"
123
+ aria-expanded={open}
124
+ aria-controls={listboxId}
125
+ aria-autocomplete="list"
126
+ {...props}
127
+ />
128
+ <ChevronDown className="pointer-events-none absolute right-3 h-4 w-4 text-fg-subtle" />
129
+ </div>
130
+ );
@@ -0,0 +1,3 @@
1
+ export { Select, type SelectOption, type SelectProps } from "./Select";
2
+ export { SelectDropdown } from "./SelectDropdown";
3
+ export { SelectTriggerButton, SelectTriggerSearch } from "./SelectTrigger";
@@ -0,0 +1,111 @@
1
+ import { useEffect } from "react";
2
+ import type { SelectOption } from "./Select";
3
+
4
+ interface UseSelectKeyboardOptions<T extends string> {
5
+ open: boolean;
6
+ searchable: boolean;
7
+ searchValue: string;
8
+ highlightedIndex: number;
9
+ displayOptions: readonly SelectOption<T>[];
10
+ setOpen: (open: boolean) => void;
11
+ setHighlightedIndex: React.Dispatch<React.SetStateAction<number>>;
12
+ setSearchValue: (value: string) => void;
13
+ handleSelect: (value: T) => void;
14
+ focusTrigger: () => void;
15
+ }
16
+
17
+ export function useSelectKeyboard<T extends string>({
18
+ open,
19
+ searchable,
20
+ searchValue,
21
+ highlightedIndex,
22
+ displayOptions,
23
+ setOpen,
24
+ setHighlightedIndex,
25
+ setSearchValue,
26
+ handleSelect,
27
+ focusTrigger,
28
+ }: UseSelectKeyboardOptions<T>) {
29
+ useEffect(() => {
30
+ const handleKeyDown = (event: KeyboardEvent) => {
31
+ if (!open) return;
32
+
33
+ const isTyping = searchable && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
34
+ if (isTyping) return;
35
+
36
+ switch (event.key) {
37
+ case "Escape":
38
+ event.preventDefault();
39
+ if (searchable && searchValue) {
40
+ setSearchValue("");
41
+ setHighlightedIndex(0);
42
+ } else {
43
+ setOpen(false);
44
+ focusTrigger();
45
+ }
46
+ break;
47
+
48
+ case "ArrowDown":
49
+ event.preventDefault();
50
+ setHighlightedIndex((prev) => (prev < displayOptions.length - 1 ? prev + 1 : prev));
51
+ break;
52
+
53
+ case "ArrowUp":
54
+ event.preventDefault();
55
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
56
+ break;
57
+
58
+ case "Home":
59
+ event.preventDefault();
60
+ setHighlightedIndex(0);
61
+ break;
62
+
63
+ case "End":
64
+ event.preventDefault();
65
+ setHighlightedIndex(displayOptions.length - 1);
66
+ break;
67
+
68
+ case "Enter":
69
+ event.preventDefault();
70
+ if (highlightedIndex >= 0 && highlightedIndex < displayOptions.length) {
71
+ handleSelect(displayOptions[highlightedIndex]!.value);
72
+ }
73
+ break;
74
+
75
+ case " ":
76
+ if (!searchable) {
77
+ event.preventDefault();
78
+ if (highlightedIndex >= 0 && highlightedIndex < displayOptions.length) {
79
+ handleSelect(displayOptions[highlightedIndex]!.value);
80
+ }
81
+ }
82
+ break;
83
+
84
+ case "Tab":
85
+ setOpen(false);
86
+ break;
87
+
88
+ default:
89
+ if (!searchable && event.key.length === 1) {
90
+ const letter = event.key.toLowerCase();
91
+ const startIndex = highlightedIndex + 1;
92
+ const matchIndex = displayOptions.findIndex((opt, idx) => idx >= startIndex && opt.label.toLowerCase().startsWith(letter));
93
+
94
+ if (matchIndex >= 0) {
95
+ setHighlightedIndex(matchIndex);
96
+ } else {
97
+ const wrapMatchIndex = displayOptions.findIndex((opt) => opt.label.toLowerCase().startsWith(letter));
98
+ if (wrapMatchIndex >= 0) {
99
+ setHighlightedIndex(wrapMatchIndex);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ };
105
+
106
+ if (open) {
107
+ document.addEventListener("keydown", handleKeyDown);
108
+ return () => document.removeEventListener("keydown", handleKeyDown);
109
+ }
110
+ }, [open, searchable, searchValue, highlightedIndex, displayOptions, setOpen, setHighlightedIndex, setSearchValue, handleSelect, focusTrigger]);
111
+ }
@@ -0,0 +1,20 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import * as React from "react";
3
+
4
+ export type SeparatorProps = React.HTMLAttributes<HTMLDivElement> & {
5
+ orientation?: "horizontal" | "vertical";
6
+ decorative?: boolean;
7
+ };
8
+
9
+ const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ role={decorative ? "none" : "separator"}
13
+ {...(!decorative && { "aria-orientation": orientation })}
14
+ className={cn("shrink-0 bg-border-normal", orientation === "horizontal" ? "h-px w-full" : "h-full w-px", className)}
15
+ {...props}
16
+ />
17
+ ));
18
+ Separator.displayName = "Separator";
19
+
20
+ export { Separator };
@@ -0,0 +1 @@
1
+ export { Separator, type SeparatorProps } from "./Separator";
@@ -0,0 +1,136 @@
1
+ import { FloatingFocusManager, useDismiss, useFloating, useInteractions, useRole } from "@floating-ui/react";
2
+ import { useMergedFloatingRef, usePresence } from "@sixthshift/design-system/hooks";
3
+ import { cn } from "@sixthshift/design-system/utils";
4
+ import { type HTMLAttributes, type ReactNode, useCallback, useEffect, useMemo } from "react";
5
+ import { SheetBody, SheetContext, SheetFooter, SheetHeader } from "./components";
6
+
7
+ // =============================================================================
8
+ // Types
9
+ // =============================================================================
10
+
11
+ export type SheetProps = Pick<HTMLAttributes<HTMLDivElement>, "className" | "style"> & {
12
+ /** Whether the sheet is open. Sheet owns its own mount lifecycle so exit animations can play. */
13
+ open: boolean;
14
+ /** Called when the sheet requests a state change (Esc, outside press, X button). */
15
+ onOpenChange: (open: boolean) => void;
16
+ /** Which edge the sheet slides in from */
17
+ side?: "right" | "left";
18
+ /** Sheet width on desktop — mobile is always full width */
19
+ size?: "sm" | "md" | "lg";
20
+ /** When true (default), Esc closes the sheet. */
21
+ dismissable?: boolean;
22
+ /** When true, clicking outside the sheet closes it. Defaults to false — Sheet is a persistent workspace, not a transient popover. */
23
+ dismissOnOutsidePress?: boolean;
24
+ /** When true, shows a close (X) button in the header */
25
+ closable?: boolean;
26
+ /** Children — use Sheet.Header, Sheet.Body, Sheet.Footer */
27
+ children: ReactNode;
28
+ };
29
+
30
+ // =============================================================================
31
+ // Size classes
32
+ // =============================================================================
33
+
34
+ const sizeClasses = {
35
+ sm: "sm:w-[22rem]",
36
+ md: "sm:w-[30rem]",
37
+ lg: "sm:w-[40rem]",
38
+ };
39
+
40
+ // =============================================================================
41
+ // Sheet
42
+ // =============================================================================
43
+
44
+ const SheetRoot = ({
45
+ open,
46
+ onOpenChange,
47
+ side = "right",
48
+ size = "md",
49
+ dismissable = true,
50
+ dismissOnOutsidePress = false,
51
+ closable,
52
+ className,
53
+ style,
54
+ children,
55
+ }: SheetProps) => {
56
+ const { ref: presenceRef, state, isMounted, show, hide } = usePresence();
57
+
58
+ // Drive presence from the controlled `open` prop.
59
+ // When `open` flips to false, we play the exit animation, then unmount.
60
+ useEffect(() => {
61
+ if (open) {
62
+ show();
63
+ } else if (isMounted) {
64
+ hide();
65
+ }
66
+ }, [open, isMounted, show, hide]);
67
+
68
+ const requestClose = useCallback(() => {
69
+ onOpenChange(false);
70
+ }, [onOpenChange]);
71
+
72
+ const { refs, context } = useFloating({
73
+ open,
74
+ onOpenChange: (nextOpen) => {
75
+ if (!nextOpen) requestClose();
76
+ },
77
+ });
78
+
79
+ const mergedRef = useMergedFloatingRef(refs, presenceRef);
80
+
81
+ const dismiss = useDismiss(context, {
82
+ enabled: dismissable,
83
+ escapeKey: true,
84
+ outsidePress: dismissOnOutsidePress,
85
+ outsidePressEvent: "mousedown",
86
+ });
87
+
88
+ const role = useRole(context, { role: "dialog" });
89
+
90
+ const { getFloatingProps } = useInteractions([dismiss, role]);
91
+
92
+ const sheetContextValue = useMemo(() => ({ onClose: requestClose, closable }), [requestClose, closable]);
93
+
94
+ if (!isMounted) return null;
95
+
96
+ const isEntering = state === "entering";
97
+ const isExiting = state === "exiting";
98
+
99
+ return (
100
+ <FloatingFocusManager context={context} modal={false}>
101
+ <div
102
+ ref={mergedRef}
103
+ tabIndex={-1}
104
+ data-state={state}
105
+ data-side={side}
106
+ className={cn(
107
+ "fixed top-0 bottom-0 z-sheet flex flex-col overflow-hidden bg-bg-normal outline-hidden",
108
+ "border-border-normal shadow-lg",
109
+ side === "right" ? "right-0 border-l" : "left-0 border-r",
110
+ sizeClasses[size],
111
+ "max-sm:inset-x-0",
112
+ isEntering && side === "right" && "animate-slide-right-in",
113
+ isExiting && side === "right" && "animate-slide-right-out",
114
+ isEntering && side === "left" && "animate-slide-left-in",
115
+ isExiting && side === "left" && "animate-slide-left-out",
116
+ className
117
+ )}
118
+ style={style}
119
+ {...getFloatingProps({})}
120
+ >
121
+ <SheetContext.Provider value={sheetContextValue}>{children}</SheetContext.Provider>
122
+ </div>
123
+ </FloatingFocusManager>
124
+ );
125
+ };
126
+ SheetRoot.displayName = "Sheet";
127
+
128
+ // =============================================================================
129
+ // Compound Component Export
130
+ // =============================================================================
131
+
132
+ export const Sheet = Object.assign(SheetRoot, {
133
+ Header: SheetHeader,
134
+ Body: SheetBody,
135
+ Footer: SheetFooter,
136
+ });
@@ -0,0 +1,9 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import * as React from "react";
3
+
4
+ export type SheetBodyProps = React.HTMLAttributes<HTMLDivElement>;
5
+
6
+ export const SheetBody = React.forwardRef<HTMLDivElement, SheetBodyProps>(({ className, ...props }, ref) => (
7
+ <div ref={ref} className={cn("flex-1 overflow-y-auto p-6", className)} {...props} />
8
+ ));
9
+ SheetBody.displayName = "SheetBody";
@@ -0,0 +1,23 @@
1
+ import { createContext, type KeyboardEvent, type MouseEvent, type PropsWithChildren, useContext, useMemo } from "react";
2
+
3
+ export type OnClose = (event?: MouseEvent<Element> | KeyboardEvent<Element>) => void;
4
+
5
+ interface SheetContextValue {
6
+ onClose: OnClose;
7
+ closable?: boolean | undefined;
8
+ }
9
+
10
+ export const SheetContext = createContext<SheetContextValue | null>(null);
11
+
12
+ export function useSheetContext() {
13
+ const context = useContext(SheetContext);
14
+ if (!context) {
15
+ throw new Error("useSheetContext must be used within a Sheet");
16
+ }
17
+ return context;
18
+ }
19
+
20
+ export const SheetContextProvider = ({ onClose, closable, children }: PropsWithChildren<{ onClose: OnClose; closable?: boolean }>) => {
21
+ const value = useMemo(() => ({ onClose, closable }), [onClose, closable]);
22
+ return <SheetContext.Provider value={value}>{children}</SheetContext.Provider>;
23
+ };
@@ -0,0 +1,9 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import * as React from "react";
3
+
4
+ export type SheetFooterProps = React.HTMLAttributes<HTMLDivElement>;
5
+
6
+ export const SheetFooter = React.forwardRef<HTMLDivElement, SheetFooterProps>(({ className, ...props }, ref) => (
7
+ <div ref={ref} className={cn("flex items-center border-border-normal border-t p-4", className)} {...props} />
8
+ ));
9
+ SheetFooter.displayName = "SheetFooter";
@@ -0,0 +1,29 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import { X } from "lucide-react";
3
+ import * as React from "react";
4
+ import { SheetContext } from "./SheetContext";
5
+
6
+ export type SheetHeaderProps = React.HTMLAttributes<HTMLDivElement>;
7
+
8
+ export const SheetHeader = React.forwardRef<HTMLDivElement, SheetHeaderProps>(({ className, children, ...props }, ref) => {
9
+ const context = React.useContext(SheetContext);
10
+ const closable = context?.closable;
11
+ const onClose = context?.onClose;
12
+
13
+ return (
14
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 border-border-normal border-b p-6", closable && "pr-12", className)} {...props}>
15
+ {children}
16
+ {closable && (
17
+ <button
18
+ type="button"
19
+ onClick={onClose}
20
+ aria-label="Close"
21
+ className="absolute top-4 right-4 cursor-pointer rounded-md p-1 text-fg-subtle transition-colors hover:bg-bg-subtle hover:text-fg-normal"
22
+ >
23
+ <X size={16} />
24
+ </button>
25
+ )}
26
+ </div>
27
+ );
28
+ });
29
+ SheetHeader.displayName = "SheetHeader";
@@ -0,0 +1,9 @@
1
+ export { SheetBody, type SheetBodyProps } from "./SheetBody";
2
+ export {
3
+ type OnClose,
4
+ SheetContext,
5
+ SheetContextProvider,
6
+ useSheetContext,
7
+ } from "./SheetContext";
8
+ export { SheetFooter, type SheetFooterProps } from "./SheetFooter";
9
+ export { SheetHeader, type SheetHeaderProps } from "./SheetHeader";
@@ -0,0 +1,13 @@
1
+ export {
2
+ type OnClose,
3
+ SheetBody,
4
+ type SheetBodyProps,
5
+ SheetContext,
6
+ SheetContextProvider,
7
+ SheetFooter,
8
+ type SheetFooterProps,
9
+ SheetHeader,
10
+ type SheetHeaderProps,
11
+ useSheetContext,
12
+ } from "./components";
13
+ export { Sheet, type SheetProps } from "./Sheet";
@@ -0,0 +1,7 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+
3
+ const Skeleton = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
4
+ return <div className={cn("animate-pulse rounded-md bg-bg-strong/10", className)} {...props} />;
5
+ };
6
+
7
+ export { Skeleton };
@@ -0,0 +1 @@
1
+ export { Skeleton } from "./Skeleton";
@@ -0,0 +1,93 @@
1
+ import type { Interpolation } from "@sixthshift/design-system/line-chart";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import type * as React from "react";
4
+
5
+ export type SparklineProps = React.HTMLAttributes<HTMLSpanElement> & {
6
+ data: number[];
7
+ width?: number;
8
+ height?: number;
9
+ color?: string;
10
+ fillArea?: boolean;
11
+ interpolation?: Interpolation;
12
+ strokeWidth?: number;
13
+ };
14
+
15
+ function buildPath(points: { x: number; y: number }[], interpolation: Interpolation): string {
16
+ if (points.length === 0) return "";
17
+ const parts = [`M ${points[0]!.x} ${points[0]!.y}`];
18
+
19
+ switch (interpolation) {
20
+ case "monotone":
21
+ for (let i = 1; i < points.length; i++) {
22
+ const prev = points[i - 1]!;
23
+ const curr = points[i]!;
24
+ const cpx = (prev.x + curr.x) / 2;
25
+ parts.push(`C ${cpx} ${prev.y}, ${cpx} ${curr.y}, ${curr.x} ${curr.y}`);
26
+ }
27
+ break;
28
+ case "stepAfter":
29
+ for (let i = 1; i < points.length; i++) {
30
+ const prev = points[i - 1]!;
31
+ const curr = points[i]!;
32
+ parts.push(`L ${curr.x} ${prev.y}`);
33
+ parts.push(`L ${curr.x} ${curr.y}`);
34
+ }
35
+ break;
36
+ case "stepBefore":
37
+ for (let i = 1; i < points.length; i++) {
38
+ const prev = points[i - 1]!;
39
+ const curr = points[i]!;
40
+ parts.push(`L ${prev.x} ${curr.y}`);
41
+ parts.push(`L ${curr.x} ${curr.y}`);
42
+ }
43
+ break;
44
+ default:
45
+ for (let i = 1; i < points.length; i++) {
46
+ parts.push(`L ${points[i]!.x} ${points[i]!.y}`);
47
+ }
48
+ break;
49
+ }
50
+
51
+ return parts.join(" ");
52
+ }
53
+
54
+ export const Sparkline = ({
55
+ data,
56
+ width = 80,
57
+ height = 24,
58
+ color = "var(--fg-brand)",
59
+ fillArea = false,
60
+ interpolation = "monotone",
61
+ strokeWidth = 1.5,
62
+ className,
63
+ ...rest
64
+ }: SparklineProps) => {
65
+ if (data.length < 2) return null;
66
+
67
+ const min = Math.min(...data);
68
+ const max = Math.max(...data);
69
+ const range = max - min || 1;
70
+
71
+ const padY = 2;
72
+ const padX = 1;
73
+ const innerW = width - padX * 2;
74
+ const innerH = height - padY * 2;
75
+
76
+ const points = data.map((v, i) => ({
77
+ x: padX + (i / (data.length - 1)) * innerW,
78
+ y: padY + innerH - ((v - min) / range) * innerH,
79
+ }));
80
+
81
+ const path = buildPath(points, interpolation);
82
+
83
+ return (
84
+ <span {...rest} className={cn("inline-block align-middle", className)}>
85
+ <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} role="img" aria-label="Trend line">
86
+ {fillArea && path && (
87
+ <path d={`${path} L ${points[points.length - 1]!.x} ${height - padY} L ${points[0]!.x} ${height - padY} Z`} fill={color} opacity={0.12} />
88
+ )}
89
+ <path d={path} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" />
90
+ </svg>
91
+ </span>
92
+ );
93
+ };
@@ -0,0 +1 @@
1
+ export { Sparkline, type SparklineProps } from "./Sparkline";
@@ -0,0 +1,41 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import * as React from "react";
4
+
5
+ const spinnerVariants = cva("animate-spin text-fg-subtle", {
6
+ variants: {
7
+ size: {
8
+ sm: "h-4 w-4",
9
+ default: "h-6 w-6",
10
+ lg: "h-8 w-8",
11
+ xl: "h-12 w-12",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ size: "default",
16
+ },
17
+ });
18
+
19
+ export type SpinnerProps = React.SVGAttributes<SVGElement> & VariantProps<typeof spinnerVariants>;
20
+
21
+ const Spinner = React.forwardRef<SVGSVGElement, SpinnerProps>(({ className, size, ...props }, ref) => (
22
+ <svg
23
+ ref={ref}
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ fill="none"
26
+ viewBox="0 0 24 24"
27
+ className={cn(spinnerVariants({ size, className }))}
28
+ aria-hidden="true"
29
+ {...props}
30
+ >
31
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
32
+ <path
33
+ className="opacity-75"
34
+ fill="currentColor"
35
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
36
+ />
37
+ </svg>
38
+ ));
39
+ Spinner.displayName = "Spinner";
40
+
41
+ export { Spinner, spinnerVariants };
@@ -0,0 +1 @@
1
+ export { Spinner, type SpinnerProps, spinnerVariants } from "./Spinner";