@robomous/ui-core 0.1.0 → 0.2.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/README.md +19 -7
- package/dist/{primitives → components}/alert.js +1 -1
- package/dist/{primitives → components}/badge.js +1 -1
- package/dist/{primitives → components}/button.d.ts +1 -1
- package/dist/{primitives → components}/button.js +4 -1
- package/dist/{primitives → components}/card.d.ts +1 -1
- package/dist/{primitives → components}/card.js +2 -2
- package/dist/{primitives → components}/combobox.js +4 -4
- package/dist/{primitives → components}/dialog.js +1 -1
- package/dist/{primitives → components}/dropdown-menu.js +12 -7
- package/dist/{primitives → components}/field.js +2 -4
- package/dist/{primitives → components}/input-group.js +1 -1
- package/dist/{primitives → components}/input.js +1 -1
- package/dist/{primitives → components}/label.js +1 -1
- package/dist/components/progress.js +7 -0
- package/dist/{primitives → components}/select.d.ts +3 -1
- package/dist/{primitives → components}/select.js +7 -4
- package/dist/{primitives → components}/separator.js +1 -1
- package/dist/{primitives → components}/sheet.js +1 -1
- package/dist/{primitives → components}/skeleton.js +1 -1
- package/dist/{primitives → components}/sonner.js +17 -12
- package/dist/{primitives → components}/table.d.ts +1 -1
- package/dist/{primitives → components}/table.js +3 -3
- package/dist/{primitives → components}/tabs.js +1 -1
- package/dist/{primitives → components}/textarea.js +1 -1
- package/dist/{primitives → components}/tooltip.js +1 -1
- package/dist/gates/index.d.ts +28 -0
- package/dist/gates/index.js +142 -0
- package/dist/index.d.ts +31 -32
- package/dist/index.js +32 -33
- package/dist/{tokens.d.ts → theme/tokens.d.ts} +5 -5
- package/dist/{tokens.js → theme/tokens.js} +12 -12
- package/package.json +23 -12
- package/{shadcn → src/components}/alert.tsx +14 -21
- package/src/{primitives → components}/badge.tsx +15 -21
- package/src/{primitives → components}/button.tsx +13 -10
- package/{shadcn → src/components}/card.tsx +16 -31
- package/src/{primitives → components}/combobox.test.tsx +15 -2
- package/src/{primitives → components}/combobox.tsx +45 -74
- package/src/{primitives/primitives.test.tsx → components/components.test.tsx} +106 -69
- package/src/{primitives → components}/dialog.tsx +31 -54
- package/src/{primitives → components}/dropdown-menu.tsx +51 -65
- package/src/{primitives → components}/field.tsx +48 -62
- package/src/{primitives → components}/input-group.tsx +37 -49
- package/{shadcn → src/components}/input.tsx +5 -5
- package/{shadcn → src/components}/label.tsx +7 -10
- package/src/{primitives → components}/progress.tsx +7 -6
- package/{shadcn → src/components}/select.tsx +40 -40
- package/{shadcn → src/components}/separator.tsx +7 -7
- package/src/{primitives → components}/sheet.tsx +27 -44
- package/{shadcn → src/components}/skeleton.tsx +3 -3
- package/src/{primitives → components}/sonner.test.tsx +12 -4
- package/src/components/sonner.tsx +67 -0
- package/{shadcn → src/components}/table.tsx +17 -44
- package/{shadcn → src/components}/tabs.tsx +16 -26
- package/{shadcn → src/components}/textarea.tsx +5 -5
- package/{shadcn → src/components}/tooltip.tsx +12 -16
- package/src/gates/design.test.ts +188 -0
- package/src/gates/index.test.ts +11 -0
- package/src/gates/index.ts +165 -0
- package/src/gates/tokens.test.ts +215 -0
- package/src/harness.test.tsx +1 -1
- package/src/index.ts +63 -33
- package/src/theme/imports.test.ts +160 -0
- package/src/{statusTone.test.ts → theme/statusTone.test.ts} +5 -1
- package/src/{styles.css → theme/styles.css} +45 -31
- package/src/theme/tailwind.css +646 -0
- package/src/{tokens.test.ts → theme/tokens.test.ts} +30 -23
- package/src/{tokens.ts → theme/tokens.ts} +12 -12
- package/components.json +0 -13
- package/dist/lib/button.d.ts +0 -1
- package/dist/lib/button.js +0 -4
- package/dist/lib/cn.d.ts +0 -18
- package/dist/lib/cn.js +0 -21
- package/dist/lib/menu.d.ts +0 -1
- package/dist/lib/menu.js +0 -16
- package/dist/lib/progress.d.ts +0 -3
- package/dist/lib/progress.js +0 -6
- package/dist/lib/select.d.ts +0 -1
- package/dist/lib/select.js +0 -3
- package/dist/primitives/progress.js +0 -7
- package/gates/canonical.test.mjs +0 -71
- package/gates/extensions.test.mjs +0 -373
- package/gates/index.d.mts +0 -51
- package/gates/index.mjs +0 -514
- package/gates/tokens.test.mjs +0 -276
- package/shadcn/README.md +0 -13
- package/shadcn/badge.tsx +0 -49
- package/shadcn/button.tsx +0 -67
- package/shadcn/combobox.tsx +0 -299
- package/shadcn/dialog.tsx +0 -166
- package/shadcn/dropdown-menu.tsx +0 -267
- package/shadcn/field.tsx +0 -236
- package/shadcn/input-group.tsx +0 -154
- package/shadcn/progress.tsx +0 -29
- package/shadcn/sheet.tsx +0 -145
- package/shadcn/sonner.tsx +0 -49
- package/src/lib/button.ts +0 -4
- package/src/lib/cn.test.ts +0 -33
- package/src/lib/cn.ts +0 -23
- package/src/lib/menu.ts +0 -16
- package/src/lib/progress.ts +0 -6
- package/src/lib/select.ts +0 -4
- package/src/primitives/alert.tsx +0 -76
- package/src/primitives/card.tsx +0 -103
- package/src/primitives/input.tsx +0 -19
- package/src/primitives/label.tsx +0 -22
- package/src/primitives/select.tsx +0 -190
- package/src/primitives/separator.tsx +0 -28
- package/src/primitives/skeleton.tsx +0 -13
- package/src/primitives/sonner.tsx +0 -65
- package/src/primitives/table.tsx +0 -114
- package/src/primitives/tabs.tsx +0 -88
- package/src/primitives/textarea.tsx +0 -18
- package/src/primitives/tooltip.tsx +0 -57
- /package/dist/{primitives → components}/alert.d.ts +0 -0
- /package/dist/{primitives → components}/badge.d.ts +0 -0
- /package/dist/{primitives → components}/combobox.d.ts +0 -0
- /package/dist/{primitives → components}/dialog.d.ts +0 -0
- /package/dist/{primitives → components}/dropdown-menu.d.ts +0 -0
- /package/dist/{primitives → components}/field.d.ts +0 -0
- /package/dist/{primitives → components}/input-group.d.ts +0 -0
- /package/dist/{primitives → components}/input.d.ts +0 -0
- /package/dist/{primitives → components}/label.d.ts +0 -0
- /package/dist/{primitives → components}/progress.d.ts +0 -0
- /package/dist/{primitives → components}/separator.d.ts +0 -0
- /package/dist/{primitives → components}/sheet.d.ts +0 -0
- /package/dist/{primitives → components}/skeleton.d.ts +0 -0
- /package/dist/{primitives → components}/sonner.d.ts +0 -0
- /package/dist/{primitives → components}/tabs.d.ts +0 -0
- /package/dist/{primitives → components}/textarea.d.ts +0 -0
- /package/dist/{primitives → components}/tooltip.d.ts +0 -0
- /package/dist/{statusTone.d.ts → theme/statusTone.d.ts} +0 -0
- /package/dist/{statusTone.js → theme/statusTone.js} +0 -0
- /package/src/{statusTone.ts → theme/statusTone.ts} +0 -0
|
@@ -16,7 +16,9 @@ describe("toaster theme", () => {
|
|
|
16
16
|
const { container } = render(<Toaster />);
|
|
17
17
|
toast("hi");
|
|
18
18
|
await waitFor(() => {
|
|
19
|
-
expect(
|
|
19
|
+
expect(
|
|
20
|
+
container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme"),
|
|
21
|
+
).toBe("light");
|
|
20
22
|
});
|
|
21
23
|
});
|
|
22
24
|
it("is dark when <html> carries .dark", async () => {
|
|
@@ -24,18 +26,24 @@ describe("toaster theme", () => {
|
|
|
24
26
|
const { container } = render(<Toaster />);
|
|
25
27
|
toast("hi");
|
|
26
28
|
await waitFor(() => {
|
|
27
|
-
expect(
|
|
29
|
+
expect(
|
|
30
|
+
container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme"),
|
|
31
|
+
).toBe("dark");
|
|
28
32
|
});
|
|
29
33
|
});
|
|
30
34
|
it("follows <html> from light to dark after mount, via the MutationObserver", async () => {
|
|
31
35
|
const { container } = render(<Toaster />);
|
|
32
36
|
toast("hi");
|
|
33
37
|
await waitFor(() => {
|
|
34
|
-
expect(
|
|
38
|
+
expect(
|
|
39
|
+
container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme"),
|
|
40
|
+
).toBe("light");
|
|
35
41
|
});
|
|
36
42
|
document.documentElement.classList.add("dark");
|
|
37
43
|
await waitFor(() => {
|
|
38
|
-
expect(
|
|
44
|
+
expect(
|
|
45
|
+
container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme"),
|
|
46
|
+
).toBe("dark");
|
|
39
47
|
});
|
|
40
48
|
});
|
|
41
49
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
|
5
|
+
import {
|
|
6
|
+
CircleCheckIcon,
|
|
7
|
+
InfoIcon,
|
|
8
|
+
TriangleAlertIcon,
|
|
9
|
+
OctagonXIcon,
|
|
10
|
+
Loader2Icon,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
|
|
13
|
+
// The theme this package runs on has one source: the `dark` class on <html>,
|
|
14
|
+
// which is what `styles.css`'s `@custom-variant dark` keys off and therefore
|
|
15
|
+
// what every other component in here already follows. A toast is portalled
|
|
16
|
+
// outside the tree but reads the same page, so it has to answer to the same
|
|
17
|
+
// source — hence this hook rather than a `next-themes` `useTheme`, which asks
|
|
18
|
+
// for a React provider none of this runs under. Sonner needs the answer as a
|
|
19
|
+
// value, not a class, which is the only reason the class has to be observed
|
|
20
|
+
// at all.
|
|
21
|
+
function useTheme(): { theme: "light" | "dark" } {
|
|
22
|
+
const read = () =>
|
|
23
|
+
typeof document !== "undefined" && document.documentElement.classList.contains("dark")
|
|
24
|
+
? "dark"
|
|
25
|
+
: "light";
|
|
26
|
+
const [theme, setTheme] = React.useState<"light" | "dark">(read);
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
const observer = new MutationObserver(() => setTheme(read()));
|
|
29
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, []);
|
|
32
|
+
return { theme };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
36
|
+
const { theme = "system" } = useTheme();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Sonner
|
|
40
|
+
theme={theme as ToasterProps["theme"]}
|
|
41
|
+
className="toaster group"
|
|
42
|
+
icons={{
|
|
43
|
+
success: <CircleCheckIcon className="size-4" />,
|
|
44
|
+
info: <InfoIcon className="size-4" />,
|
|
45
|
+
warning: <TriangleAlertIcon className="size-4" />,
|
|
46
|
+
error: <OctagonXIcon className="size-4" />,
|
|
47
|
+
loading: <Loader2Icon className="size-4 animate-spin" />,
|
|
48
|
+
}}
|
|
49
|
+
style={
|
|
50
|
+
{
|
|
51
|
+
"--normal-bg": "var(--popover)",
|
|
52
|
+
"--normal-text": "var(--popover-foreground)",
|
|
53
|
+
"--normal-border": "var(--border)",
|
|
54
|
+
"--border-radius": "var(--radius)",
|
|
55
|
+
} as React.CSSProperties
|
|
56
|
+
}
|
|
57
|
+
toastOptions={{
|
|
58
|
+
classNames: {
|
|
59
|
+
toast: "cn-toast",
|
|
60
|
+
},
|
|
61
|
+
}}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export { Toaster };
|
|
@@ -1,30 +1,21 @@
|
|
|
1
|
-
import * as React from "react"
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import { cn } from "
|
|
3
|
+
import { cn } from "cn";
|
|
4
4
|
|
|
5
5
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6
6
|
return (
|
|
7
|
-
<div
|
|
8
|
-
data-slot="table-container"
|
|
9
|
-
className="relative w-full overflow-x-auto"
|
|
10
|
-
>
|
|
7
|
+
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
|
11
8
|
<table
|
|
12
9
|
data-slot="table"
|
|
13
10
|
className={cn("w-full caption-bottom text-sm", className)}
|
|
14
11
|
{...props}
|
|
15
12
|
/>
|
|
16
13
|
</div>
|
|
17
|
-
)
|
|
14
|
+
);
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
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
|
-
)
|
|
18
|
+
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
|
|
28
19
|
}
|
|
29
20
|
|
|
30
21
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
@@ -34,20 +25,17 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
|
34
25
|
className={cn("[&_tr:last-child]:border-0", className)}
|
|
35
26
|
{...props}
|
|
36
27
|
/>
|
|
37
|
-
)
|
|
28
|
+
);
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
41
32
|
return (
|
|
42
33
|
<tfoot
|
|
43
34
|
data-slot="table-footer"
|
|
44
|
-
className={cn(
|
|
45
|
-
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
46
|
-
className
|
|
47
|
-
)}
|
|
35
|
+
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
48
36
|
{...props}
|
|
49
37
|
/>
|
|
50
|
-
)
|
|
38
|
+
);
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
@@ -56,11 +44,11 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
|
56
44
|
data-slot="table-row"
|
|
57
45
|
className={cn(
|
|
58
46
|
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
|
59
|
-
className
|
|
47
|
+
className,
|
|
60
48
|
)}
|
|
61
49
|
{...props}
|
|
62
50
|
/>
|
|
63
|
-
)
|
|
51
|
+
);
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
@@ -69,46 +57,31 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
69
57
|
data-slot="table-head"
|
|
70
58
|
className={cn(
|
|
71
59
|
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
|
72
|
-
className
|
|
60
|
+
className,
|
|
73
61
|
)}
|
|
74
62
|
{...props}
|
|
75
63
|
/>
|
|
76
|
-
)
|
|
64
|
+
);
|
|
77
65
|
}
|
|
78
66
|
|
|
79
67
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
80
68
|
return (
|
|
81
69
|
<td
|
|
82
70
|
data-slot="table-cell"
|
|
83
|
-
className={cn(
|
|
84
|
-
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
|
85
|
-
className
|
|
86
|
-
)}
|
|
71
|
+
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
|
87
72
|
{...props}
|
|
88
73
|
/>
|
|
89
|
-
)
|
|
74
|
+
);
|
|
90
75
|
}
|
|
91
76
|
|
|
92
|
-
function TableCaption({
|
|
93
|
-
className,
|
|
94
|
-
...props
|
|
95
|
-
}: React.ComponentProps<"caption">) {
|
|
77
|
+
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
|
96
78
|
return (
|
|
97
79
|
<caption
|
|
98
80
|
data-slot="table-caption"
|
|
99
81
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
100
82
|
{...props}
|
|
101
83
|
/>
|
|
102
|
-
)
|
|
84
|
+
);
|
|
103
85
|
}
|
|
104
86
|
|
|
105
|
-
export {
|
|
106
|
-
Table,
|
|
107
|
-
TableHeader,
|
|
108
|
-
TableBody,
|
|
109
|
-
TableFooter,
|
|
110
|
-
TableHead,
|
|
111
|
-
TableRow,
|
|
112
|
-
TableCell,
|
|
113
|
-
TableCaption,
|
|
114
|
-
}
|
|
87
|
+
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
-
import { Tabs as TabsPrimitive } from "radix-ui"
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
4
4
|
|
|
5
|
-
import { cn } from "
|
|
5
|
+
import { cn } from "cn";
|
|
6
6
|
|
|
7
7
|
function Tabs({
|
|
8
8
|
className,
|
|
@@ -13,13 +13,10 @@ function Tabs({
|
|
|
13
13
|
<TabsPrimitive.Root
|
|
14
14
|
data-slot="tabs"
|
|
15
15
|
data-orientation={orientation}
|
|
16
|
-
className={cn(
|
|
17
|
-
"group/tabs flex gap-2 data-horizontal:flex-col",
|
|
18
|
-
className
|
|
19
|
-
)}
|
|
16
|
+
className={cn("group/tabs flex gap-2 data-horizontal:flex-col", className)}
|
|
20
17
|
{...props}
|
|
21
18
|
/>
|
|
22
|
-
)
|
|
19
|
+
);
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
const tabsListVariants = cva(
|
|
@@ -34,15 +31,14 @@ const tabsListVariants = cva(
|
|
|
34
31
|
defaultVariants: {
|
|
35
32
|
variant: "default",
|
|
36
33
|
},
|
|
37
|
-
}
|
|
38
|
-
)
|
|
34
|
+
},
|
|
35
|
+
);
|
|
39
36
|
|
|
40
37
|
function TabsList({
|
|
41
38
|
className,
|
|
42
39
|
variant = "default",
|
|
43
40
|
...props
|
|
44
|
-
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
|
45
|
-
VariantProps<typeof tabsListVariants>) {
|
|
41
|
+
}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
|
|
46
42
|
return (
|
|
47
43
|
<TabsPrimitive.List
|
|
48
44
|
data-slot="tabs-list"
|
|
@@ -50,13 +46,10 @@ function TabsList({
|
|
|
50
46
|
className={cn(tabsListVariants({ variant }), className)}
|
|
51
47
|
{...props}
|
|
52
48
|
/>
|
|
53
|
-
)
|
|
49
|
+
);
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
function TabsTrigger({
|
|
57
|
-
className,
|
|
58
|
-
...props
|
|
59
|
-
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
52
|
+
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
60
53
|
return (
|
|
61
54
|
<TabsPrimitive.Trigger
|
|
62
55
|
data-slot="tabs-trigger"
|
|
@@ -65,24 +58,21 @@ function TabsTrigger({
|
|
|
65
58
|
"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
59
|
"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
60
|
"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
|
|
61
|
+
className,
|
|
69
62
|
)}
|
|
70
63
|
{...props}
|
|
71
64
|
/>
|
|
72
|
-
)
|
|
65
|
+
);
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
function TabsContent({
|
|
76
|
-
className,
|
|
77
|
-
...props
|
|
78
|
-
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
68
|
+
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
79
69
|
return (
|
|
80
70
|
<TabsPrimitive.Content
|
|
81
71
|
data-slot="tabs-content"
|
|
82
72
|
className={cn("flex-1 text-sm outline-none", className)}
|
|
83
73
|
{...props}
|
|
84
74
|
/>
|
|
85
|
-
)
|
|
75
|
+
);
|
|
86
76
|
}
|
|
87
77
|
|
|
88
|
-
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
|
78
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as React from "react"
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import { cn } from "
|
|
3
|
+
import { cn } from "cn";
|
|
4
4
|
|
|
5
5
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
6
|
return (
|
|
@@ -8,11 +8,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
|
8
8
|
data-slot="textarea"
|
|
9
9
|
className={cn(
|
|
10
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
|
|
11
|
+
className,
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
14
14
|
/>
|
|
15
|
-
)
|
|
15
|
+
);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export { Textarea }
|
|
18
|
+
export { Textarea };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
|
5
5
|
|
|
6
|
-
import { cn } from "
|
|
6
|
+
import { cn } from "cn";
|
|
7
7
|
|
|
8
8
|
function TooltipProvider({
|
|
9
9
|
delayDuration = 0,
|
|
@@ -15,19 +15,15 @@ function TooltipProvider({
|
|
|
15
15
|
delayDuration={delayDuration}
|
|
16
16
|
{...props}
|
|
17
17
|
/>
|
|
18
|
-
)
|
|
18
|
+
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function Tooltip({
|
|
22
|
-
...props
|
|
23
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
24
|
-
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
21
|
+
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
22
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
function TooltipTrigger({
|
|
28
|
-
...props
|
|
29
|
-
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
30
|
-
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
25
|
+
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
26
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
function TooltipContent({
|
|
@@ -43,7 +39,7 @@ function TooltipContent({
|
|
|
43
39
|
sideOffset={sideOffset}
|
|
44
40
|
className={cn(
|
|
45
41
|
"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
|
|
42
|
+
className,
|
|
47
43
|
)}
|
|
48
44
|
{...props}
|
|
49
45
|
>
|
|
@@ -51,7 +47,7 @@ function TooltipContent({
|
|
|
51
47
|
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
|
52
48
|
</TooltipPrimitive.Content>
|
|
53
49
|
</TooltipPrimitive.Portal>
|
|
54
|
-
)
|
|
50
|
+
);
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
|
53
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { expect, test } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { competingStatusPaletteIn, statusPaletteIn } from "./index.js";
|
|
9
|
+
|
|
10
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
11
|
+
const read = (rel: string) => readFileSync(path.join(REPO, rel), "utf8");
|
|
12
|
+
|
|
13
|
+
const SOURCE = /\.(?:ts|tsx|css)$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Assembles a Tailwind utility fixture (e.g. "bg-emerald-500") from its
|
|
17
|
+
* prefix and family/shade at runtime. This file scans its own package (it
|
|
18
|
+
* lives under `src/`), so a fixture spelled out contiguously would trip the
|
|
19
|
+
* very gates below on itself — the same reason `HEX` and
|
|
20
|
+
* `RETIRED_ICON_PACKAGE` are built from fragments in the sibling gate files.
|
|
21
|
+
*/
|
|
22
|
+
const cls = (prefix: string, familyAndShade: string) => `${prefix}-${familyAndShade}`;
|
|
23
|
+
|
|
24
|
+
/** Every tracked package file matching `SOURCE`. */
|
|
25
|
+
function packageSources(): string[] {
|
|
26
|
+
const listed = spawnSync("git", ["ls-files", "-z", "src"], { cwd: REPO, encoding: "utf8" });
|
|
27
|
+
expect(listed.status, `git ls-files failed: ${listed.stderr}`).toBe(0);
|
|
28
|
+
return listed.stdout.split("\0").filter((name) => SOURCE.test(name));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FOUNDATION_BADGE = ["success", "warning", "info", "quiet"];
|
|
32
|
+
|
|
33
|
+
/** The class string of one variant line in a cva source. */
|
|
34
|
+
function variantClasses(source: string, key: string): string {
|
|
35
|
+
const m = source.match(new RegExp(String.raw`^\s*"?${key}"?:\s*\n?\s*"([^"]*)"`, "m"));
|
|
36
|
+
expect(m, `no variant ${key}`).toBeTruthy();
|
|
37
|
+
return m![1];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BADGE = "src/components/badge.tsx";
|
|
41
|
+
|
|
42
|
+
test("Badge's status vocabulary is exactly the four owned names", () => {
|
|
43
|
+
const source = read("src/components/badge.tsx");
|
|
44
|
+
for (const name of FOUNDATION_BADGE) {
|
|
45
|
+
expect(source.includes(`${name}:`), `badge.tsx is missing the ${name} variant`).toBeTruthy();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a status Badge paints a soft surface and readable ink, never a coloured stroke", () => {
|
|
50
|
+
const src = read(BADGE);
|
|
51
|
+
const stroke = /\bborder-(?:emerald|amber|sky|success|warning|destructive|primary)\b/;
|
|
52
|
+
for (const k of [...FOUNDATION_BADGE, "destructive"]) {
|
|
53
|
+
expect(variantClasses(src, k), `${k} adds a coloured border`).not.toMatch(stroke);
|
|
54
|
+
}
|
|
55
|
+
expect(variantClasses(src, "success")).toMatch(/\bbg-emerald-500\/10\b/);
|
|
56
|
+
expect(variantClasses(src, "success")).toMatch(/\btext-emerald-700\b/);
|
|
57
|
+
expect(variantClasses(src, "warning")).toMatch(/\bbg-amber-500\/10\b/);
|
|
58
|
+
expect(variantClasses(src, "warning")).toMatch(/\btext-amber-700\b/);
|
|
59
|
+
expect(variantClasses(src, "info")).toMatch(/\bbg-sky-500\/10\b/);
|
|
60
|
+
expect(variantClasses(src, "info")).toMatch(/\btext-sky-700\b/);
|
|
61
|
+
expect(variantClasses(src, "quiet")).toMatch(/\bbg-muted\b/);
|
|
62
|
+
expect(variantClasses(src, "quiet")).toMatch(/\btext-muted-foreground\b/);
|
|
63
|
+
// Official destructive stays on the semantic token, never red-*.
|
|
64
|
+
expect(variantClasses(src, "destructive")).toMatch(/\bbg-destructive\/10\b/);
|
|
65
|
+
expect(src).not.toMatch(/\b(?:bg|text)-red-\d/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("statusPaletteIn finds the emerald/amber/sky family, and not a token or a comment", () => {
|
|
69
|
+
expect(statusPaletteIn("a.tsx", ` className="${cls("bg", "emerald-500/10")}"`)).toEqual([
|
|
70
|
+
`a.tsx:1: className="${cls("bg", "emerald-500/10")}"`,
|
|
71
|
+
]);
|
|
72
|
+
expect(
|
|
73
|
+
statusPaletteIn(
|
|
74
|
+
"b.tsx",
|
|
75
|
+
` className="${cls("border", "amber-400")} dark:${cls("border", "amber-300")}"`,
|
|
76
|
+
),
|
|
77
|
+
).toEqual([
|
|
78
|
+
`b.tsx:1: className="${cls("border", "amber-400")} dark:${cls("border", "amber-300")}"`,
|
|
79
|
+
]);
|
|
80
|
+
expect(statusPaletteIn("c.tsx", ` className="${cls("ring", "sky-500")}"`)).toEqual([
|
|
81
|
+
`c.tsx:1: className="${cls("ring", "sky-500")}"`,
|
|
82
|
+
]);
|
|
83
|
+
expect(statusPaletteIn("d.tsx", ` className="${cls("shadow", "emerald-500/20")}"`)).toEqual([
|
|
84
|
+
`d.tsx:1: className="${cls("shadow", "emerald-500/20")}"`,
|
|
85
|
+
]);
|
|
86
|
+
// A token, not the palette.
|
|
87
|
+
expect(statusPaletteIn("e.tsx", ` className="bg-primary text-primary-foreground"`)).toEqual([]);
|
|
88
|
+
// The family name without a shade is not yet a colour.
|
|
89
|
+
expect(statusPaletteIn("f.tsx", ` className="bg-emerald"`)).toEqual([]);
|
|
90
|
+
// A comment recalling the palette states history, not a usage.
|
|
91
|
+
expect(
|
|
92
|
+
statusPaletteIn("g.tsx", ` // never ${cls("bg", "emerald-500")} outside statusTone`),
|
|
93
|
+
).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("competingStatusPaletteIn finds a rival colour family, and not the status palette itself", () => {
|
|
97
|
+
expect(competingStatusPaletteIn("a.tsx", ` className="${cls("bg", "green-500")}"`)).toEqual([
|
|
98
|
+
`a.tsx:1: className="${cls("bg", "green-500")}"`,
|
|
99
|
+
]);
|
|
100
|
+
expect(competingStatusPaletteIn("b.tsx", ` className="${cls("text", "yellow-700")}"`)).toEqual([
|
|
101
|
+
`b.tsx:1: className="${cls("text", "yellow-700")}"`,
|
|
102
|
+
]);
|
|
103
|
+
expect(competingStatusPaletteIn("c.tsx", ` className="${cls("border", "blue-400")}"`)).toEqual([
|
|
104
|
+
`c.tsx:1: className="${cls("border", "blue-400")}"`,
|
|
105
|
+
]);
|
|
106
|
+
expect(competingStatusPaletteIn("d.tsx", ` className="${cls("bg", "emerald-500")}"`)).toEqual(
|
|
107
|
+
[],
|
|
108
|
+
);
|
|
109
|
+
expect(
|
|
110
|
+
competingStatusPaletteIn("e.tsx", ` // never ${cls("bg", "red-500")} for a destructive state`),
|
|
111
|
+
).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("the status palette lives in exactly Badge and statusTone, nowhere else", () => {
|
|
115
|
+
const ALLOWED_PALETTE_FILES = [
|
|
116
|
+
"src/components/badge.tsx",
|
|
117
|
+
"src/theme/statusTone.ts",
|
|
118
|
+
"src/theme/statusTone.test.ts",
|
|
119
|
+
];
|
|
120
|
+
const tracked = packageSources();
|
|
121
|
+
expect(
|
|
122
|
+
tracked.length > 0,
|
|
123
|
+
"the scan found no package sources, so it proves nothing",
|
|
124
|
+
).toBeTruthy();
|
|
125
|
+
|
|
126
|
+
const offenders = tracked
|
|
127
|
+
.filter((file) => !ALLOWED_PALETTE_FILES.includes(file))
|
|
128
|
+
.flatMap((file) => statusPaletteIn(file, readFileSync(path.join(REPO, file), "utf8")));
|
|
129
|
+
expect(
|
|
130
|
+
offenders,
|
|
131
|
+
"the status palette has exactly one home outside Badge and statusTone — read the tone from " +
|
|
132
|
+
`src/theme/statusTone.ts instead:\n${offenders.join("\n")}`,
|
|
133
|
+
).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("no competing colour family stands in for the status palette anywhere in the package", () => {
|
|
137
|
+
const tracked = packageSources();
|
|
138
|
+
expect(
|
|
139
|
+
tracked.length > 0,
|
|
140
|
+
"the scan found no package sources, so it proves nothing",
|
|
141
|
+
).toBeTruthy();
|
|
142
|
+
|
|
143
|
+
const offenders = tracked.flatMap((file) =>
|
|
144
|
+
competingStatusPaletteIn(file, readFileSync(path.join(REPO, file), "utf8")),
|
|
145
|
+
);
|
|
146
|
+
expect(
|
|
147
|
+
offenders,
|
|
148
|
+
`a competing colour family stands in for the status palette:\n${offenders.join("\n")}`,
|
|
149
|
+
).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Every component module is part of the public surface.
|
|
154
|
+
*
|
|
155
|
+
* `src/index.ts` lists its exports one by one rather than re-exporting a
|
|
156
|
+
* directory, which is what keeps the promise auditable — and is also what lets
|
|
157
|
+
* a finished component sit in `src/components/` for months without ever
|
|
158
|
+
* reaching a consumer. `separator.tsx` did exactly that: written, imported by
|
|
159
|
+
* `field.tsx`, never exported, and nothing noticed because every other check
|
|
160
|
+
* here reads the files rather than the surface.
|
|
161
|
+
*
|
|
162
|
+
* A module that is deliberately internal has no home in `src/components/`; move
|
|
163
|
+
* it beside its one caller instead, and this gate stops asking about it.
|
|
164
|
+
*/
|
|
165
|
+
test("src/index.ts exports every component module in src/components", () => {
|
|
166
|
+
const listed = spawnSync("git", ["ls-files", "-z", "src/components"], {
|
|
167
|
+
cwd: REPO,
|
|
168
|
+
encoding: "utf8",
|
|
169
|
+
});
|
|
170
|
+
expect(listed.status, `git ls-files failed: ${listed.stderr}`).toBe(0);
|
|
171
|
+
|
|
172
|
+
const modules = listed.stdout
|
|
173
|
+
.split("\0")
|
|
174
|
+
.filter((name) => name.endsWith(".tsx") && !name.includes(".test."))
|
|
175
|
+
.map((name) => path.basename(name, ".tsx"));
|
|
176
|
+
expect(
|
|
177
|
+
modules.length > 0,
|
|
178
|
+
"no component modules were found, so this proves nothing",
|
|
179
|
+
).toBeTruthy();
|
|
180
|
+
|
|
181
|
+
const surface = read("src/index.ts");
|
|
182
|
+
const missing = modules.filter((name) => !surface.includes(`from "./components/${name}.js"`));
|
|
183
|
+
expect(
|
|
184
|
+
missing,
|
|
185
|
+
"these components exist but no consumer can import them — add an export to src/index.ts, " +
|
|
186
|
+
`or move the module next to its only caller:\n${missing.join("\n")}`,
|
|
187
|
+
).toEqual([]);
|
|
188
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { expect, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
test("foundationTokenNames resolves the stylesheet from the built package", () => {
|
|
6
|
+
const script = `import("./dist/gates/index.js").then((m) => { const n = m.foundationTokenNames(); if (!n.includes("background")) { throw new Error("missing token: " + n.join(",")); } console.log("ok"); });`;
|
|
7
|
+
const out = execFileSync(process.execPath, ["--input-type=module", "-e", script], {
|
|
8
|
+
encoding: "utf8",
|
|
9
|
+
});
|
|
10
|
+
expect(out.trim()).toBe("ok");
|
|
11
|
+
});
|