@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,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"
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,37 @@
1
+ /**
2
+ * A test boundary is a clean one: nothing a test mounted is still doing work
3
+ * when the next test begins, or when the file ends.
4
+ *
5
+ * Radix's focus scope returns focus from a `setTimeout(…, 0)` scheduled by its
6
+ * unmount cleanup — after `cleanup()` has already run. Left alone, that timer
7
+ * races the next test, and after a file's last test it races vitest's
8
+ * environment teardown; when teardown wins, it dispatches a `CustomEvent` from
9
+ * Node's realm into a closed jsdom document and the run ends with
10
+ * `Errors 1 error` under a green test count. `vitest.setup.ts` drains one
11
+ * macrotask after every `cleanup()` so the timer fires while the document it
12
+ * targets still exists. These two tests hold that property: the first ends
13
+ * with a dialog mounted, the second proves its unmount had finished before the
14
+ * second began. Delete the drain and the second fails.
15
+ */
16
+
17
+ import { render } from "@testing-library/react";
18
+ import { expect, it, vi } from "vitest";
19
+
20
+ import { Dialog, DialogContent, DialogTitle } from "./primitives/dialog";
21
+
22
+ const unmountFocusReturned = vi.fn();
23
+
24
+ it("ends with a focus-trapping dialog still mounted", () => {
25
+ render(
26
+ <Dialog open>
27
+ <DialogContent onCloseAutoFocus={unmountFocusReturned}>
28
+ <DialogTitle>still open when the test ends</DialogTitle>
29
+ </DialogContent>
30
+ </Dialog>,
31
+ );
32
+ expect(unmountFocusReturned).not.toHaveBeenCalled();
33
+ });
34
+
35
+ it("and the next test starts only after that dialog's unmount timer has run", () => {
36
+ expect(unmountFocusReturned).toHaveBeenCalledTimes(1);
37
+ });
package/src/index.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `@robomous/ui-core` — the Robomous design system.
3
+ *
4
+ * Extracted from Robomous/VisionSet. UI primitives: Radix behaviour under
5
+ * shadcn Nova styling, iconed with lucide; the foundation design tokens; and
6
+ * the status-tone vocabulary. A consumer imports exactly:
7
+ *
8
+ * ```ts
9
+ * import "@robomous/ui-core/styles.css"; // once, in the app's entry
10
+ * import { Button, Card } from "@robomous/ui-core";
11
+ * ```
12
+ *
13
+ * The public surface is listed explicitly rather than `export *`, so what this
14
+ * package promises stays auditable. The gates that keep the primitives
15
+ * canonical are published too: `import { ... } from "@robomous/ui-core/gates"`.
16
+ */
17
+
18
+ // The design tokens, and their prose contract in DESIGN.md.
19
+ export { cssVar, DARK_THEME, LIGHT_THEME, THEME } from "./tokens.js";
20
+
21
+ export { inlineLink } from "./lib/button.js";
22
+ export { cn } from "./lib/cn.js";
23
+ export { menuSurface } from "./lib/menu.js";
24
+ export { twoLineTrigger } from "./lib/select.js";
25
+ export { progressAria } from "./lib/progress.js";
26
+
27
+ // The one home for status colour outside the Badge.
28
+ export { STATUS_INK, TONE_BORDER, TONE_FILL, type StatusTone } from "./statusTone.js";
29
+
30
+ // Primitives — Radix behaviour under shadcn Nova styling, iconed with lucide.
31
+ export { Button, buttonVariants } from "./primitives/button.js";
32
+ export {
33
+ Card,
34
+ CardAction,
35
+ CardContent,
36
+ CardDescription,
37
+ CardFooter,
38
+ CardHeader,
39
+ CardTitle,
40
+ } from "./primitives/card.js";
41
+ export { Input } from "./primitives/input.js";
42
+ export { Textarea } from "./primitives/textarea.js";
43
+ export { Label } from "./primitives/label.js";
44
+ export {
45
+ Field,
46
+ FieldContent,
47
+ FieldDescription,
48
+ FieldError,
49
+ FieldGroup,
50
+ FieldLabel,
51
+ FieldLegend,
52
+ FieldSeparator,
53
+ FieldSet,
54
+ FieldTitle,
55
+ } from "./primitives/field.js";
56
+ export {
57
+ InputGroup,
58
+ InputGroupAddon,
59
+ InputGroupButton,
60
+ InputGroupInput,
61
+ InputGroupText,
62
+ InputGroupTextarea,
63
+ } from "./primitives/input-group.js";
64
+ export {
65
+ Combobox,
66
+ ComboboxChip,
67
+ ComboboxChips,
68
+ ComboboxChipsInput,
69
+ ComboboxCollection,
70
+ ComboboxContent,
71
+ ComboboxEmpty,
72
+ ComboboxGroup,
73
+ ComboboxInput,
74
+ ComboboxItem,
75
+ ComboboxLabel,
76
+ ComboboxList,
77
+ ComboboxSeparator,
78
+ ComboboxTrigger,
79
+ ComboboxValue,
80
+ useComboboxAnchor,
81
+ } from "./primitives/combobox.js";
82
+ export { Badge, badgeVariants } from "./primitives/badge.js";
83
+ export { Alert, AlertAction, AlertDescription, AlertTitle } from "./primitives/alert.js";
84
+ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./primitives/dialog.js";
85
+ export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./primitives/sheet.js";
86
+ export { Tabs, TabsContent, TabsList, tabsListVariants, TabsTrigger } from "./primitives/tabs.js";
87
+ export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "./primitives/select.js";
88
+ export {
89
+ DropdownMenu,
90
+ DropdownMenuCheckboxItem,
91
+ DropdownMenuContent,
92
+ DropdownMenuGroup,
93
+ DropdownMenuItem,
94
+ DropdownMenuLabel,
95
+ DropdownMenuPortal,
96
+ DropdownMenuRadioGroup,
97
+ DropdownMenuRadioItem,
98
+ DropdownMenuSeparator,
99
+ DropdownMenuShortcut,
100
+ DropdownMenuSub,
101
+ DropdownMenuSubContent,
102
+ DropdownMenuSubTrigger,
103
+ DropdownMenuTrigger,
104
+ } from "./primitives/dropdown-menu.js";
105
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./primitives/tooltip.js";
106
+ export { Progress } from "./primitives/progress.js";
107
+ export { Skeleton } from "./primitives/skeleton.js";
108
+ export { Toaster } from "./primitives/sonner.js";
109
+ export { toast } from "sonner";
110
+ export {
111
+ Table,
112
+ TableBody,
113
+ TableCaption,
114
+ TableCell,
115
+ TableFooter,
116
+ TableHead,
117
+ TableHeader,
118
+ TableRow,
119
+ } from "./primitives/table.js";
@@ -0,0 +1,4 @@
1
+ // A link button is inline prose, not a boxed control: the canonical variant keeps
2
+ // the button's height and padding, so the sites that sit inside a sentence or a
3
+ // table cell hand them back.
4
+ export const inlineLink = "h-auto p-0";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `cn` — the generic merge cases. No custom `tailwind-merge` configuration
3
+ * lives behind this one (the shadcn preset's names do not collide with a
4
+ * stock class group the way v1's `--text-*` scale did), so there is nothing
5
+ * scale-specific left to pin here.
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import { cn } from "./cn";
11
+
12
+ describe("cn", () => {
13
+ it("lets a caller override a stock utility", () => {
14
+ expect(cn("px-4", "px-6")).toBe("px-6");
15
+ expect(cn("rounded-md", "rounded-full")).toBe("rounded-full");
16
+ });
17
+
18
+ it("keeps utilities that do not conflict", () => {
19
+ expect(cn("flex", "items-center").split(" ").sort()).toEqual(["flex", "items-center"]);
20
+ });
21
+
22
+ it("drops falsy input rather than emitting it", () => {
23
+ expect(cn("flex", false, undefined, null, "gap-2")).toBe("flex gap-2");
24
+ });
25
+
26
+ it("keeps a semantic colour beside a layout utility", () => {
27
+ expect(cn("bg-primary", "rounded-lg").split(" ").sort()).toEqual(["bg-primary", "rounded-lg"]);
28
+ });
29
+
30
+ it("lets a caller override a semantic background colour", () => {
31
+ expect(cn("bg-primary", "bg-destructive")).toBe("bg-destructive");
32
+ });
33
+ });
package/src/lib/cn.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `cn` — merge class names, last conflicting utility wins.
3
+ *
4
+ * `clsx` handles the conditionals; `tailwind-merge` handles the part a naive
5
+ * join gets wrong. Without it `cn("px-4", props.className)` with `px-6`
6
+ * passed in emits both, and which one applies is decided by the order
7
+ * Tailwind happened to write them into the stylesheet — so a caller's
8
+ * override works or does not depending on a rule nobody can see. That is why
9
+ * the `className` prop on every primitive here is a real extension point
10
+ * rather than a suggestion.
11
+ *
12
+ * The shadcn preset's own vocabulary needs no custom `tailwind-merge`
13
+ * configuration — unlike v1's `--text-*` scale, none of its names collide
14
+ * with a stock Tailwind class group — so this is the bare merge, the same
15
+ * shape the CLI scratch's own `lib/utils.ts` ships.
16
+ */
17
+
18
+ import { clsx, type ClassValue } from "clsx";
19
+ import { twMerge } from "tailwind-merge";
20
+
21
+ export function cn(...inputs: readonly ClassValue[]): string {
22
+ return twMerge(clsx(inputs));
23
+ }
@@ -0,0 +1,16 @@
1
+ // What every `DropdownMenuContent` call site adds to the canonical surface, in
2
+ // one constant because both halves are the same rule — a menu that behaves like
3
+ // a menu (DESIGN.md, Motion).
4
+ //
5
+ // `data-closed:animate-none!`: a menu surface leaves on the frame it is
6
+ // dismissed. While Radix runs an exit animation the dismissable layer stays
7
+ // mounted, and a press that should open the next menu is swallowed as the
8
+ // dismissal of this one.
9
+ //
10
+ // `w-auto`: a menu sizes to its items, not to its trigger. Canonical carries
11
+ // `w-(--radix-dropdown-menu-trigger-width) min-w-32`, which pins the surface to
12
+ // the trigger — behind an icon-sized button that is the 128px floor, and every
13
+ // item longer than that wraps (the row actions on ModelsScreen, the Annotate
14
+ // picker). `min-w-32` survives as the floor it was meant to be, because
15
+ // tailwind-merge replaces only the `w-*` in the same utility group.
16
+ export const menuSurface = "data-closed:animate-none! w-auto";
@@ -0,0 +1,6 @@
1
+ // Canonical `Progress` reads `value` only to size the indicator and never
2
+ // forwards it to Radix's `Root`, so the number a screen reader would announce
3
+ // is otherwise never rendered at all — a caller that needs it says so again here.
4
+ export function progressAria(value: number): { readonly "aria-valuenow": number } {
5
+ return { "aria-valuenow": value };
6
+ }
@@ -0,0 +1,4 @@
1
+ // A two-line option needs the closed trigger to grow and to stop clamping its
2
+ // value; both overrides name canonical's own variants so the merge replaces them.
3
+ export const twoLineTrigger =
4
+ "data-[size=default]:h-auto min-h-8 *:data-[slot=select-value]:line-clamp-none";
@@ -0,0 +1,76 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../lib/cn.js"
5
+
6
+ const alertVariants = cva(
7
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
67
+ return (
68
+ <div
69
+ data-slot="alert-action"
70
+ className={cn("absolute top-2 right-2", className)}
71
+ {...props}
72
+ />
73
+ )
74
+ }
75
+
76
+ export { Alert, AlertTitle, AlertDescription, AlertAction }
@@ -0,0 +1,57 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "../lib/cn.js"
6
+
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ success:
18
+ "bg-emerald-500/10 text-emerald-700 focus-visible:ring-emerald-500/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:focus-visible:ring-emerald-400/40 [a]:hover:bg-emerald-500/20 dark:[a]:hover:bg-emerald-400/20",
19
+ warning:
20
+ "bg-amber-500/10 text-amber-700 focus-visible:ring-amber-500/20 dark:bg-amber-400/10 dark:text-amber-400 dark:focus-visible:ring-amber-400/40 [a]:hover:bg-amber-500/20 dark:[a]:hover:bg-amber-400/20",
21
+ info:
22
+ "bg-sky-500/10 text-sky-700 focus-visible:ring-sky-500/20 dark:bg-sky-400/10 dark:text-sky-400 dark:focus-visible:ring-sky-400/40 [a]:hover:bg-sky-500/20 dark:[a]:hover:bg-sky-400/20",
23
+ quiet:
24
+ "bg-muted text-muted-foreground [a]:hover:bg-muted/80",
25
+ outline:
26
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
27
+ ghost:
28
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
29
+ link: "text-primary underline-offset-4 hover:underline",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ },
35
+ }
36
+ )
37
+
38
+ function Badge({
39
+ className,
40
+ variant = "default",
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"span"> &
44
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
45
+ const Comp = asChild ? Slot.Root : "span"
46
+
47
+ return (
48
+ <Comp
49
+ data-slot="badge"
50
+ data-variant={variant}
51
+ className={cn(badgeVariants({ variant }), className)}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ export { Badge, badgeVariants }
@@ -0,0 +1,67 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "../lib/cn.js"
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
13
+ outline:
14
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
15
+ secondary:
16
+ "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
17
+ ghost:
18
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
19
+ destructive:
20
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default:
25
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
28
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
29
+ icon: "size-8",
30
+ "icon-xs":
31
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
32
+ "icon-sm":
33
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
34
+ "icon-lg": "size-9",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ size: "default",
40
+ },
41
+ }
42
+ )
43
+
44
+ function Button({
45
+ className,
46
+ variant = "default",
47
+ size = "default",
48
+ asChild = false,
49
+ ...props
50
+ }: React.ComponentProps<"button"> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ asChild?: boolean
53
+ }) {
54
+ const Comp = asChild ? Slot.Root : "button"
55
+
56
+ return (
57
+ <Comp
58
+ data-slot="button"
59
+ data-variant={variant}
60
+ data-size={size}
61
+ className={cn(buttonVariants({ variant, size, className }))}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export { Button, buttonVariants }
@@ -0,0 +1,103 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../lib/cn.js"
4
+
5
+ function Card({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
10
+ return (
11
+ <div
12
+ data-slot="card"
13
+ data-size={size}
14
+ className={cn(
15
+ "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="card-title"
40
+ className={cn(
41
+ "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="card-description"
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="card-action"
63
+ className={cn(
64
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="card-content"
76
+ className={cn("px-(--card-spacing)", className)}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="card-footer"
86
+ className={cn(
87
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ export {
96
+ Card,
97
+ CardHeader,
98
+ CardFooter,
99
+ CardTitle,
100
+ CardAction,
101
+ CardDescription,
102
+ CardContent,
103
+ }
@@ -0,0 +1,38 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it } from "vitest";
4
+ import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "./combobox";
5
+
6
+ const FRUIT = ["apple", "banana", "cherry"];
7
+
8
+ function Demo() {
9
+ return (
10
+ <Combobox items={FRUIT}>
11
+ <ComboboxInput placeholder="Pick" aria-label="Fruit" />
12
+ <ComboboxContent>
13
+ <ComboboxEmpty>No matches</ComboboxEmpty>
14
+ <ComboboxList>{(item: string) => <ComboboxItem key={item} value={item}>{item}</ComboboxItem>}</ComboboxList>
15
+ </ComboboxContent>
16
+ </Combobox>
17
+ );
18
+ }
19
+
20
+ describe("combobox", () => {
21
+ it("filters as you type and offers the survivors as options", async () => {
22
+ const user = userEvent.setup();
23
+ render(<Demo />);
24
+ const input = screen.getByRole("combobox", { name: "Fruit" });
25
+ await user.type(input, "an");
26
+ expect(await screen.findByRole("option", { name: "banana" })).toBeTruthy();
27
+ expect(screen.queryByRole("option", { name: "cherry" })).toBeNull();
28
+ });
29
+ it("closes on Escape", async () => {
30
+ const user = userEvent.setup();
31
+ render(<Demo />);
32
+ const input = screen.getByRole("combobox", { name: "Fruit" });
33
+ await user.type(input, "a");
34
+ await screen.findByRole("option", { name: "apple" });
35
+ await user.keyboard("{Escape}");
36
+ expect(screen.queryByRole("option", { name: "apple" })).toBeNull();
37
+ });
38
+ });