@vvedantb/ui 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/package.json +124 -4
- package/src/ai-elements/artifact.tsx +99 -0
- package/src/ai-elements/attachments.tsx +366 -0
- package/src/ai-elements/chain-of-thought.tsx +212 -0
- package/src/ai-elements/code-block.tsx +478 -0
- package/src/ai-elements/conversation.tsx +157 -0
- package/src/ai-elements/image.tsx +25 -0
- package/src/ai-elements/inline-citation.tsx +252 -0
- package/src/ai-elements/message.tsx +269 -0
- package/src/ai-elements/model-selector.tsx +211 -0
- package/src/ai-elements/prompt-input.tsx +1018 -0
- package/src/ai-elements/queue.tsx +251 -0
- package/src/ai-elements/reasoning.tsx +178 -0
- package/src/ai-elements/shimmer.tsx +56 -0
- package/src/ai-elements/sources.tsx +77 -0
- package/src/ai-elements/speech-input.tsx +290 -0
- package/src/ai-elements/suggestion.tsx +191 -0
- package/src/css.d.ts +2 -0
- package/src/index.ts +96 -0
- package/src/motion/presets.ts +35 -0
- package/src/motion/presets.vibot.ts +105 -0
- package/src/ui/_menu-classes.ts +30 -0
- package/src/ui/_menu-classes.vibot.ts +33 -0
- package/src/ui/accordion.tsx +81 -0
- package/src/ui/accordion.verve.tsx +56 -0
- package/src/ui/badge.tsx +34 -0
- package/src/ui/breadcrumb.tsx +83 -0
- package/src/ui/button-group.tsx +84 -0
- package/src/ui/button.tsx +57 -0
- package/src/ui/card.tsx +27 -0
- package/src/ui/carousel.tsx +211 -0
- package/src/ui/checkbox.tsx +27 -0
- package/src/ui/clear-input.tsx +170 -0
- package/src/ui/clear-input.vibot.tsx +76 -0
- package/src/ui/clearInputDissolve.ts +214 -0
- package/src/ui/collapsible.tsx +7 -0
- package/src/ui/command.tsx +148 -0
- package/src/ui/context-menu.tsx +173 -0
- package/src/ui/dialog.tsx +149 -0
- package/src/ui/dropdown-menu.tsx +178 -0
- package/src/ui/hover-card.tsx +31 -0
- package/src/ui/input-group.tsx +162 -0
- package/src/ui/input-group.verve.tsx +150 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/kbd.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/labeled-switch-row.tsx +53 -0
- package/src/ui/modalTransition.ts +85 -0
- package/src/ui/pagination.tsx +47 -0
- package/src/ui/popover.tsx +33 -0
- package/src/ui/progress.tsx +25 -0
- package/src/ui/radix-select.tsx +202 -0
- package/src/ui/scroll-area.tsx +55 -0
- package/src/ui/scroll-area.verve.tsx +43 -0
- package/src/ui/search-input.tsx +29 -0
- package/src/ui/select.tsx +163 -0
- package/src/ui/separator.tsx +28 -0
- package/src/ui/sheet.tsx +87 -0
- package/src/ui/skeleton.tsx +18 -0
- package/src/ui/sonner.css +16 -0
- package/src/ui/sonner.tsx +47 -0
- package/src/ui/spinner.tsx +27 -0
- package/src/ui/surface-classes.ts +55 -0
- package/src/ui/switch.tsx +26 -0
- package/src/ui/table.tsx +123 -0
- package/src/ui/tabs.tsx +140 -0
- package/src/ui/tabsSliding.ts +56 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/time-picker.tsx +170 -0
- package/src/ui/tooltip.tsx +25 -0
- package/src/ui/visually-hidden.tsx +3 -0
- package/src/utils/cn.ts +16 -0
- package/src/utils/ref.ts +10 -0
- 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 };
|
package/src/ui/table.tsx
ADDED
|
@@ -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
|
+
};
|
package/src/ui/tabs.tsx
ADDED
|
@@ -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 };
|
package/src/utils/cn.ts
ADDED
|
@@ -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
|
+
}
|
package/src/utils/ref.ts
ADDED
|
@@ -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