@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.
Files changed (135) hide show
  1. package/README.md +19 -7
  2. package/dist/{primitives → components}/alert.js +1 -1
  3. package/dist/{primitives → components}/badge.js +1 -1
  4. package/dist/{primitives → components}/button.d.ts +1 -1
  5. package/dist/{primitives → components}/button.js +4 -1
  6. package/dist/{primitives → components}/card.d.ts +1 -1
  7. package/dist/{primitives → components}/card.js +2 -2
  8. package/dist/{primitives → components}/combobox.js +4 -4
  9. package/dist/{primitives → components}/dialog.js +1 -1
  10. package/dist/{primitives → components}/dropdown-menu.js +12 -7
  11. package/dist/{primitives → components}/field.js +2 -4
  12. package/dist/{primitives → components}/input-group.js +1 -1
  13. package/dist/{primitives → components}/input.js +1 -1
  14. package/dist/{primitives → components}/label.js +1 -1
  15. package/dist/components/progress.js +7 -0
  16. package/dist/{primitives → components}/select.d.ts +3 -1
  17. package/dist/{primitives → components}/select.js +7 -4
  18. package/dist/{primitives → components}/separator.js +1 -1
  19. package/dist/{primitives → components}/sheet.js +1 -1
  20. package/dist/{primitives → components}/skeleton.js +1 -1
  21. package/dist/{primitives → components}/sonner.js +17 -12
  22. package/dist/{primitives → components}/table.d.ts +1 -1
  23. package/dist/{primitives → components}/table.js +3 -3
  24. package/dist/{primitives → components}/tabs.js +1 -1
  25. package/dist/{primitives → components}/textarea.js +1 -1
  26. package/dist/{primitives → components}/tooltip.js +1 -1
  27. package/dist/gates/index.d.ts +28 -0
  28. package/dist/gates/index.js +142 -0
  29. package/dist/index.d.ts +31 -32
  30. package/dist/index.js +32 -33
  31. package/dist/{tokens.d.ts → theme/tokens.d.ts} +5 -5
  32. package/dist/{tokens.js → theme/tokens.js} +12 -12
  33. package/package.json +23 -12
  34. package/{shadcn → src/components}/alert.tsx +14 -21
  35. package/src/{primitives → components}/badge.tsx +15 -21
  36. package/src/{primitives → components}/button.tsx +13 -10
  37. package/{shadcn → src/components}/card.tsx +16 -31
  38. package/src/{primitives → components}/combobox.test.tsx +15 -2
  39. package/src/{primitives → components}/combobox.tsx +45 -74
  40. package/src/{primitives/primitives.test.tsx → components/components.test.tsx} +106 -69
  41. package/src/{primitives → components}/dialog.tsx +31 -54
  42. package/src/{primitives → components}/dropdown-menu.tsx +51 -65
  43. package/src/{primitives → components}/field.tsx +48 -62
  44. package/src/{primitives → components}/input-group.tsx +37 -49
  45. package/{shadcn → src/components}/input.tsx +5 -5
  46. package/{shadcn → src/components}/label.tsx +7 -10
  47. package/src/{primitives → components}/progress.tsx +7 -6
  48. package/{shadcn → src/components}/select.tsx +40 -40
  49. package/{shadcn → src/components}/separator.tsx +7 -7
  50. package/src/{primitives → components}/sheet.tsx +27 -44
  51. package/{shadcn → src/components}/skeleton.tsx +3 -3
  52. package/src/{primitives → components}/sonner.test.tsx +12 -4
  53. package/src/components/sonner.tsx +67 -0
  54. package/{shadcn → src/components}/table.tsx +17 -44
  55. package/{shadcn → src/components}/tabs.tsx +16 -26
  56. package/{shadcn → src/components}/textarea.tsx +5 -5
  57. package/{shadcn → src/components}/tooltip.tsx +12 -16
  58. package/src/gates/design.test.ts +188 -0
  59. package/src/gates/index.test.ts +11 -0
  60. package/src/gates/index.ts +165 -0
  61. package/src/gates/tokens.test.ts +215 -0
  62. package/src/harness.test.tsx +1 -1
  63. package/src/index.ts +63 -33
  64. package/src/theme/imports.test.ts +160 -0
  65. package/src/{statusTone.test.ts → theme/statusTone.test.ts} +5 -1
  66. package/src/{styles.css → theme/styles.css} +45 -31
  67. package/src/theme/tailwind.css +646 -0
  68. package/src/{tokens.test.ts → theme/tokens.test.ts} +30 -23
  69. package/src/{tokens.ts → theme/tokens.ts} +12 -12
  70. package/components.json +0 -13
  71. package/dist/lib/button.d.ts +0 -1
  72. package/dist/lib/button.js +0 -4
  73. package/dist/lib/cn.d.ts +0 -18
  74. package/dist/lib/cn.js +0 -21
  75. package/dist/lib/menu.d.ts +0 -1
  76. package/dist/lib/menu.js +0 -16
  77. package/dist/lib/progress.d.ts +0 -3
  78. package/dist/lib/progress.js +0 -6
  79. package/dist/lib/select.d.ts +0 -1
  80. package/dist/lib/select.js +0 -3
  81. package/dist/primitives/progress.js +0 -7
  82. package/gates/canonical.test.mjs +0 -71
  83. package/gates/extensions.test.mjs +0 -373
  84. package/gates/index.d.mts +0 -51
  85. package/gates/index.mjs +0 -514
  86. package/gates/tokens.test.mjs +0 -276
  87. package/shadcn/README.md +0 -13
  88. package/shadcn/badge.tsx +0 -49
  89. package/shadcn/button.tsx +0 -67
  90. package/shadcn/combobox.tsx +0 -299
  91. package/shadcn/dialog.tsx +0 -166
  92. package/shadcn/dropdown-menu.tsx +0 -267
  93. package/shadcn/field.tsx +0 -236
  94. package/shadcn/input-group.tsx +0 -154
  95. package/shadcn/progress.tsx +0 -29
  96. package/shadcn/sheet.tsx +0 -145
  97. package/shadcn/sonner.tsx +0 -49
  98. package/src/lib/button.ts +0 -4
  99. package/src/lib/cn.test.ts +0 -33
  100. package/src/lib/cn.ts +0 -23
  101. package/src/lib/menu.ts +0 -16
  102. package/src/lib/progress.ts +0 -6
  103. package/src/lib/select.ts +0 -4
  104. package/src/primitives/alert.tsx +0 -76
  105. package/src/primitives/card.tsx +0 -103
  106. package/src/primitives/input.tsx +0 -19
  107. package/src/primitives/label.tsx +0 -22
  108. package/src/primitives/select.tsx +0 -190
  109. package/src/primitives/separator.tsx +0 -28
  110. package/src/primitives/skeleton.tsx +0 -13
  111. package/src/primitives/sonner.tsx +0 -65
  112. package/src/primitives/table.tsx +0 -114
  113. package/src/primitives/tabs.tsx +0 -88
  114. package/src/primitives/textarea.tsx +0 -18
  115. package/src/primitives/tooltip.tsx +0 -57
  116. /package/dist/{primitives → components}/alert.d.ts +0 -0
  117. /package/dist/{primitives → components}/badge.d.ts +0 -0
  118. /package/dist/{primitives → components}/combobox.d.ts +0 -0
  119. /package/dist/{primitives → components}/dialog.d.ts +0 -0
  120. /package/dist/{primitives → components}/dropdown-menu.d.ts +0 -0
  121. /package/dist/{primitives → components}/field.d.ts +0 -0
  122. /package/dist/{primitives → components}/input-group.d.ts +0 -0
  123. /package/dist/{primitives → components}/input.d.ts +0 -0
  124. /package/dist/{primitives → components}/label.d.ts +0 -0
  125. /package/dist/{primitives → components}/progress.d.ts +0 -0
  126. /package/dist/{primitives → components}/separator.d.ts +0 -0
  127. /package/dist/{primitives → components}/sheet.d.ts +0 -0
  128. /package/dist/{primitives → components}/skeleton.d.ts +0 -0
  129. /package/dist/{primitives → components}/sonner.d.ts +0 -0
  130. /package/dist/{primitives → components}/tabs.d.ts +0 -0
  131. /package/dist/{primitives → components}/textarea.d.ts +0 -0
  132. /package/dist/{primitives → components}/tooltip.d.ts +0 -0
  133. /package/dist/{statusTone.d.ts → theme/statusTone.d.ts} +0 -0
  134. /package/dist/{statusTone.js → theme/statusTone.js} +0 -0
  135. /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(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("light");
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(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("dark");
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(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("light");
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(container.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("dark");
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 "@/lib/cn"
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 "@/lib/cn"
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 "@/lib/cn"
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 "@/lib/cn"
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
+ });