@vvedantb/ui 0.0.0-stage → 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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +124 -4
  3. package/src/ai-elements/artifact.tsx +99 -0
  4. package/src/ai-elements/attachments.tsx +366 -0
  5. package/src/ai-elements/chain-of-thought.tsx +212 -0
  6. package/src/ai-elements/code-block.tsx +478 -0
  7. package/src/ai-elements/conversation.tsx +157 -0
  8. package/src/ai-elements/image.tsx +25 -0
  9. package/src/ai-elements/inline-citation.tsx +252 -0
  10. package/src/ai-elements/message.tsx +269 -0
  11. package/src/ai-elements/model-selector.tsx +211 -0
  12. package/src/ai-elements/prompt-input.tsx +1018 -0
  13. package/src/ai-elements/queue.tsx +251 -0
  14. package/src/ai-elements/reasoning.tsx +178 -0
  15. package/src/ai-elements/shimmer.tsx +56 -0
  16. package/src/ai-elements/sources.tsx +77 -0
  17. package/src/ai-elements/speech-input.tsx +290 -0
  18. package/src/ai-elements/suggestion.tsx +191 -0
  19. package/src/css.d.ts +2 -0
  20. package/src/index.ts +96 -0
  21. package/src/motion/presets.ts +35 -0
  22. package/src/motion/presets.vibot.ts +105 -0
  23. package/src/ui/_menu-classes.ts +30 -0
  24. package/src/ui/_menu-classes.vibot.ts +33 -0
  25. package/src/ui/accordion.tsx +81 -0
  26. package/src/ui/accordion.verve.tsx +56 -0
  27. package/src/ui/badge.tsx +34 -0
  28. package/src/ui/breadcrumb.tsx +83 -0
  29. package/src/ui/button-group.tsx +84 -0
  30. package/src/ui/button.tsx +57 -0
  31. package/src/ui/card.tsx +27 -0
  32. package/src/ui/carousel.tsx +211 -0
  33. package/src/ui/checkbox.tsx +27 -0
  34. package/src/ui/clear-input.tsx +170 -0
  35. package/src/ui/clear-input.vibot.tsx +76 -0
  36. package/src/ui/clearInputDissolve.ts +214 -0
  37. package/src/ui/collapsible.tsx +7 -0
  38. package/src/ui/command.tsx +148 -0
  39. package/src/ui/context-menu.tsx +173 -0
  40. package/src/ui/dialog.tsx +149 -0
  41. package/src/ui/dropdown-menu.tsx +178 -0
  42. package/src/ui/hover-card.tsx +31 -0
  43. package/src/ui/input-group.tsx +162 -0
  44. package/src/ui/input-group.verve.tsx +150 -0
  45. package/src/ui/input.tsx +21 -0
  46. package/src/ui/kbd.tsx +18 -0
  47. package/src/ui/label.tsx +20 -0
  48. package/src/ui/labeled-switch-row.tsx +53 -0
  49. package/src/ui/modalTransition.ts +85 -0
  50. package/src/ui/pagination.tsx +47 -0
  51. package/src/ui/popover.tsx +33 -0
  52. package/src/ui/progress.tsx +25 -0
  53. package/src/ui/radix-select.tsx +202 -0
  54. package/src/ui/scroll-area.tsx +55 -0
  55. package/src/ui/scroll-area.verve.tsx +43 -0
  56. package/src/ui/search-input.tsx +29 -0
  57. package/src/ui/select.tsx +163 -0
  58. package/src/ui/separator.tsx +28 -0
  59. package/src/ui/sheet.tsx +87 -0
  60. package/src/ui/skeleton.tsx +18 -0
  61. package/src/ui/sonner.css +16 -0
  62. package/src/ui/sonner.tsx +47 -0
  63. package/src/ui/spinner.tsx +27 -0
  64. package/src/ui/surface-classes.ts +55 -0
  65. package/src/ui/switch.tsx +26 -0
  66. package/src/ui/table.tsx +123 -0
  67. package/src/ui/tabs.tsx +140 -0
  68. package/src/ui/tabsSliding.ts +56 -0
  69. package/src/ui/textarea.tsx +21 -0
  70. package/src/ui/time-picker.tsx +170 -0
  71. package/src/ui/tooltip.tsx +25 -0
  72. package/src/ui/visually-hidden.tsx +3 -0
  73. package/src/utils/cn.ts +16 -0
  74. package/src/utils/ref.ts +10 -0
  75. package/README.md +0 -3
