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