@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,40 @@
1
+ import { Caption } from "@sixthshift/design-system/caption";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import type { ReactNode } from "react";
4
+
5
+ export type StatsCardProps = {
6
+ /** Title of the stats card */
7
+ title: string;
8
+ /** Description text shown below the title */
9
+ description: string;
10
+ /** Optional icon displayed in the header */
11
+ icon?: ReactNode;
12
+ /** Visual status indicator via left border color */
13
+ status?: "healthy" | "warning" | "error" | "neutral";
14
+ /** Card content */
15
+ children: ReactNode;
16
+ /** Additional CSS classes */
17
+ className?: string;
18
+ };
19
+
20
+ const statusColors = {
21
+ healthy: "border-l-green-500",
22
+ warning: "border-l-amber-500",
23
+ error: "border-l-red-500",
24
+ neutral: "border-l-border-subtle",
25
+ };
26
+
27
+ export const StatsCard = ({ title, description, icon, status = "neutral", children, className }: StatsCardProps) => {
28
+ return (
29
+ <div className={cn("flex flex-col gap-3 rounded border border-border-subtle border-l-2 bg-bg-subtle p-4", statusColors[status], className)}>
30
+ <div className="flex items-start justify-between gap-2">
31
+ <div className="flex flex-col gap-0.5">
32
+ <span className="font-medium text-fg-normal">{title}</span>
33
+ <Caption>{description}</Caption>
34
+ </div>
35
+ {icon && <div className="text-fg-subtle">{icon}</div>}
36
+ </div>
37
+ <div className="flex-1">{children}</div>
38
+ </div>
39
+ );
40
+ };
@@ -0,0 +1 @@
1
+ export { StatsCard, type StatsCardProps } from "./StatsCard";
@@ -0,0 +1,128 @@
1
+ import { useControllableState } from "@sixthshift/design-system/hooks";
2
+ import { Label } from "@sixthshift/design-system/label";
3
+ import { cn } from "@sixthshift/design-system/utils";
4
+ import * as React from "react";
5
+
6
+ export type SwitchProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange" | "value"> & {
7
+ /** Controlled checked state */
8
+ checked?: boolean;
9
+ /** Default checked state for uncontrolled mode */
10
+ defaultChecked?: boolean;
11
+ /** Called when checked state changes */
12
+ onCheckedChange?: (checked: boolean) => void;
13
+ /** Shows a loading spinner in the thumb and prevents interaction */
14
+ pending?: boolean;
15
+ /** Input name for form submission */
16
+ name?: string;
17
+ /** Value submitted with form (defaults to "on") */
18
+ value?: string;
19
+ /** Label rendered next to the switch. Wraps in a <Label> for a11y. */
20
+ label?: React.ReactNode;
21
+ /** Classes applied to the wrapping element when `label` is provided */
22
+ labelClassName?: string;
23
+ };
24
+
25
+ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
26
+ (
27
+ {
28
+ className,
29
+ checked: controlledChecked,
30
+ defaultChecked = false,
31
+ onCheckedChange,
32
+ pending = false,
33
+ disabled,
34
+ name,
35
+ value = "on",
36
+ label,
37
+ labelClassName,
38
+ id: idProp,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ const generatedId = React.useId();
44
+ const id = idProp ?? (label ? generatedId : undefined);
45
+
46
+ const [checked, setChecked] = useControllableState({
47
+ value: controlledChecked,
48
+ defaultValue: defaultChecked,
49
+ onChange: onCheckedChange,
50
+ });
51
+
52
+ const button = (
53
+ <>
54
+ <button
55
+ type="button"
56
+ role="switch"
57
+ id={id}
58
+ aria-checked={checked}
59
+ aria-busy={pending || undefined}
60
+ data-state={checked ? "checked" : "unchecked"}
61
+ data-pending={pending || undefined}
62
+ disabled={disabled}
63
+ ref={ref}
64
+ onClick={() => !pending && setChecked(!checked)}
65
+ className={cn(
66
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg-normal disabled:cursor-not-allowed disabled:opacity-50",
67
+ checked ? "bg-bg-brand" : "bg-bg-subtle",
68
+ pending && "cursor-default",
69
+ className
70
+ )}
71
+ {...props}
72
+ >
73
+ <span
74
+ className={cn(
75
+ "pointer-events-none flex h-4 w-4 items-center justify-center rounded-full bg-bg-normal shadow-lg ring-0 transition-transform",
76
+ checked ? "translate-x-4" : "translate-x-0"
77
+ )}
78
+ >
79
+ {pending && (
80
+ <svg className="h-2.5 w-2.5 animate-spin text-fg-subtle" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
81
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
82
+ <path
83
+ className="opacity-75"
84
+ fill="currentColor"
85
+ 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"
86
+ />
87
+ </svg>
88
+ )}
89
+ </span>
90
+ </button>
91
+ {name && (
92
+ <input
93
+ type="checkbox"
94
+ aria-hidden
95
+ tabIndex={-1}
96
+ name={name}
97
+ value={value}
98
+ checked={checked}
99
+ disabled={disabled}
100
+ readOnly
101
+ style={{
102
+ position: "absolute",
103
+ pointerEvents: "none",
104
+ opacity: 0,
105
+ margin: 0,
106
+ width: 0,
107
+ height: 0,
108
+ }}
109
+ />
110
+ )}
111
+ </>
112
+ );
113
+
114
+ if (!label) return button;
115
+
116
+ return (
117
+ <div className={cn("flex items-center gap-2", labelClassName)}>
118
+ {button}
119
+ <Label htmlFor={id} className={cn(disabled && "cursor-not-allowed opacity-50")}>
120
+ {label}
121
+ </Label>
122
+ </div>
123
+ );
124
+ }
125
+ );
126
+ Switch.displayName = "Switch";
127
+
128
+ export { Switch };
@@ -0,0 +1 @@
1
+ export { Switch, type SwitchProps } from "./Switch";
@@ -0,0 +1,49 @@
1
+ import { useControllableState } from "@sixthshift/design-system/hooks";
2
+ import type * as React from "react";
3
+ import { useId } from "react";
4
+ import { type TabItem, TabsContext } from "./components/TabsContext";
5
+ import { TabsList } from "./components/TabsList";
6
+ import { TabsPanels } from "./components/TabsPanels";
7
+
8
+ export type TabsProps = {
9
+ /** Tab definitions */
10
+ items: TabItem[];
11
+ /** Controlled selected value */
12
+ value?: string;
13
+ /** Default value for uncontrolled mode */
14
+ defaultValue?: string;
15
+ /** Callback when selection changes */
16
+ onValueChange?: (value: string) => void;
17
+ /** Children - compound components (Tabs.List, Tabs.Panels) */
18
+ children: React.ReactNode;
19
+ };
20
+
21
+ const TabsRoot = ({ items, value: controlledValue, defaultValue, onValueChange, children }: TabsProps) => {
22
+ // Default to first non-disabled item if no defaultValue provided
23
+ const firstEnabledValue = items.find((item) => !item.disabled)?.value ?? items[0]?.value ?? "";
24
+
25
+ const [value, setValue] = useControllableState({
26
+ value: controlledValue,
27
+ defaultValue: defaultValue ?? firstEnabledValue,
28
+ onChange: onValueChange,
29
+ });
30
+
31
+ const id = useId();
32
+ const baseId = `tabs-${id}`;
33
+
34
+ const contextValue: React.ContextType<typeof TabsContext> = {
35
+ items,
36
+ value,
37
+ onValueChange: setValue,
38
+ baseId,
39
+ };
40
+
41
+ return <TabsContext.Provider value={contextValue}>{children}</TabsContext.Provider>;
42
+ };
43
+
44
+ export const Tabs = Object.assign(TabsRoot, {
45
+ List: TabsList,
46
+ Panels: TabsPanels,
47
+ });
48
+
49
+ export type { TabItem };
@@ -0,0 +1,35 @@
1
+ import { createContext, type ReactNode, useContext } from "react";
2
+
3
+ export type TabItem = {
4
+ /** Unique identifier for this tab */
5
+ value: string;
6
+ /** Tab trigger content (displayed in the tab list) */
7
+ label: ReactNode;
8
+ /** Optional badge to display on the tab */
9
+ badge?: number | string;
10
+ /** Whether this tab is disabled */
11
+ disabled?: boolean;
12
+ /** Panel content - can be a ReactNode or lazy function */
13
+ content: ReactNode | (() => ReactNode);
14
+ };
15
+
16
+ export type TabsContextValue = {
17
+ /** All tab items */
18
+ items: TabItem[];
19
+ /** Currently selected tab value */
20
+ value: string;
21
+ /** Callback when selection changes */
22
+ onValueChange: (value: string) => void;
23
+ /** Base ID for generating ARIA IDs */
24
+ baseId: string;
25
+ };
26
+
27
+ export const TabsContext = createContext<TabsContextValue | null>(null);
28
+
29
+ export function useTabsContext() {
30
+ const context = useContext(TabsContext);
31
+ if (!context) {
32
+ throw new Error("Tabs compound components must be used within <Tabs>");
33
+ }
34
+ return context;
35
+ }
@@ -0,0 +1,98 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import type * as React from "react";
3
+ import { useCallback, useRef } from "react";
4
+ import { useTabsContext } from "./TabsContext";
5
+ import { TabsTrigger } from "./TabsTrigger";
6
+
7
+ export type TabsListProps = {
8
+ /** Layout direction */
9
+ orientation?: "horizontal" | "vertical";
10
+ /** Additional CSS classes */
11
+ className?: string;
12
+ };
13
+
14
+ export const TabsList = ({ orientation = "horizontal", className }: TabsListProps) => {
15
+ const { items, value, onValueChange } = useTabsContext();
16
+ const triggerRefs = useRef<(HTMLButtonElement | null)[]>([]);
17
+
18
+ // Get enabled items and their indices
19
+ const enabledItems = items.map((item, index) => ({ item, index })).filter(({ item }) => !item.disabled);
20
+
21
+ const getCurrentEnabledIndex = useCallback(() => {
22
+ return enabledItems.findIndex(({ item }) => item.value === value);
23
+ }, [enabledItems, value]);
24
+
25
+ const focusAndSelect = useCallback(
26
+ (index: number) => {
27
+ const targetRef = triggerRefs.current[index];
28
+ if (targetRef) {
29
+ targetRef.focus();
30
+ const item = items[index];
31
+ if (item && !item.disabled) {
32
+ onValueChange(item.value);
33
+ }
34
+ }
35
+ },
36
+ [items, onValueChange]
37
+ );
38
+
39
+ const handleKeyDown = useCallback(
40
+ (event: React.KeyboardEvent) => {
41
+ const currentEnabledIdx = getCurrentEnabledIndex();
42
+ if (currentEnabledIdx === -1 || enabledItems.length === 0) return;
43
+
44
+ const isHorizontal = orientation === "horizontal";
45
+ const prevKey = isHorizontal ? "ArrowLeft" : "ArrowUp";
46
+ const nextKey = isHorizontal ? "ArrowRight" : "ArrowDown";
47
+
48
+ let newEnabledIdx = currentEnabledIdx;
49
+
50
+ switch (event.key) {
51
+ case prevKey:
52
+ event.preventDefault();
53
+ newEnabledIdx = currentEnabledIdx > 0 ? currentEnabledIdx - 1 : enabledItems.length - 1;
54
+ break;
55
+ case nextKey:
56
+ event.preventDefault();
57
+ newEnabledIdx = currentEnabledIdx < enabledItems.length - 1 ? currentEnabledIdx + 1 : 0;
58
+ break;
59
+ case "Home":
60
+ event.preventDefault();
61
+ newEnabledIdx = 0;
62
+ break;
63
+ case "End":
64
+ event.preventDefault();
65
+ newEnabledIdx = enabledItems.length - 1;
66
+ break;
67
+ default:
68
+ return;
69
+ }
70
+
71
+ const targetItem = enabledItems[newEnabledIdx];
72
+ if (targetItem) {
73
+ focusAndSelect(targetItem.index);
74
+ }
75
+ },
76
+ [orientation, getCurrentEnabledIndex, enabledItems, focusAndSelect]
77
+ );
78
+
79
+ return (
80
+ <div
81
+ role="tablist"
82
+ aria-orientation={orientation}
83
+ className={cn("inline-flex gap-1", orientation === "vertical" ? "flex-col" : "flex-row items-center", className)}
84
+ onKeyDown={handleKeyDown}
85
+ >
86
+ {items.map((item, index) => (
87
+ <TabsTrigger
88
+ key={item.value}
89
+ item={item}
90
+ index={index}
91
+ ref={(el) => {
92
+ triggerRefs.current[index] = el;
93
+ }}
94
+ />
95
+ ))}
96
+ </div>
97
+ );
98
+ };
@@ -0,0 +1,26 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import { useTabsContext } from "./TabsContext";
3
+
4
+ export type TabsPanelsProps = {
5
+ /** Additional CSS classes */
6
+ className?: string;
7
+ };
8
+
9
+ export const TabsPanels = ({ className }: TabsPanelsProps) => {
10
+ const { items, value, baseId } = useTabsContext();
11
+
12
+ const activeItem = items.find((item) => item.value === value);
13
+ if (!activeItem) return null;
14
+
15
+ const panelId = `${baseId}-panel-${value}`;
16
+ const triggerId = `${baseId}-trigger-${value}`;
17
+
18
+ // Support lazy content
19
+ const content = typeof activeItem.content === "function" ? activeItem.content() : activeItem.content;
20
+
21
+ return (
22
+ <div id={panelId} role="tabpanel" aria-labelledby={triggerId} className={cn("focus-visible:outline-hidden", className)}>
23
+ {content}
24
+ </div>
25
+ );
26
+ };
@@ -0,0 +1,69 @@
1
+ import { Badge } from "@sixthshift/design-system/badge";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
5
+ import { type TabItem, useTabsContext } from "./TabsContext";
6
+
7
+ const tabsTriggerVariants = cva(
8
+ `inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md
9
+ px-3 py-1.5 text-sm font-medium cursor-pointer transition-colors focus-visible:outline-hidden
10
+ focus-visible:ring-2 focus-visible:ring-focus-ring
11
+ focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50`,
12
+ {
13
+ variants: {
14
+ selected: {
15
+ true: "bg-bg-brand-pressed text-fg-on-brand-pressed",
16
+ false: "text-fg-subtle hover:bg-bg-subtle-hovered hover:text-fg-normal",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ selected: false,
21
+ },
22
+ }
23
+ );
24
+
25
+ export type TabsTriggerProps = {
26
+ item: TabItem;
27
+ index: number;
28
+ };
29
+
30
+ export const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(({ item, index }, ref) => {
31
+ const { value, onValueChange, baseId } = useTabsContext();
32
+ const isSelected = value === item.value;
33
+ const triggerId = `${baseId}-trigger-${item.value}`;
34
+ const panelId = `${baseId}-panel-${item.value}`;
35
+
36
+ const handleClick = () => {
37
+ if (!item.disabled) {
38
+ onValueChange(item.value);
39
+ }
40
+ };
41
+
42
+ return (
43
+ <button
44
+ ref={ref}
45
+ id={triggerId}
46
+ role="tab"
47
+ type="button"
48
+ aria-selected={isSelected}
49
+ // Only the selected panel is mounted (content is lazy), so pointing an
50
+ // unselected tab at a panel id absent from the DOM would be a dangling
51
+ // reference. Omit it instead.
52
+ aria-controls={isSelected ? panelId : undefined}
53
+ tabIndex={isSelected ? 0 : -1}
54
+ disabled={item.disabled}
55
+ data-index={index}
56
+ className={cn(tabsTriggerVariants({ selected: isSelected }))}
57
+ onClick={handleClick}
58
+ >
59
+ {item.label}
60
+ {item.badge !== undefined && (
61
+ <Badge variant="soft" intent="neutral" className="ml-1 min-w-[1.25rem] px-1.5 py-0">
62
+ {item.badge}
63
+ </Badge>
64
+ )}
65
+ </button>
66
+ );
67
+ });
68
+
69
+ TabsTrigger.displayName = "TabsTrigger";
@@ -0,0 +1,3 @@
1
+ export { TabsList, type TabsListProps } from "./components/TabsList";
2
+ export { TabsPanels, type TabsPanelsProps } from "./components/TabsPanels";
3
+ export { type TabItem, Tabs, type TabsProps } from "./Tabs";
@@ -0,0 +1,46 @@
1
+ import { Badge } from "@sixthshift/design-system/badge";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import { X } from "lucide-react";
4
+
5
+ /**
6
+ * One tag, rendered prettily. A `namespace:value` tag (`project:website`)
7
+ * shows the namespace muted; a plain tag (`urgent`) renders as-is.
8
+ *
9
+ * Pure presentation. Entity-reference tags (`person:per_…`) are NOT rendered
10
+ * here — the web `TagRef` intercepts those and resolves them to a name chip
11
+ * (identity-rendering.md); TagChip never shows a raw id.
12
+ *
13
+ * Two disjoint modes: navigable (no `onRemove`; the caller wraps it in a link)
14
+ * or removable (`onRemove` renders an × — used by `TagInput` and edit surfaces).
15
+ */
16
+ export type TagChipProps = {
17
+ tag: string;
18
+ /** When set, renders an × affordance. Omit for read-only / navigable chips. */
19
+ onRemove?: () => void;
20
+ /** `sm` (default) for rows; `default` for detail pages. */
21
+ size?: "sm" | "default";
22
+ className?: string;
23
+ };
24
+
25
+ export const TagChip = ({ tag, onRemove, size = "sm", className }: TagChipProps) => {
26
+ const sep = tag.indexOf(":");
27
+ const namespace = sep > 0 ? tag.slice(0, sep) : null;
28
+ const value = sep > 0 ? tag.slice(sep + 1) : tag;
29
+
30
+ return (
31
+ <Badge variant="outline" intent="muted" className={cn("gap-1 font-normal", size === "sm" ? "px-1.5 py-0 text-[10px]" : "px-2 py-0.5 text-xs", className)}>
32
+ {namespace && <span className="text-fg-subtle">{namespace}:</span>}
33
+ <span>{value}</span>
34
+ {onRemove && (
35
+ <button
36
+ type="button"
37
+ onClick={onRemove}
38
+ aria-label={`Remove ${tag}`}
39
+ className="-mr-0.5 ml-0.5 rounded-sm text-fg-subtle hover:text-fg-normal focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
40
+ >
41
+ <X className="h-3 w-3" />
42
+ </button>
43
+ )}
44
+ </Badge>
45
+ );
46
+ };
@@ -0,0 +1 @@
1
+ export { TagChip, type TagChipProps } from "./TagChip";
@@ -0,0 +1,60 @@
1
+ import { TagChip } from "@sixthshift/design-system/tag-chip";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import { type KeyboardEvent, useState } from "react";
4
+
5
+ /**
6
+ * Token input for tags: existing tags render as removable chips, typing plus
7
+ * Enter or comma commits a new one, Backspace on an empty field removes the
8
+ * last. Duplicates are ignored. Controlled — the parent owns `value`.
9
+ *
10
+ * The chrome matches `Input` (same border/focus tokens) so it sits naturally
11
+ * among other form fields.
12
+ */
13
+ export type TagInputProps = {
14
+ value: string[];
15
+ onChange: (tags: string[]) => void;
16
+ placeholder?: string;
17
+ id?: string;
18
+ className?: string;
19
+ };
20
+
21
+ export const TagInput = ({ value, onChange, placeholder = "Add a tag…", id, className }: TagInputProps) => {
22
+ const [draft, setDraft] = useState("");
23
+
24
+ const commit = () => {
25
+ const tag = draft.trim();
26
+ if (tag && !value.includes(tag)) onChange([...value, tag]);
27
+ setDraft("");
28
+ };
29
+
30
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
31
+ if (e.key === "Enter" || e.key === ",") {
32
+ e.preventDefault();
33
+ commit();
34
+ } else if (e.key === "Backspace" && draft === "" && value.length > 0) {
35
+ onChange(value.slice(0, -1));
36
+ }
37
+ };
38
+
39
+ return (
40
+ <div
41
+ className={cn(
42
+ "flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-border-normal bg-bg-normal px-2 py-1.5 text-sm shadow-xs transition-colors focus-within:ring-2 focus-within:ring-focus-ring",
43
+ className
44
+ )}
45
+ >
46
+ {value.map((tag) => (
47
+ <TagChip key={tag} tag={tag} onRemove={() => onChange(value.filter((t) => t !== tag))} />
48
+ ))}
49
+ <input
50
+ id={id}
51
+ value={draft}
52
+ onChange={(e) => setDraft(e.target.value)}
53
+ onKeyDown={handleKeyDown}
54
+ onBlur={commit}
55
+ placeholder={value.length > 0 ? "" : placeholder}
56
+ className="min-w-[8ch] flex-1 bg-transparent outline-none placeholder:text-fg-subtle"
57
+ />
58
+ </div>
59
+ );
60
+ };
@@ -0,0 +1 @@
1
+ export { TagInput, type TagInputProps } from "./TagInput";
@@ -0,0 +1,35 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import * as React from "react";
3
+
4
+ /*
5
+ * Text Preset Reference
6
+ * ---------------------
7
+ * When creating semantic text presets, consider these Tailwind classes:
8
+ *
9
+ * Size: text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-3xl, text-4xl
10
+ * Weight: font-normal, font-medium, font-semibold, font-bold
11
+ * Color: text-fg-normal, text-fg-subtle, text-fg-strong, text-fg-brand, text-fg-danger, text-fg-success, text-fg-warning
12
+ * Leading: leading-none, leading-tight, leading-snug, leading-normal, leading-relaxed, leading-loose
13
+ * Tracking: tracking-tighter, tracking-tight, tracking-normal, tracking-wide, tracking-wider, tracking-widest
14
+ * Align: text-left, text-center, text-right
15
+ * Transform: uppercase, lowercase, capitalize, normal-case
16
+ * Truncate: truncate
17
+ */
18
+
19
+ export type TextElement = "span" | "p" | "div" | "label" | "code" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
20
+
21
+ export type TextProps = React.HTMLAttributes<HTMLElement> & {
22
+ as?: TextElement;
23
+ };
24
+
25
+ const Text = React.forwardRef<HTMLElement, TextProps>(({ as: Comp = "span", className, ...props }, ref) => (
26
+ <Comp
27
+ // biome-ignore lint/suspicious/noExplicitAny: polymorphic ref requires any
28
+ ref={ref as any}
29
+ className={cn(className)}
30
+ {...props}
31
+ />
32
+ ));
33
+ Text.displayName = "Text";
34
+
35
+ export { Text };
@@ -0,0 +1 @@
1
+ export { Text, type TextElement, type TextProps } from "./Text";
@@ -0,0 +1,35 @@
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 textInlineVariants = cva("inline-flex items-center", {
6
+ variants: {
7
+ gap: {
8
+ none: "gap-0",
9
+ xs: "gap-0.5",
10
+ sm: "gap-1",
11
+ md: "gap-2",
12
+ lg: "gap-3",
13
+ xl: "gap-4",
14
+ },
15
+ align: {
16
+ start: "items-start",
17
+ center: "items-center",
18
+ end: "items-end",
19
+ baseline: "items-baseline",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ gap: "sm",
24
+ align: "baseline",
25
+ },
26
+ });
27
+
28
+ export type TextInlineProps = React.HTMLAttributes<HTMLSpanElement> & VariantProps<typeof textInlineVariants>;
29
+
30
+ const TextInline = React.forwardRef<HTMLSpanElement, TextInlineProps>(({ className, gap, align, ...props }, ref) => (
31
+ <span ref={ref} className={cn(textInlineVariants({ gap, align }), className)} {...props} />
32
+ ));
33
+ TextInline.displayName = "TextInline";
34
+
35
+ export { TextInline, textInlineVariants };
@@ -0,0 +1 @@
1
+ export { TextInline, type TextInlineProps, textInlineVariants } from "./TextInline";
@@ -0,0 +1,20 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import * as React from "react";
3
+
4
+ export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
5
+
6
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
7
+ return (
8
+ <textarea
9
+ className={cn(
10
+ "flex min-h-[60px] w-full rounded-md border border-border-normal bg-transparent px-3 py-2 text-sm shadow-xs placeholder:text-fg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
11
+ className
12
+ )}
13
+ ref={ref}
14
+ {...props}
15
+ />
16
+ );
17
+ });
18
+ Textarea.displayName = "Textarea";
19
+
20
+ export { Textarea };
@@ -0,0 +1 @@
1
+ export { Textarea, type TextareaProps } from "./Textarea";