@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,29 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import { X } from "lucide-react";
3
+ import * as React from "react";
4
+ import { ModalContext } from "./ModalContext";
5
+
6
+ export type ModalHeaderProps = React.HTMLAttributes<HTMLDivElement>;
7
+
8
+ export const ModalHeader = React.forwardRef<HTMLDivElement, ModalHeaderProps>(({ className, children, ...props }, ref) => {
9
+ const context = React.useContext(ModalContext);
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 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
+ ModalHeader.displayName = "ModalHeader";
@@ -0,0 +1,9 @@
1
+ export { ModalBody, type ModalBodyProps } from "./ModalBody";
2
+ export {
3
+ ModalContext,
4
+ ModalContextProvider,
5
+ type OnClose,
6
+ useModalContext,
7
+ } from "./ModalContext";
8
+ export { ModalFooter, type ModalFooterProps } from "./ModalFooter";
9
+ export { ModalHeader, type ModalHeaderProps } from "./ModalHeader";
@@ -0,0 +1,13 @@
1
+ export {
2
+ ModalBody,
3
+ type ModalBodyProps,
4
+ ModalContext,
5
+ ModalContextProvider,
6
+ ModalFooter,
7
+ type ModalFooterProps,
8
+ ModalHeader,
9
+ type ModalHeaderProps,
10
+ type OnClose,
11
+ useModalContext,
12
+ } from "./components";
13
+ export { Modal, type ModalProps } from "./Modal";
@@ -0,0 +1,53 @@
1
+ import { useComponents } from "@sixthshift/design-system/components";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import type { NavItem as NavItemType, NavSection, RenderLinkFn } from "../NavSide";
4
+
5
+ export type NavBottomProps = {
6
+ /** Navigation sections (will be flattened) */
7
+ sections: NavSection[];
8
+ /** Callback to determine if an item is active */
9
+ isActive: (item: NavItemType) => boolean;
10
+ /** Render function for links (allows router integration). Falls back to ComponentsContext Link. */
11
+ renderLink?: RenderLinkFn;
12
+ /** Maximum number of items to display (default: 5) */
13
+ maxItems?: number;
14
+ /** Additional class names */
15
+ className?: string;
16
+ };
17
+
18
+ export const NavBottom = ({ sections, isActive, renderLink, maxItems = 5, className }: NavBottomProps) => {
19
+ const { Link } = useComponents();
20
+ const linkRenderer: RenderLinkFn = renderLink ?? (({ to, ...rest }) => <Link href={to} {...rest} />);
21
+ // Flatten all sections and limit to maxItems
22
+ const items = sections.flatMap((section) => section.items).slice(0, maxItems);
23
+
24
+ return (
25
+ <nav className={cn("fixed inset-x-0 bottom-0 z-app-bar bg-bg-brand-strong text-fg-on-brand-strong", className)}>
26
+ <ul className="flex items-center justify-around">
27
+ {items.map((item) => {
28
+ const Icon = item.icon;
29
+ const active = isActive(item);
30
+
31
+ return (
32
+ <li key={item.to}>
33
+ {linkRenderer({
34
+ to: item.to,
35
+ className: cn(
36
+ "flex flex-col items-center gap-1 rounded-md px-3 py-2 text-xs",
37
+ "hover:bg-bg-brand-hovered hover:text-fg-on-brand-hovered",
38
+ active && "bg-bg-brand-pressed"
39
+ ),
40
+ children: (
41
+ <>
42
+ <Icon className="h-5 w-5" />
43
+ <span>{item.label}</span>
44
+ </>
45
+ ),
46
+ })}
47
+ </li>
48
+ );
49
+ })}
50
+ </ul>
51
+ </nav>
52
+ );
53
+ };
@@ -0,0 +1,3 @@
1
+ // Re-export types from NavSide for convenience
2
+ export type { IconComponent, NavItem, NavSection, RenderLinkFn } from "../NavSide";
3
+ export { NavBottom, type NavBottomProps } from "./NavBottom";
@@ -0,0 +1,39 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import type { NavItem as NavItemType, RenderLinkFn } from "./types";
3
+
4
+ export type NavItemProps = {
5
+ item: NavItemType;
6
+ active: boolean;
7
+ expanded: boolean;
8
+ renderLink: RenderLinkFn;
9
+ };
10
+
11
+ export const NavItem = ({ item, active, expanded, renderLink }: NavItemProps) => {
12
+ const Icon = item.icon;
13
+
14
+ return (
15
+ <>
16
+ {renderLink({
17
+ to: item.to,
18
+ className: cn(
19
+ "flex items-center gap-3 rounded-md px-2 py-2 font-medium text-sm",
20
+ "hover:bg-bg-brand-hovered hover:text-fg-on-brand-hovered",
21
+ active && "bg-bg-brand-pressed"
22
+ ),
23
+ // Only include title when collapsed (for tooltip)
24
+ ...(expanded ? {} : { title: item.label }),
25
+ children: (
26
+ <>
27
+ <Icon className="h-6 w-6 shrink-0" />
28
+ {expanded && <span className="flex-1">{item.label}</span>}
29
+ {expanded && item.badge !== undefined && item.badge > 0 && (
30
+ <span className="flex min-w-5 items-center justify-center rounded-full bg-bg-on-brand/20 px-1.5 py-0.5 font-medium text-fg-on-brand text-xs tabular-nums">
31
+ {item.badge > 9 ? "9+" : item.badge}
32
+ </span>
33
+ )}
34
+ </>
35
+ ),
36
+ })}
37
+ </>
38
+ );
39
+ };
@@ -0,0 +1,40 @@
1
+ import { useComponents } from "@sixthshift/design-system/components";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import { NavItem } from "./NavItem";
4
+ import type { NavItem as NavItemType, NavSection, RenderLinkFn } from "./types";
5
+
6
+ export type NavSideProps = {
7
+ /** Navigation sections */
8
+ sections: NavSection[];
9
+ /** Whether the nav is expanded (shows labels) or collapsed (icons only) */
10
+ expanded?: boolean;
11
+ /** Callback to determine if an item is active */
12
+ isActive: (item: NavItemType) => boolean;
13
+ /** Render function for links (allows router integration). Falls back to ComponentsContext Link. */
14
+ renderLink?: RenderLinkFn;
15
+ /** Additional class names */
16
+ className?: string;
17
+ };
18
+
19
+ export const NavSide = ({ sections, expanded = true, isActive, renderLink, className }: NavSideProps) => {
20
+ const { Link } = useComponents();
21
+ const linkRenderer: RenderLinkFn = renderLink ?? (({ to, ...rest }) => <Link href={to} {...rest} />);
22
+ return (
23
+ <aside className={cn("flex flex-col bg-bg-brand-strong text-fg-on-brand-strong", expanded ? "w-48" : "w-16", className)}>
24
+ <nav className="flex-1 overflow-y-auto py-3">
25
+ {sections.map((section, sectionIndex) => (
26
+ <div key={section.id}>
27
+ {sectionIndex > 0 && <div className="my-2 border-border-subtle border-t" />}
28
+ <ul className={cn("flex flex-col gap-0.5 px-3")}>
29
+ {section.items.map((item) => (
30
+ <li key={item.to}>
31
+ <NavItem item={item} active={isActive(item)} expanded={expanded} renderLink={linkRenderer} />
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </div>
36
+ ))}
37
+ </nav>
38
+ </aside>
39
+ );
40
+ };
@@ -0,0 +1,2 @@
1
+ export { NavSide, type NavSideProps } from "./NavSide";
2
+ export type { IconComponent, NavItem, NavSection, RenderLinkFn } from "./types";
@@ -0,0 +1,36 @@
1
+ import type { ComponentType, ReactNode, SVGProps } from "react";
2
+
3
+ /**
4
+ * Icon component type - compatible with lucide-react and similar icon libraries
5
+ */
6
+ export type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
7
+
8
+ /**
9
+ * A single navigation item
10
+ */
11
+ export type NavItem = {
12
+ /** Route path */
13
+ to: string;
14
+ /** Display label */
15
+ label: string;
16
+ /** Icon component (e.g., from lucide-react) */
17
+ icon: IconComponent;
18
+ /** Optional count badge (e.g. pending suggestions). Rendered when > 0. */
19
+ badge?: number;
20
+ };
21
+
22
+ /**
23
+ * A group of navigation items
24
+ */
25
+ export type NavSection = {
26
+ /** Unique identifier for the section */
27
+ id: string;
28
+ /** Navigation items in this section */
29
+ items: NavItem[];
30
+ };
31
+
32
+ /**
33
+ * Render function for navigation links.
34
+ * Allows router-agnostic link rendering.
35
+ */
36
+ export type RenderLinkFn = (props: { to: string; className: string; title?: string; children: ReactNode }) => ReactNode;
@@ -0,0 +1,86 @@
1
+ import { Button } from "@sixthshift/design-system/button";
2
+ import { Select, type SelectOption } from "@sixthshift/design-system/select";
3
+ import { cn } from "@sixthshift/design-system/utils";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
8
+
9
+ export type PaginationProps = React.HTMLAttributes<HTMLDivElement> & {
10
+ /** Current page index (0-based) */
11
+ page: number;
12
+ /** Number of items per page */
13
+ pageSize: number;
14
+ /** Total number of items */
15
+ total: number;
16
+ /** Called when page changes */
17
+ onPageChange: (page: number) => void;
18
+ /** Called when page size changes */
19
+ onPageSizeChange: (size: number) => void;
20
+ /** Available page size options */
21
+ pageSizeOptions?: number[];
22
+ /** Label for rows per page (default: "Rows per page") */
23
+ rowsLabel?: string;
24
+ };
25
+
26
+ const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(
27
+ (
28
+ { page, pageSize, total, onPageChange, onPageSizeChange, pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, rowsLabel = "Rows per page", className, ...props },
29
+ ref
30
+ ) => {
31
+ const start = total > 0 ? page * pageSize + 1 : 0;
32
+ const end = Math.min((page + 1) * pageSize, total);
33
+ const totalPages = Math.ceil(total / pageSize);
34
+
35
+ const canGoPrev = page > 0;
36
+ const canGoNext = page < totalPages - 1;
37
+
38
+ const selectOptions: SelectOption<string>[] = pageSizeOptions.map((size) => ({
39
+ value: String(size),
40
+ label: String(size),
41
+ }));
42
+
43
+ const handlePageSizeChange = (value: string) => {
44
+ onPageSizeChange(Number(value));
45
+ onPageChange(0);
46
+ };
47
+
48
+ return (
49
+ <div ref={ref} className={cn("flex items-center justify-between gap-4 px-4 py-2", className)} {...props}>
50
+ {/* Page size selector */}
51
+ <div className="flex items-center gap-2">
52
+ <span className="text-fg-subtle text-sm">{rowsLabel}</span>
53
+ <Select value={String(pageSize)} options={selectOptions} onValueChange={handlePageSizeChange} className="w-20" />
54
+ </div>
55
+
56
+ {/* Results info */}
57
+ <span className="text-fg-subtle text-sm">
58
+ {total > 0 ? (
59
+ <>
60
+ <span className="font-medium text-fg-normal">{start}</span>
61
+ {"-"}
62
+ <span className="font-medium text-fg-normal">{end}</span>
63
+ {" of "}
64
+ <span className="font-medium text-fg-normal">{total}</span>
65
+ </>
66
+ ) : (
67
+ "No results"
68
+ )}
69
+ </span>
70
+
71
+ {/* Navigation buttons */}
72
+ <div className="flex items-center gap-1">
73
+ <Button variant="ghost" size="icon" onClick={() => onPageChange(page - 1)} disabled={!canGoPrev} aria-label="Previous page">
74
+ <ChevronLeft />
75
+ </Button>
76
+ <Button variant="ghost" size="icon" onClick={() => onPageChange(page + 1)} disabled={!canGoNext} aria-label="Next page">
77
+ <ChevronRight />
78
+ </Button>
79
+ </div>
80
+ </div>
81
+ );
82
+ }
83
+ );
84
+ Pagination.displayName = "Pagination";
85
+
86
+ export { Pagination };
@@ -0,0 +1,2 @@
1
+ export type { PaginationProps } from "./Pagination";
2
+ export { Pagination } from "./Pagination";
@@ -0,0 +1,68 @@
1
+ import { autoUpdate, flip, offset, type Placement, shift, useClick, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
2
+ import { useControllableState } from "@sixthshift/design-system/hooks";
3
+ import type * as React from "react";
4
+ import { useId } from "react";
5
+ import { PopoverBody } from "./components/PopoverBody";
6
+ import { PopoverClose } from "./components/PopoverClose";
7
+ import { PopoverContext } from "./components/PopoverContext";
8
+ import { PopoverTrigger } from "./components/PopoverTrigger";
9
+
10
+ export type PopoverProps = {
11
+ /** Controlled open state */
12
+ open?: boolean;
13
+ /** Callback when open state changes */
14
+ onOpenChange?: (open: boolean) => void;
15
+ /** Default open state for uncontrolled mode */
16
+ defaultOpen?: boolean;
17
+ /** Placement of the popover relative to trigger */
18
+ placement?: Placement;
19
+ /** Offset from the trigger in pixels */
20
+ offsetPx?: number;
21
+ /** Children - compound components */
22
+ children: React.ReactNode;
23
+ };
24
+
25
+ const PopoverRoot = ({ open: controlledOpen, onOpenChange, defaultOpen = false, placement = "bottom", offsetPx = 8, children }: PopoverProps) => {
26
+ const [open, setOpen] = useControllableState({
27
+ value: controlledOpen,
28
+ defaultValue: defaultOpen,
29
+ onChange: onOpenChange,
30
+ });
31
+
32
+ const { refs, floatingStyles, context } = useFloating({
33
+ open,
34
+ onOpenChange: setOpen,
35
+ placement,
36
+ strategy: "fixed",
37
+ middleware: [offset(offsetPx), flip(), shift({ padding: 8 })],
38
+ whileElementsMounted: autoUpdate,
39
+ });
40
+
41
+ const click = useClick(context);
42
+ const dismiss = useDismiss(context);
43
+ const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
44
+
45
+ const id = useId();
46
+ const contentId = `popover-content-${id}`;
47
+
48
+ const contextValue = {
49
+ open,
50
+ onOpenChange: setOpen,
51
+ refs: {
52
+ setReference: refs.setReference,
53
+ setFloating: refs.setFloating,
54
+ },
55
+ floatingStyles,
56
+ getReferenceProps,
57
+ getFloatingProps,
58
+ contentId,
59
+ };
60
+
61
+ return <PopoverContext.Provider value={contextValue}>{children}</PopoverContext.Provider>;
62
+ };
63
+
64
+ export const Popover = Object.assign(PopoverRoot, {
65
+ Trigger: PopoverTrigger,
66
+ Body: PopoverBody,
67
+ Close: PopoverClose,
68
+ });
@@ -0,0 +1,32 @@
1
+ import { FloatingPortal } from "@floating-ui/react";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import * as React from "react";
4
+ import { usePopoverContext } from "./PopoverContext";
5
+
6
+ export type PopoverBodyProps = React.HTMLAttributes<HTMLDivElement>;
7
+
8
+ export const PopoverBody = React.forwardRef<HTMLDivElement, PopoverBodyProps>(({ className, children, ...props }, forwardedRef) => {
9
+ const { open, refs, floatingStyles, getFloatingProps, contentId } = usePopoverContext();
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
+ id={contentId}
22
+ role="dialog"
23
+ style={floatingStyles}
24
+ className={cn("z-popover rounded-lg border border-border-normal bg-bg-normal p-4 shadow-lg", className)}
25
+ {...getFloatingProps(props)}
26
+ >
27
+ {children}
28
+ </div>
29
+ </FloatingPortal>
30
+ );
31
+ });
32
+ PopoverBody.displayName = "PopoverBody";
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ import { Slot } from "../../../internal/Slot";
3
+ import { usePopoverContext } from "./PopoverContext";
4
+
5
+ export type PopoverCloseProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
6
+ /** Render as child element */
7
+ asChild?: boolean;
8
+ };
9
+
10
+ export const PopoverClose = React.forwardRef<HTMLButtonElement, PopoverCloseProps>(({ asChild = false, onClick, ...props }, ref) => {
11
+ const { onOpenChange } = usePopoverContext();
12
+ const Comp = asChild ? Slot : "button";
13
+
14
+ return (
15
+ <Comp
16
+ ref={ref}
17
+ type={asChild ? undefined : "button"}
18
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
19
+ onClick?.(e);
20
+ onOpenChange(false);
21
+ }}
22
+ {...props}
23
+ />
24
+ );
25
+ });
26
+ PopoverClose.displayName = "PopoverClose";
@@ -0,0 +1,24 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ export type PopoverContextValue = {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ refs: {
7
+ setReference: (node: HTMLElement | null) => void;
8
+ setFloating: (node: HTMLElement | null) => void;
9
+ };
10
+ floatingStyles: React.CSSProperties;
11
+ getReferenceProps: (props?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
12
+ getFloatingProps: (props?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
13
+ contentId: string;
14
+ };
15
+
16
+ export const PopoverContext = createContext<PopoverContextValue | null>(null);
17
+
18
+ export function usePopoverContext() {
19
+ const context = useContext(PopoverContext);
20
+ if (!context) {
21
+ throw new Error("Popover compound components must be used within <Popover>");
22
+ }
23
+ return context;
24
+ }
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ import { Slot } from "../../../internal/Slot";
3
+ import { usePopoverContext } from "./PopoverContext";
4
+
5
+ export type PopoverTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
6
+ /** Render as child element */
7
+ asChild?: boolean;
8
+ };
9
+
10
+ export const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(({ asChild = false, children, ...props }, forwardedRef) => {
11
+ const { refs, getReferenceProps, contentId, open } = usePopoverContext();
12
+ const Comp = asChild ? Slot : "button";
13
+
14
+ return (
15
+ <Comp
16
+ ref={(node: HTMLButtonElement | null) => {
17
+ refs.setReference(node);
18
+ if (typeof forwardedRef === "function") forwardedRef(node);
19
+ else if (forwardedRef) forwardedRef.current = node;
20
+ }}
21
+ type={asChild ? undefined : "button"}
22
+ aria-haspopup="dialog"
23
+ aria-expanded={open}
24
+ aria-controls={open ? contentId : undefined}
25
+ {...getReferenceProps(props)}
26
+ >
27
+ {children}
28
+ </Comp>
29
+ );
30
+ });
31
+ PopoverTrigger.displayName = "PopoverTrigger";
@@ -0,0 +1,10 @@
1
+ export { PopoverBody, type PopoverBodyProps } from "./components/PopoverBody";
2
+ export {
3
+ PopoverClose,
4
+ type PopoverCloseProps,
5
+ } from "./components/PopoverClose";
6
+ export {
7
+ PopoverTrigger,
8
+ type PopoverTriggerProps,
9
+ } from "./components/PopoverTrigger";
10
+ export { Popover, type PopoverProps } from "./Popover";
@@ -0,0 +1,42 @@
1
+ import { Caption } from "@sixthshift/design-system/caption";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+
4
+ export type ProgressBarProps = {
5
+ completed: number;
6
+ total: number;
7
+ showFraction?: boolean;
8
+ /** Accessible name for the progress bar. */
9
+ label?: string;
10
+ className?: string;
11
+ };
12
+
13
+ export const ProgressBar = ({ completed, total, showFraction = true, label = "Progress", className }: ProgressBarProps) => {
14
+ // A caller can hand us anything; the bar must not render an out-of-range
15
+ // width (a negative `width` is invalid CSS) or report impossible progress.
16
+ const isMeasurable = total > 0;
17
+ const clamped = isMeasurable ? Math.min(Math.max(completed, 0), total) : 0;
18
+ const percentage = isMeasurable ? (clamped / total) * 100 : 0;
19
+
20
+ return (
21
+ <div className={cn("flex items-center gap-3", className)}>
22
+ <div
23
+ role="progressbar"
24
+ aria-label={label}
25
+ aria-valuemin={0}
26
+ aria-valuemax={isMeasurable ? total : undefined}
27
+ // An indeterminate progressbar omits aria-valuenow; with no positive
28
+ // total there is no meaningful progress to report.
29
+ aria-valuenow={isMeasurable ? clamped : undefined}
30
+ aria-valuetext={isMeasurable ? `${clamped} of ${total}` : undefined}
31
+ className="h-2 flex-1 overflow-hidden rounded-full bg-bg-strong"
32
+ >
33
+ <div className="h-full rounded-full bg-fg-success transition-all" style={{ width: `${percentage}%` }} />
34
+ </div>
35
+ {showFraction && (
36
+ <Caption className="shrink-0">
37
+ {completed}/{total}
38
+ </Caption>
39
+ )}
40
+ </div>
41
+ );
42
+ };
@@ -0,0 +1 @@
1
+ export { ProgressBar, type ProgressBarProps } from "./ProgressBar";
@@ -0,0 +1,89 @@
1
+ import { Label } from "@sixthshift/design-system/label";
2
+ import { cn } from "@sixthshift/design-system/utils";
3
+ import * as React from "react";
4
+
5
+ export type RadioButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange" | "value"> & {
6
+ /** Whether the radio button is selected */
7
+ checked?: boolean;
8
+ /** Callback when selection changes */
9
+ onCheckedChange?: (checked: boolean) => void;
10
+ /** Input name for form submission (groups radio buttons together) */
11
+ name?: string;
12
+ /** Value submitted with form (defaults to "on") */
13
+ value?: string;
14
+ /** Label rendered next to the radio button. Wraps in a <Label> for a11y. */
15
+ label?: React.ReactNode;
16
+ /** Classes applied to the wrapping element when `label` is provided */
17
+ labelClassName?: string;
18
+ };
19
+
20
+ const RadioButtonIcon = () => (
21
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-2 w-2" aria-hidden="true">
22
+ <circle cx="12" cy="12" r="6" />
23
+ </svg>
24
+ );
25
+
26
+ const RadioButton = React.forwardRef<HTMLButtonElement, RadioButtonProps>(
27
+ ({ className, checked = false, onCheckedChange, disabled, name, value = "on", label, labelClassName, id: idProp, ...props }, ref) => {
28
+ const generatedId = React.useId();
29
+ const id = idProp ?? (label ? generatedId : undefined);
30
+
31
+ const button = (
32
+ <>
33
+ {/* biome-ignore lint/a11y/useSemanticElements: Custom radio button with button role="radio" is WAI-ARIA best practice for styled radio buttons */}
34
+ <button
35
+ type="button"
36
+ role="radio"
37
+ id={id}
38
+ aria-checked={checked}
39
+ data-state={checked ? "checked" : "unchecked"}
40
+ disabled={disabled}
41
+ ref={ref}
42
+ onClick={() => onCheckedChange?.(!checked)}
43
+ className={cn(
44
+ "peer h-4 w-4 shrink-0 cursor-pointer rounded-full border border-border-brand shadow focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
45
+ checked && "bg-bg-brand text-fg-on-brand",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ <span className="flex items-center justify-center text-current">{checked && <RadioButtonIcon />}</span>
51
+ </button>
52
+ {name && (
53
+ <input
54
+ type="radio"
55
+ aria-hidden
56
+ tabIndex={-1}
57
+ name={name}
58
+ value={value}
59
+ checked={checked}
60
+ disabled={disabled}
61
+ readOnly
62
+ style={{
63
+ position: "absolute",
64
+ pointerEvents: "none",
65
+ opacity: 0,
66
+ margin: 0,
67
+ width: 0,
68
+ height: 0,
69
+ }}
70
+ />
71
+ )}
72
+ </>
73
+ );
74
+
75
+ if (!label) return button;
76
+
77
+ return (
78
+ <div className={cn("flex items-center gap-2", labelClassName)}>
79
+ {button}
80
+ <Label htmlFor={id} className={cn(disabled && "cursor-not-allowed opacity-50")}>
81
+ {label}
82
+ </Label>
83
+ </div>
84
+ );
85
+ }
86
+ );
87
+ RadioButton.displayName = "RadioButton";
88
+
89
+ export { RadioButton };
@@ -0,0 +1 @@
1
+ export { RadioButton, type RadioButtonProps } from "./RadioButton";