@@ -0,0 +1,27 @@
1
+ import { IconLoader2 } from "@tabler/icons-react";
2
+ import { cn } from "../utils/cn";
3
+
4
+ const sizeClasses = {
5
+ sm: "size-4",
6
+ md: "size-6",
7
+ lg: "size-8",
8
+ };
9
+
10
+ function Spinner({
11
+ size = "md",
12
+ className,
13
+ ...props
14
+ }: Omit<React.ComponentPropsWithoutRef<"svg">, "size" | "stroke"> & {
15
+ size?: "sm" | "md" | "lg";
16
+ }) {
17
+ return (
18
+ <IconLoader2
19
+ role="status"
20
+ aria-label="Loading"
21
+ className={cn("animate-spin text-muted", sizeClasses[size], className)}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ export { Spinner };
@@ -0,0 +1,55 @@
1
+ export const overlaySurfaceClass =
2
+ "bg-overlay text-overlay-foreground smooth-shadow-ring-xl";
3
+
4
+ export const modalBackdropClass = "bg-backdrop backdrop-blur-md";
5
+
6
+ export const staticSurfaceClass =
7
+ "bg-surface-secondary text-surface-foreground border-0 shadow-none backdrop-blur-none";
8
+
9
+ export const flatElevatedSurfaceClass =
10
+ "bg-surface text-surface-foreground border-0 shadow-none backdrop-blur-none";
11
+
12
+ export const flatElevatedSurfaceHoverClass =
13
+ "transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary";
14
+
15
+ export const flatElevatedSurfaceFocusClass = "focus-within:outline-hidden";
16
+
17
+ export const chatInputContainerClass =
18
+ "bg-surface-secondary text-foreground shadow-none backdrop-blur-none";
19
+
20
+ export const flatPanelSubtleClass =
21
+ "bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none";
22
+
23
+ export const flatPanelSubtleHoverClass =
24
+ "transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary";
25
+
26
+ export const flatInteractiveClass =
27
+ "border-0 bg-transparent shadow-none backdrop-blur-none transition-[background-color,transform] duration-200 ease-smooth active:duration-100 active:ease-out active:scale-[0.97] hover:bg-surface-secondary hover:text-foreground";
28
+
29
+ export const segmentActiveClass = "bg-segment text-segment-foreground";
30
+
31
+ export const segmentActiveStateClass =
32
+ "data-[state=active]:bg-segment data-[state=active]:text-segment-foreground";
33
+
34
+ export const segmentOpenStateClass =
35
+ "data-[state=open]:bg-segment data-[state=open]:text-segment-foreground";
36
+
37
+ export const segmentExpandedStateClass =
38
+ "aria-expanded:bg-segment aria-expanded:text-segment-foreground";
39
+
40
+ export const defaultActiveClass = "bg-default text-default-foreground";
41
+
42
+ export const defaultSelectedStateClass =
43
+ "data-[state=selected]:bg-default data-[state=selected]:text-default-foreground";
44
+
45
+ export const defaultActiveStateClass =
46
+ "data-[state=active]:bg-default data-[state=active]:text-default-foreground";
47
+
48
+ export const menuItemHighlightClass =
49
+ "focus:bg-surface-tertiary focus:text-foreground data-highlighted:bg-surface-tertiary data-highlighted:text-foreground";
50
+
51
+ export const menuItemEmphasisHighlightClass =
52
+ "focus:bg-default focus:text-default-foreground data-highlighted:bg-default data-highlighted:text-default-foreground data-[state=open]:bg-default data-[state=open]:text-default-foreground";
53
+
54
+ export const menuListSelectedClass =
55
+ "data-[selected=true]:bg-default data-[selected=true]:text-default-foreground";
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
3
+ import { cn } from "../utils/cn";
4
+
5
+ const Switch = React.forwardRef<
6
+ React.ComponentRef<typeof SwitchPrimitive.Root>,
7
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
8
+ >(({ className, ...props }, ref) => (
9
+ <SwitchPrimitive.Root
10
+ className={cn(
11
+ "peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-border bg-default transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=unchecked]:bg-default before:absolute before:inset-[-8px] before:content-['']",
12
+ className,
13
+ )}
14
+ {...props}
15
+ ref={ref}
16
+ >
17
+ <SwitchPrimitive.Thumb
18
+ className={cn(
19
+ "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-[transform,background-color] duration-200 ease-smooth data-[state=checked]:translate-x-4 data-[state=checked]:bg-accent-foreground data-[state=unchecked]:translate-x-0",
20
+ )}
21
+ />
22
+ </SwitchPrimitive.Root>
23
+ ));
24
+ Switch.displayName = SwitchPrimitive.Root.displayName;
25
+
26
+ export { Switch };
@@ -0,0 +1,123 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils/cn";
3
+
4
+ const Table = React.forwardRef<
5
+ HTMLTableElement,
6
+ React.HTMLAttributes<HTMLTableElement>
7
+ >(({ className, ...props }, ref) => (
8
+ <div className="relative w-full overflow-auto rounded-lg max-sm:min-w-0 max-sm:max-w-full">
9
+ <table
10
+ ref={ref}
11
+ className={cn("w-full caption-bottom text-sm", className)}
12
+ {...props}
13
+ />
14
+ </div>
15
+ ));
16
+ Table.displayName = "Table";
17
+
18
+ const TableHeader = React.forwardRef<
19
+ HTMLTableSectionElement,
20
+ React.HTMLAttributes<HTMLTableSectionElement>
21
+ >(({ className, ...props }, ref) => (
22
+ <thead
23
+ ref={ref}
24
+ className={cn("[&_tr]:border-b [&_tr]:border-separator", className)}
25
+ {...props}
26
+ />
27
+ ));
28
+ TableHeader.displayName = "TableHeader";
29
+
30
+ const TableBody = React.forwardRef<
31
+ HTMLTableSectionElement,
32
+ React.HTMLAttributes<HTMLTableSectionElement>
33
+ >(({ className, ...props }, ref) => (
34
+ <tbody
35
+ ref={ref}
36
+ className={cn("[&_tr:last-child]:border-0", className)}
37
+ {...props}
38
+ />
39
+ ));
40
+ TableBody.displayName = "TableBody";
41
+
42
+ const TableFooter = React.forwardRef<
43
+ HTMLTableSectionElement,
44
+ React.HTMLAttributes<HTMLTableSectionElement>
45
+ >(({ className, ...props }, ref) => (
46
+ <tfoot
47
+ ref={ref}
48
+ className={cn(
49
+ "border-t border-separator bg-surface-secondary/30 font-medium [&>tr]:last:border-b-0",
50
+ className,
51
+ )}
52
+ {...props}
53
+ />
54
+ ));
55
+ TableFooter.displayName = "TableFooter";
56
+
57
+ const TableRow = React.forwardRef<
58
+ HTMLTableRowElement,
59
+ React.HTMLAttributes<HTMLTableRowElement>
60
+ >(({ className, ...props }, ref) => (
61
+ <tr
62
+ ref={ref}
63
+ className={cn(
64
+ "border-b border-separator transition-colors duration-200 ease-smooth hover:bg-surface-tertiary/50 data-[state=selected]:bg-surface-secondary",
65
+ className,
66
+ )}
67
+ {...props}
68
+ />
69
+ ));
70
+ TableRow.displayName = "TableRow";
71
+
72
+ const TableHead = React.forwardRef<
73
+ HTMLTableCellElement,
74
+ React.ThHTMLAttributes<HTMLTableCellElement>
75
+ >(({ className, ...props }, ref) => (
76
+ <th
77
+ ref={ref}
78
+ className={cn(
79
+ "h-11 px-4 text-left align-middle text-xs font-semibold uppercase tracking-widest text-muted [&:has([role=checkbox])]:pr-0",
80
+ className,
81
+ )}
82
+ {...props}
83
+ />
84
+ ));
85
+ TableHead.displayName = "TableHead";
86
+
87
+ const TableCell = React.forwardRef<
88
+ HTMLTableCellElement,
89
+ React.TdHTMLAttributes<HTMLTableCellElement>
90
+ >(({ className, ...props }, ref) => (
91
+ <td
92
+ ref={ref}
93
+ className={cn(
94
+ "p-4 align-middle text-sm [&:has([role=checkbox])]:pr-0",
95
+ className,
96
+ )}
97
+ {...props}
98
+ />
99
+ ));
100
+ TableCell.displayName = "TableCell";
101
+
102
+ const TableCaption = React.forwardRef<
103
+ HTMLTableCaptionElement,
104
+ React.HTMLAttributes<HTMLTableCaptionElement>
105
+ >(({ className, ...props }, ref) => (
106
+ <caption
107
+ ref={ref}
108
+ className={cn("mt-4 text-sm text-muted", className)}
109
+ {...props}
110
+ />
111
+ ));
112
+ TableCaption.displayName = "TableCaption";
113
+
114
+ export {
115
+ Table,
116
+ TableHeader,
117
+ TableBody,
118
+ TableFooter,
119
+ TableHead,
120
+ TableRow,
121
+ TableCell,
122
+ TableCaption,
123
+ };
@@ -0,0 +1,140 @@
1
+ import * as React from "react";
2
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
3
+ import { cn } from "../utils/cn";
4
+ import { assignRef } from "../utils/ref";
5
+ import { syncTabsPill } from "./tabsSliding";
6
+
7
+ const Tabs = TabsPrimitive.Root;
8
+
9
+ // `pills` (default): isolated rounded pills with a gap, no shared track.
10
+ // `segmented`: connected bar with a sliding pill, kept for marketing surfaces.
11
+ type TabsListVariant = "pills" | "segmented";
12
+
13
+ const TabsList = React.forwardRef<
14
+ React.ComponentRef<typeof TabsPrimitive.List>,
15
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
16
+ variant?: TabsListVariant;
17
+ }
18
+ >(({ className, children, variant = "pills", ...props }, ref) => {
19
+ const listRef = React.useRef<React.ComponentRef<
20
+ typeof TabsPrimitive.List
21
+ > | null>(null);
22
+ const pillRef = React.useRef<HTMLSpanElement>(null);
23
+ const pillReadyRef = React.useRef(false);
24
+
25
+ const mergedRef = React.useCallback(
26
+ (node: React.ComponentRef<typeof TabsPrimitive.List> | null) => {
27
+ listRef.current = node;
28
+ assignRef(ref, node);
29
+ },
30
+ [ref],
31
+ );
32
+
33
+ React.useLayoutEffect(() => {
34
+ const list = listRef.current;
35
+ const pill = pillRef.current;
36
+ if (!list || !pill) {
37
+ return;
38
+ }
39
+
40
+ const snap = () => {
41
+ syncTabsPill(list, pill, false);
42
+ };
43
+
44
+ const animate = () => {
45
+ syncTabsPill(list, pill, true);
46
+ };
47
+
48
+ snap();
49
+
50
+ requestAnimationFrame(() => {
51
+ pillReadyRef.current = true;
52
+ });
53
+
54
+ const onWindowResize = () => {
55
+ snap();
56
+ };
57
+ window.addEventListener("resize", onWindowResize);
58
+
59
+ const resizeObserver = new ResizeObserver(() => {
60
+ snap();
61
+ });
62
+ resizeObserver.observe(list);
63
+
64
+ const mutationObserver = new MutationObserver(() => {
65
+ if (pillReadyRef.current) {
66
+ animate();
67
+ } else {
68
+ snap();
69
+ }
70
+ });
71
+ mutationObserver.observe(list, {
72
+ subtree: true,
73
+ attributes: true,
74
+ attributeFilter: ["data-state", "aria-selected"],
75
+ });
76
+
77
+ return () => {
78
+ window.removeEventListener("resize", onWindowResize);
79
+ resizeObserver.disconnect();
80
+ mutationObserver.disconnect();
81
+ };
82
+ }, [variant]);
83
+
84
+ return (
85
+ <TabsPrimitive.List
86
+ ref={mergedRef}
87
+ className={cn(
88
+ "t-tabs max-sm:max-w-full max-sm:justify-center-safe max-sm:overflow-x-auto max-sm:scrollbar-none",
89
+ variant === "segmented" ? "t-tabs--segmented" : "t-tabs--pills",
90
+ className,
91
+ )}
92
+ data-variant={variant}
93
+ {...props}
94
+ >
95
+ {variant === "segmented" ? (
96
+ <span ref={pillRef} className="t-tabs-pill" aria-hidden="true" />
97
+ ) : null}
98
+ {children}
99
+ </TabsPrimitive.List>
100
+ );
101
+ });
102
+ TabsList.displayName = TabsPrimitive.List.displayName;
103
+
104
+ const TabsTrigger = React.forwardRef<
105
+ React.ComponentRef<typeof TabsPrimitive.Trigger>,
106
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
107
+ >(({ className, ...props }, ref) => (
108
+ <TabsPrimitive.Trigger
109
+ ref={ref}
110
+ className={cn(
111
+ "t-tab inline-flex items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
112
+ className,
113
+ )}
114
+ {...props}
115
+ />
116
+ ));
117
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
118
+
119
+ const TabsContent = React.forwardRef<
120
+ React.ComponentRef<typeof TabsPrimitive.Content>,
121
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
122
+ >(({ className, ...props }, ref) => (
123
+ <TabsPrimitive.Content
124
+ ref={ref}
125
+ className={cn(
126
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2",
127
+ className,
128
+ )}
129
+ {...props}
130
+ />
131
+ ));
132
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
133
+
134
+ // raw radix primitives, use these when you need a tabs layout that the
135
+ // styled `tabs`/`tabslist`/`tabstrigger`/`tabscontent` defaults don't fit
136
+ // (e.g a vertical sidebar of tabs inside a popover) the styled exports
137
+ // above are tuned for horizontal pill, style tab bars
138
+ export { TabsPrimitive };
139
+
140
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,56 @@
1
+ // transitions.dev tabs sliding, see 16, tabs, sliding.md
2
+
3
+ function findActiveTab(list: HTMLElement): HTMLElement | null {
4
+ const tabs = list.querySelectorAll<HTMLElement>(".t-tab");
5
+ for (const tab of tabs) {
6
+ if (
7
+ tab.getAttribute("aria-selected") === "true" ||
8
+ tab.getAttribute("data-state") === "active"
9
+ ) {
10
+ return tab;
11
+ }
12
+ }
13
+ if (tabs.length === 0) {
14
+ return null;
15
+ }
16
+ return tabs[0] ?? null;
17
+ }
18
+
19
+ function moveTabsPill(
20
+ pill: HTMLElement,
21
+ tab: HTMLElement,
22
+ animate: boolean,
23
+ ): void {
24
+ const left = tab.offsetLeft;
25
+ const width = tab.offsetWidth;
26
+ const top = tab.offsetTop;
27
+ const height = tab.offsetHeight;
28
+
29
+ if (!animate) {
30
+ const previousTransition = pill.style.transition;
31
+ pill.style.transition = "none";
32
+ pill.style.transform = `translateX(${left}px)`;
33
+ pill.style.width = `${width}px`;
34
+ pill.style.top = `${top}px`;
35
+ pill.style.height = `${height}px`;
36
+ void pill.offsetWidth;
37
+ pill.style.transition = previousTransition;
38
+ return;
39
+ }
40
+
41
+ pill.style.transform = `translateX(${left}px)`;
42
+ pill.style.width = `${width}px`;
43
+ pill.style.top = `${top}px`;
44
+ pill.style.height = `${height}px`;
45
+ }
46
+
47
+ export function syncTabsPill(
48
+ list: HTMLElement,
49
+ pill: HTMLElement,
50
+ animate: boolean,
51
+ ): void {
52
+ const tab = findActiveTab(list);
53
+ if (tab) {
54
+ moveTabsPill(pill, tab, animate);
55
+ }
56
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils/cn";
3
+
4
+ const Textarea = React.forwardRef<
5
+ HTMLTextAreaElement,
6
+ React.ComponentProps<"textarea">
7
+ >(({ className, ...props }, ref) => {
8
+ return (
9
+ <textarea
10
+ className={cn(
11
+ "flex min-h-20 w-full max-sm:min-w-0 rounded-none border border-border bg-field-background px-3.5 py-2.5 text-base sm:text-sm placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
12
+ className,
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ );
18
+ });
19
+ Textarea.displayName = "Textarea";
20
+
21
+ export { Textarea };
@@ -0,0 +1,170 @@
1
+ import * as React from "react";
2
+ import { IconClock } from "@tabler/icons-react";
3
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
4
+ import { cn } from "../utils/cn";
5
+
6
+ // custom 24, hour "hh:mm" time picker replaces `<input type="time">` because the native picker varies wildly
7
+ // across browsers and platforms (chrome/firefox/safari each render a
8
+ // different control, and the spinner buttons feel laggy on windows) ux:, the trigger is styled like an input, same height/border/radius, so it slots into forms without bespoke alignment work, clicking the trigger (or its clock icon) opens a popover with two scrollable columns: hours 00, 23 on the left, minutes 00, 59 on the right click an item to select, the popover stays open so users can set both halves in one trip, selected items use the same `bg, surface, tertiary` treatment used elsewhere for active states, no rings or borders, matching the design system the component is fully controlled, `value` is always "hh:mm" 24h and
9
+ // the consumer is responsible for any local↔utc conversion that keeps
10
+ // the picker free of timezone logic and matches how the rest of the
11
+ // codebase (e.g dream mode) shapes scheduling values
12
+ interface TimePickerProps {
13
+ value: string;
14
+ onChange: (value: string) => void;
15
+ className?: string;
16
+ disabled?: boolean;
17
+ ariaLabel?: string;
18
+ // width of the trigger button defaults to a compact 110px
19
+ triggerClassName?: string;
20
+ }
21
+
22
+ const HOURS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"));
23
+ const MINUTES = Array.from({ length: 60 }, (_, i) =>
24
+ String(i).padStart(2, "0"),
25
+ );
26
+
27
+ function parseValue(value: string): { hour: string; minute: string } {
28
+ const match = /^(\d{2}):(\d{2})$/.exec(value);
29
+ if (match) {
30
+ const hour = match[1];
31
+ const minute = match[2];
32
+ if (hour !== undefined && minute !== undefined) {
33
+ return { hour, minute };
34
+ }
35
+ }
36
+ return { hour: "00", minute: "00" };
37
+ }
38
+
39
+ // auto, scroll a column to its selected item when the popover opens using
40
+ // `scrollintoview({ block: "centre" })` is good enough, the popover only
41
+ // has 2 columns and a fixed height, so we don't need a custom scroll calc
42
+ function useScrollSelectedIntoView(
43
+ ref: React.RefObject<HTMLDivElement | null>,
44
+ selector: string,
45
+ open: boolean,
46
+ ): void {
47
+ React.useEffect(() => {
48
+ if (!open) return;
49
+ const root = ref.current;
50
+ if (!root) return;
51
+ const target = root.querySelector(selector);
52
+ if (target instanceof HTMLElement) {
53
+ target.scrollIntoView({ block: "center", behavior: "auto" });
54
+ }
55
+ }, [open, ref, selector]);
56
+ }
57
+
58
+ const TimePicker = React.forwardRef<HTMLButtonElement, TimePickerProps>(
59
+ (
60
+ { value, onChange, className, disabled, ariaLabel, triggerClassName },
61
+ ref,
62
+ ) => {
63
+ const [open, setOpen] = React.useState(false);
64
+ const { hour, minute } = parseValue(value);
65
+ const hoursRef = React.useRef<HTMLDivElement | null>(null);
66
+ const minutesRef = React.useRef<HTMLDivElement | null>(null);
67
+
68
+ useScrollSelectedIntoView(hoursRef, `[data-time-value="${hour}"]`, open);
69
+ useScrollSelectedIntoView(
70
+ minutesRef,
71
+ `[data-time-value="${minute}"]`,
72
+ open,
73
+ );
74
+
75
+ const handleHourSelect = (h: string) => {
76
+ onChange(`${h}:${minute}`);
77
+ };
78
+ const handleMinuteSelect = (m: string) => {
79
+ onChange(`${hour}:${m}`);
80
+ };
81
+
82
+ return (
83
+ <Popover open={open} onOpenChange={setOpen}>
84
+ <PopoverTrigger asChild>
85
+ <button
86
+ ref={ref}
87
+ type="button"
88
+ disabled={disabled}
89
+ aria-label={ariaLabel}
90
+ className={cn(
91
+ "inline-flex h-8 items-center justify-between gap-2 rounded-field border border-border bg-field-background px-2.5 text-xs font-medium tabular-nums transition-[border-color,box-shadow,background-color] duration-200 ease-smooth hover:bg-field-background/90 focus-visible:border-focus-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
92
+ triggerClassName ?? "w-[110px]",
93
+ className,
94
+ )}
95
+ >
96
+ <span>{`${hour}:${minute}`}</span>
97
+ <IconClock className="h-3.5 w-3.5 text-muted" />
98
+ </button>
99
+ </PopoverTrigger>
100
+ <PopoverContent
101
+ align="end"
102
+ sideOffset={4}
103
+ className="flex w-auto gap-1 p-2"
104
+ >
105
+ <TimeColumn
106
+ ref={hoursRef}
107
+ label="Hours"
108
+ values={HOURS}
109
+ selected={hour}
110
+ onSelect={handleHourSelect}
111
+ />
112
+ <TimeColumn
113
+ ref={minutesRef}
114
+ label="Minutes"
115
+ values={MINUTES}
116
+ selected={minute}
117
+ onSelect={handleMinuteSelect}
118
+ />
119
+ </PopoverContent>
120
+ </Popover>
121
+ );
122
+ },
123
+ );
124
+ TimePicker.displayName = "TimePicker";
125
+
126
+ interface TimeColumnProps {
127
+ label: string;
128
+ values: string[];
129
+ selected: string;
130
+ onSelect: (value: string) => void;
131
+ }
132
+
133
+ const TimeColumn = React.forwardRef<HTMLDivElement, TimeColumnProps>(
134
+ ({ label, values, selected, onSelect }, ref) => {
135
+ return (
136
+ <div className="flex w-14 flex-col">
137
+ <div className="px-2 pb-1 text-[10px] uppercase tracking-wider text-muted">
138
+ {label}
139
+ </div>
140
+ <div
141
+ ref={ref}
142
+ className="h-48 overflow-y-auto rounded-lg bg-segment py-1"
143
+ >
144
+ {values.map((v) => {
145
+ const isSelected = v === selected;
146
+ return (
147
+ <button
148
+ key={v}
149
+ type="button"
150
+ data-time-value={v}
151
+ onClick={() => onSelect(v)}
152
+ className={cn(
153
+ "flex h-7 w-full items-center justify-center text-xs tabular-nums transition-colors",
154
+ isSelected
155
+ ? "bg-accent text-accent-foreground"
156
+ : "text-foreground/80 hover:bg-surface-tertiary/50",
157
+ )}
158
+ >
159
+ {v}
160
+ </button>
161
+ );
162
+ })}
163
+ </div>
164
+ </div>
165
+ );
166
+ },
167
+ );
168
+ TimeColumn.displayName = "TimeColumn";
169
+
170
+ export { TimePicker };
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3
+ import { cn } from "../utils/cn";
4
+
5
+ const TooltipProvider = TooltipPrimitive.Provider;
6
+ const Tooltip = TooltipPrimitive.Root;
7
+ const TooltipTrigger = TooltipPrimitive.Trigger;
8
+
9
+ const TooltipContent = React.forwardRef<
10
+ React.ComponentRef<typeof TooltipPrimitive.Content>,
11
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
12
+ >(({ className, sideOffset = 4, ...props }, ref) => (
13
+ <TooltipPrimitive.Content
14
+ ref={ref}
15
+ sideOffset={sideOffset}
16
+ className={cn(
17
+ "z-50 max-w-[calc(100vw-2rem)] overflow-hidden rounded-lg bg-overlay px-2.5 py-1.5 text-xs text-overlay-foreground smooth-shadow-ring-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ ));
23
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
24
+
25
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -0,0 +1,3 @@
1
+ "use client";
2
+
3
+ export { Root as VisuallyHidden } from "@radix-ui/react-visually-hidden";
@@ -0,0 +1,16 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+
4
+ // `rounded-field` is a custom radius token — teach twMerge it conflicts with
5
+ // `rounded-none` / `rounded-md` / etc. so Input overrides actually win.
6
+ const twMerge = extendTailwindMerge({
7
+ extend: {
8
+ theme: {
9
+ borderRadius: ["field"],
10
+ },
11
+ },
12
+ });
13
+
14
+ export function cn(...inputs: ClassValue[]) {
15
+ return twMerge(clsx(inputs));
16
+ }
@@ -0,0 +1,10 @@
1
+ import type { Ref } from "react";
2
+
3
+ // forward a node to a callback or object ref (the two shapes forwardRef hands us)
4
+ export function assignRef<T>(ref: Ref<T> | undefined, node: T): void {
5
+ if (typeof ref === "function") {
6
+ ref(node);
7
+ } else if (ref) {
8
+ ref.current = node;
9
+ }
10
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.