@robomous/ui-core 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.
- package/LICENSE +202 -0
- package/README.md +38 -0
- package/components.json +13 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +47 -0
- package/dist/lib/button.d.ts +1 -0
- package/dist/lib/button.js +4 -0
- package/dist/lib/cn.d.ts +18 -0
- package/dist/lib/cn.js +21 -0
- package/dist/lib/menu.d.ts +1 -0
- package/dist/lib/menu.js +16 -0
- package/dist/lib/progress.d.ts +3 -0
- package/dist/lib/progress.js +6 -0
- package/dist/lib/select.d.ts +1 -0
- package/dist/lib/select.js +3 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.js +27 -0
- package/dist/primitives/badge.d.ts +9 -0
- package/dist/primitives/badge.js +28 -0
- package/dist/primitives/button.d.ts +10 -0
- package/dist/primitives/button.js +35 -0
- package/dist/primitives/card.d.ts +11 -0
- package/dist/primitives/card.js +24 -0
- package/dist/primitives/combobox.d.ts +24 -0
- package/dist/primitives/combobox.js +58 -0
- package/dist/primitives/dialog.d.ts +17 -0
- package/dist/primitives/dialog.js +36 -0
- package/dist/primitives/dropdown-menu.d.ts +29 -0
- package/dist/primitives/dropdown-menu.js +50 -0
- package/dist/primitives/field.d.ts +24 -0
- package/dist/primitives/field.js +67 -0
- package/dist/primitives/input-group.d.ts +16 -0
- package/dist/primitives/input-group.js +56 -0
- package/dist/primitives/input.d.ts +3 -0
- package/dist/primitives/input.js +6 -0
- package/dist/primitives/label.d.ts +4 -0
- package/dist/primitives/label.js +7 -0
- package/dist/primitives/progress.d.ts +4 -0
- package/dist/primitives/progress.js +7 -0
- package/dist/primitives/select.d.ts +15 -0
- package/dist/primitives/select.js +35 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.js +8 -0
- package/dist/primitives/sheet.d.ts +14 -0
- package/dist/primitives/sheet.js +36 -0
- package/dist/primitives/skeleton.d.ts +2 -0
- package/dist/primitives/skeleton.js +6 -0
- package/dist/primitives/sonner.d.ts +4 -0
- package/dist/primitives/sonner.js +40 -0
- package/dist/primitives/table.d.ts +10 -0
- package/dist/primitives/table.js +27 -0
- package/dist/primitives/tabs.d.ts +11 -0
- package/dist/primitives/tabs.js +28 -0
- package/dist/primitives/textarea.d.ts +3 -0
- package/dist/primitives/textarea.js +6 -0
- package/dist/primitives/tooltip.d.ts +7 -0
- package/dist/primitives/tooltip.js +17 -0
- package/dist/statusTone.d.ts +37 -0
- package/dist/statusTone.js +38 -0
- package/dist/tokens.d.ts +26 -0
- package/dist/tokens.js +96 -0
- package/gates/canonical.test.mjs +71 -0
- package/gates/extensions.test.mjs +373 -0
- package/gates/index.d.mts +51 -0
- package/gates/index.mjs +514 -0
- package/gates/tokens.test.mjs +276 -0
- package/package.json +60 -0
- package/shadcn/README.md +13 -0
- package/shadcn/alert.tsx +76 -0
- package/shadcn/badge.tsx +49 -0
- package/shadcn/button.tsx +67 -0
- package/shadcn/card.tsx +103 -0
- package/shadcn/combobox.tsx +299 -0
- package/shadcn/dialog.tsx +166 -0
- package/shadcn/dropdown-menu.tsx +267 -0
- package/shadcn/field.tsx +236 -0
- package/shadcn/input-group.tsx +154 -0
- package/shadcn/input.tsx +19 -0
- package/shadcn/label.tsx +22 -0
- package/shadcn/progress.tsx +29 -0
- package/shadcn/select.tsx +190 -0
- package/shadcn/separator.tsx +28 -0
- package/shadcn/sheet.tsx +145 -0
- package/shadcn/skeleton.tsx +13 -0
- package/shadcn/sonner.tsx +49 -0
- package/shadcn/table.tsx +114 -0
- package/shadcn/tabs.tsx +88 -0
- package/shadcn/textarea.tsx +18 -0
- package/shadcn/tooltip.tsx +57 -0
- package/src/harness.test.tsx +37 -0
- package/src/index.ts +119 -0
- package/src/lib/button.ts +4 -0
- package/src/lib/cn.test.ts +33 -0
- package/src/lib/cn.ts +23 -0
- package/src/lib/menu.ts +16 -0
- package/src/lib/progress.ts +6 -0
- package/src/lib/select.ts +4 -0
- package/src/primitives/alert.tsx +76 -0
- package/src/primitives/badge.tsx +57 -0
- package/src/primitives/button.tsx +67 -0
- package/src/primitives/card.tsx +103 -0
- package/src/primitives/combobox.test.tsx +38 -0
- package/src/primitives/combobox.tsx +299 -0
- package/src/primitives/dialog.tsx +166 -0
- package/src/primitives/dropdown-menu.tsx +267 -0
- package/src/primitives/field.tsx +236 -0
- package/src/primitives/input-group.tsx +154 -0
- package/src/primitives/input.tsx +19 -0
- package/src/primitives/label.tsx +22 -0
- package/src/primitives/primitives.test.tsx +440 -0
- package/src/primitives/progress.tsx +29 -0
- package/src/primitives/select.tsx +190 -0
- package/src/primitives/separator.tsx +28 -0
- package/src/primitives/sheet.tsx +145 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/sonner.test.tsx +41 -0
- package/src/primitives/sonner.tsx +65 -0
- package/src/primitives/table.tsx +114 -0
- package/src/primitives/tabs.tsx +88 -0
- package/src/primitives/textarea.tsx +18 -0
- package/src/primitives/tooltip.tsx +57 -0
- package/src/statusTone.test.ts +20 -0
- package/src/statusTone.ts +52 -0
- package/src/styles.css +204 -0
- package/src/tokens.test.ts +285 -0
- package/src/tokens.ts +102 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Dialog as SheetPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn.js"
|
|
5
|
+
import { Button } from "./button.js"
|
|
6
|
+
import { XIcon } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|
9
|
+
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function SheetTrigger({
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
|
15
|
+
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function SheetClose({
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
|
21
|
+
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function SheetPortal({
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
|
27
|
+
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function SheetOverlay({
|
|
31
|
+
className,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
|
34
|
+
return (
|
|
35
|
+
<SheetPrimitive.Overlay
|
|
36
|
+
data-slot="sheet-overlay"
|
|
37
|
+
className={cn(
|
|
38
|
+
"fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function SheetContent({
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
side = "right",
|
|
50
|
+
showCloseButton = true,
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
53
|
+
side?: "top" | "right" | "bottom" | "left"
|
|
54
|
+
showCloseButton?: boolean
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<SheetPortal>
|
|
58
|
+
<SheetOverlay />
|
|
59
|
+
<SheetPrimitive.Content
|
|
60
|
+
data-slot="sheet-content"
|
|
61
|
+
data-side={side}
|
|
62
|
+
className={cn(
|
|
63
|
+
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
{showCloseButton && (
|
|
70
|
+
<SheetPrimitive.Close data-slot="sheet-close" asChild>
|
|
71
|
+
<Button
|
|
72
|
+
variant="ghost"
|
|
73
|
+
className="absolute top-3 right-3"
|
|
74
|
+
size="icon-sm"
|
|
75
|
+
>
|
|
76
|
+
<XIcon
|
|
77
|
+
/>
|
|
78
|
+
<span className="sr-only">Close</span>
|
|
79
|
+
</Button>
|
|
80
|
+
</SheetPrimitive.Close>
|
|
81
|
+
)}
|
|
82
|
+
</SheetPrimitive.Content>
|
|
83
|
+
</SheetPortal>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
data-slot="sheet-header"
|
|
91
|
+
className={cn("flex flex-col gap-0.5 p-4", className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
data-slot="sheet-footer"
|
|
101
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function SheetTitle({
|
|
108
|
+
className,
|
|
109
|
+
...props
|
|
110
|
+
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
|
111
|
+
return (
|
|
112
|
+
<SheetPrimitive.Title
|
|
113
|
+
data-slot="sheet-title"
|
|
114
|
+
className={cn(
|
|
115
|
+
"font-heading text-base font-medium text-foreground",
|
|
116
|
+
className
|
|
117
|
+
)}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function SheetDescription({
|
|
124
|
+
className,
|
|
125
|
+
...props
|
|
126
|
+
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
|
127
|
+
return (
|
|
128
|
+
<SheetPrimitive.Description
|
|
129
|
+
data-slot="sheet-description"
|
|
130
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
Sheet,
|
|
138
|
+
SheetTrigger,
|
|
139
|
+
SheetClose,
|
|
140
|
+
SheetContent,
|
|
141
|
+
SheetHeader,
|
|
142
|
+
SheetFooter,
|
|
143
|
+
SheetTitle,
|
|
144
|
+
SheetDescription,
|
|
145
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("animate-pulse rounded-md bg-muted", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { render, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { toast } from "sonner";
|
|
4
|
+
import { Toaster } from "./sonner";
|
|
5
|
+
|
|
6
|
+
afterEach(() => document.documentElement.classList.remove("dark"));
|
|
7
|
+
|
|
8
|
+
// sonner only mounts `[data-sonner-toaster]` once a toast exists (it maps
|
|
9
|
+
// over `filteredToasts` and returns `null` for every position while that
|
|
10
|
+
// list is empty), and the theme it stamps on that element lands in
|
|
11
|
+
// `data-sonner-theme`, not `data-theme`. A toast is fired here to bring the
|
|
12
|
+
// element into being; `waitFor` covers the render sonner does outside this
|
|
13
|
+
// call stack when it flushes the toast through its module-global store.
|
|
14
|
+
describe("toaster theme", () => {
|
|
15
|
+
it("is light when <html> carries no class", async () => {
|
|
16
|
+
const { container } = render(<Toaster />);
|
|
17
|
+
toast("hi");
|
|
18
|
+
await waitFor(() => {
|
|
19
|
+
expect(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("light");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it("is dark when <html> carries .dark", async () => {
|
|
23
|
+
document.documentElement.classList.add("dark");
|
|
24
|
+
const { container } = render(<Toaster />);
|
|
25
|
+
toast("hi");
|
|
26
|
+
await waitFor(() => {
|
|
27
|
+
expect(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("dark");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it("follows <html> from light to dark after mount, via the MutationObserver", async () => {
|
|
31
|
+
const { container } = render(<Toaster />);
|
|
32
|
+
toast("hi");
|
|
33
|
+
await waitFor(() => {
|
|
34
|
+
expect(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("light");
|
|
35
|
+
});
|
|
36
|
+
document.documentElement.classList.add("dark");
|
|
37
|
+
await waitFor(() => {
|
|
38
|
+
expect(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("dark");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
|
5
|
+
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
// SHADCN FRAMEWORK ADAPTER: shadcn's sonner reads next-themes (a Next.js
|
|
8
|
+
// integration). VisionSet is Vite with one theme source, `.dark` on <html>
|
|
9
|
+
// (styles.css `@custom-variant dark`). This hook is the only difference from
|
|
10
|
+
// frontend/ui-core/shadcn/sonner.tsx; drop it if the app ever adopts a
|
|
11
|
+
// provider that shadcn's file can read.
|
|
12
|
+
function useTheme(): { theme: "light" | "dark" } {
|
|
13
|
+
const read = () => (typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light")
|
|
14
|
+
const [theme, setTheme] = React.useState<"light" | "dark">(read)
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
const observer = new MutationObserver(() => setTheme(read()))
|
|
17
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
|
|
18
|
+
return () => observer.disconnect()
|
|
19
|
+
}, [])
|
|
20
|
+
return { theme }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
24
|
+
const { theme = "system" } = useTheme()
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Sonner
|
|
28
|
+
theme={theme as ToasterProps["theme"]}
|
|
29
|
+
className="toaster group"
|
|
30
|
+
icons={{
|
|
31
|
+
success: (
|
|
32
|
+
<CircleCheckIcon className="size-4" />
|
|
33
|
+
),
|
|
34
|
+
info: (
|
|
35
|
+
<InfoIcon className="size-4" />
|
|
36
|
+
),
|
|
37
|
+
warning: (
|
|
38
|
+
<TriangleAlertIcon className="size-4" />
|
|
39
|
+
),
|
|
40
|
+
error: (
|
|
41
|
+
<OctagonXIcon className="size-4" />
|
|
42
|
+
),
|
|
43
|
+
loading: (
|
|
44
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
45
|
+
),
|
|
46
|
+
}}
|
|
47
|
+
style={
|
|
48
|
+
{
|
|
49
|
+
"--normal-bg": "var(--popover)",
|
|
50
|
+
"--normal-text": "var(--popover-foreground)",
|
|
51
|
+
"--normal-border": "var(--border)",
|
|
52
|
+
"--border-radius": "var(--radius)",
|
|
53
|
+
} as React.CSSProperties
|
|
54
|
+
}
|
|
55
|
+
toastOptions={{
|
|
56
|
+
classNames: {
|
|
57
|
+
toast: "cn-toast",
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Toaster }
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/cn.js"
|
|
4
|
+
|
|
5
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="table-container"
|
|
9
|
+
className="relative w-full overflow-x-auto"
|
|
10
|
+
>
|
|
11
|
+
<table
|
|
12
|
+
data-slot="table"
|
|
13
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
21
|
+
return (
|
|
22
|
+
<thead
|
|
23
|
+
data-slot="table-header"
|
|
24
|
+
className={cn("[&_tr]:border-b", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
31
|
+
return (
|
|
32
|
+
<tbody
|
|
33
|
+
data-slot="table-body"
|
|
34
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
41
|
+
return (
|
|
42
|
+
<tfoot
|
|
43
|
+
data-slot="table-footer"
|
|
44
|
+
className={cn(
|
|
45
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
54
|
+
return (
|
|
55
|
+
<tr
|
|
56
|
+
data-slot="table-row"
|
|
57
|
+
className={cn(
|
|
58
|
+
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
67
|
+
return (
|
|
68
|
+
<th
|
|
69
|
+
data-slot="table-head"
|
|
70
|
+
className={cn(
|
|
71
|
+
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
80
|
+
return (
|
|
81
|
+
<td
|
|
82
|
+
data-slot="table-cell"
|
|
83
|
+
className={cn(
|
|
84
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function TableCaption({
|
|
93
|
+
className,
|
|
94
|
+
...props
|
|
95
|
+
}: React.ComponentProps<"caption">) {
|
|
96
|
+
return (
|
|
97
|
+
<caption
|
|
98
|
+
data-slot="table-caption"
|
|
99
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
Table,
|
|
107
|
+
TableHeader,
|
|
108
|
+
TableBody,
|
|
109
|
+
TableFooter,
|
|
110
|
+
TableHead,
|
|
111
|
+
TableRow,
|
|
112
|
+
TableCell,
|
|
113
|
+
TableCaption,
|
|
114
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { Tabs as TabsPrimitive } from "radix-ui"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn.js"
|
|
6
|
+
|
|
7
|
+
function Tabs({
|
|
8
|
+
className,
|
|
9
|
+
orientation = "horizontal",
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<TabsPrimitive.Root
|
|
14
|
+
data-slot="tabs"
|
|
15
|
+
data-orientation={orientation}
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/tabs flex gap-2 data-horizontal:flex-col",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tabsListVariants = cva(
|
|
26
|
+
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
|
27
|
+
{
|
|
28
|
+
variants: {
|
|
29
|
+
variant: {
|
|
30
|
+
default: "bg-muted",
|
|
31
|
+
line: "gap-1 bg-transparent",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "default",
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
function TabsList({
|
|
41
|
+
className,
|
|
42
|
+
variant = "default",
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
|
45
|
+
VariantProps<typeof tabsListVariants>) {
|
|
46
|
+
return (
|
|
47
|
+
<TabsPrimitive.List
|
|
48
|
+
data-slot="tabs-list"
|
|
49
|
+
data-variant={variant}
|
|
50
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function TabsTrigger({
|
|
57
|
+
className,
|
|
58
|
+
...props
|
|
59
|
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
60
|
+
return (
|
|
61
|
+
<TabsPrimitive.Trigger
|
|
62
|
+
data-slot="tabs-trigger"
|
|
63
|
+
className={cn(
|
|
64
|
+
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
65
|
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
66
|
+
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
|
67
|
+
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function TabsContent({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
79
|
+
return (
|
|
80
|
+
<TabsPrimitive.Content
|
|
81
|
+
data-slot="tabs-content"
|
|
82
|
+
className={cn("flex-1 text-sm outline-none", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/cn.js"
|
|
4
|
+
|
|
5
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
data-slot="textarea"
|
|
9
|
+
className={cn(
|
|
10
|
+
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/cn.js"
|
|
7
|
+
|
|
8
|
+
function TooltipProvider({
|
|
9
|
+
delayDuration = 0,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
12
|
+
return (
|
|
13
|
+
<TooltipPrimitive.Provider
|
|
14
|
+
data-slot="tooltip-provider"
|
|
15
|
+
delayDuration={delayDuration}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Tooltip({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
24
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function TooltipTrigger({
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
30
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function TooltipContent({
|
|
34
|
+
className,
|
|
35
|
+
sideOffset = 0,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipPrimitive.Portal>
|
|
41
|
+
<TooltipPrimitive.Content
|
|
42
|
+
data-slot="tooltip-content"
|
|
43
|
+
sideOffset={sideOffset}
|
|
44
|
+
className={cn(
|
|
45
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
|
52
|
+
</TooltipPrimitive.Content>
|
|
53
|
+
</TooltipPrimitive.Portal>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { STATUS_INK, TONE_BORDER, TONE_FILL } from "./statusTone";
|
|
4
|
+
|
|
5
|
+
describe("status tones", () => {
|
|
6
|
+
it("names one Tailwind family per status and never a retired token", () => {
|
|
7
|
+
expect(TONE_FILL.success).toBe("bg-emerald-500 dark:bg-emerald-400");
|
|
8
|
+
expect(TONE_FILL.warning).toBe("bg-amber-500 dark:bg-amber-400");
|
|
9
|
+
expect(TONE_BORDER.success).toBe("border-emerald-500 dark:border-emerald-400");
|
|
10
|
+
expect(TONE_BORDER.warning).toBe("border-amber-500 dark:border-amber-400");
|
|
11
|
+
expect(TONE_FILL.destructive).toBe("bg-destructive");
|
|
12
|
+
expect(TONE_BORDER.neutral).toBe("border-border");
|
|
13
|
+
expect(STATUS_INK.warning).toBe("text-amber-700 dark:text-amber-400");
|
|
14
|
+
expect(STATUS_INK.success).toBe("text-emerald-700 dark:text-emerald-400");
|
|
15
|
+
expect(STATUS_INK.info).toBe("text-sky-700 dark:text-sky-400");
|
|
16
|
+
for (const v of [...Object.values(TONE_FILL), ...Object.values(TONE_BORDER), ...Object.values(STATUS_INK)]) {
|
|
17
|
+
expect(v).not.toMatch(/\b(?:bg|border|text)-(?:success|warning)\b/);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one home for status colour outside the `Badge`.
|
|
3
|
+
*
|
|
4
|
+
* `badge.tsx` earns its own emerald/amber/sky because a chip's `/10` surface
|
|
5
|
+
* and hairline are a treatment nothing else draws. Everything else that paints
|
|
6
|
+
* a status — a dot, a timeline cell, an inline icon or word — reads its colour
|
|
7
|
+
* from here instead of naming a Tailwind family at its own call site, so a
|
|
8
|
+
* sixth place inventing a fourth spelling of "warning" is a type error, not a
|
|
9
|
+
* diff nobody notices.
|
|
10
|
+
*/
|
|
11
|
+
export type StatusTone = "neutral" | "accent" | "success" | "warning" | "destructive";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A tone's border and its fill, as whole utility names.
|
|
15
|
+
*
|
|
16
|
+
* Whole names rather than a `border-${tone}` template, because Tailwind scans
|
|
17
|
+
* source *text*: a class assembled at runtime is a class the build never saw and
|
|
18
|
+
* therefore a rule that is never emitted. The failure is silent and looks like a
|
|
19
|
+
* styling mistake, which is why these are written out.
|
|
20
|
+
*
|
|
21
|
+
* A status **dot** and a timeline **cell** are solid marks, not `Badge`s — a
|
|
22
|
+
* full-strength `*-500` fill is right there, where the `Badge`'s `/10` surface
|
|
23
|
+
* would vanish at 4px.
|
|
24
|
+
*/
|
|
25
|
+
export const TONE_BORDER: Record<StatusTone, string> = {
|
|
26
|
+
neutral: "border-border",
|
|
27
|
+
accent: "border-primary",
|
|
28
|
+
success: "border-emerald-500 dark:border-emerald-400",
|
|
29
|
+
warning: "border-amber-500 dark:border-amber-400",
|
|
30
|
+
destructive: "border-destructive",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const TONE_FILL: Record<StatusTone, string> = {
|
|
34
|
+
neutral: "bg-muted-foreground",
|
|
35
|
+
accent: "bg-primary",
|
|
36
|
+
success: "bg-emerald-500 dark:bg-emerald-400",
|
|
37
|
+
warning: "bg-amber-500 dark:bg-amber-400",
|
|
38
|
+
destructive: "bg-destructive",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ink for an icon or a run of inline text carrying a status — a `warning`
|
|
43
|
+
* triangle beside a sentence, not a dot or a cell, so it wants a legible
|
|
44
|
+
* foreground rather than `TONE_FILL`'s solid mark. `info` has no `StatusTone`
|
|
45
|
+
* counterpart: it names sky for surfaces that are informational without being
|
|
46
|
+
* any of `neutral`/`accent`/`success`/`warning`/`destructive`.
|
|
47
|
+
*/
|
|
48
|
+
export const STATUS_INK = {
|
|
49
|
+
success: "text-emerald-700 dark:text-emerald-400",
|
|
50
|
+
warning: "text-amber-700 dark:text-amber-400",
|
|
51
|
+
info: "text-sky-700 dark:text-sky-400",
|
|
52
|
+
} as const;
|