@robomous/ui-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +38 -0
- package/components.json +13 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +47 -0
- package/dist/lib/button.d.ts +1 -0
- package/dist/lib/button.js +4 -0
- package/dist/lib/cn.d.ts +18 -0
- package/dist/lib/cn.js +21 -0
- package/dist/lib/menu.d.ts +1 -0
- package/dist/lib/menu.js +16 -0
- package/dist/lib/progress.d.ts +3 -0
- package/dist/lib/progress.js +6 -0
- package/dist/lib/select.d.ts +1 -0
- package/dist/lib/select.js +3 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.js +27 -0
- package/dist/primitives/badge.d.ts +9 -0
- package/dist/primitives/badge.js +28 -0
- package/dist/primitives/button.d.ts +10 -0
- package/dist/primitives/button.js +35 -0
- package/dist/primitives/card.d.ts +11 -0
- package/dist/primitives/card.js +24 -0
- package/dist/primitives/combobox.d.ts +24 -0
- package/dist/primitives/combobox.js +58 -0
- package/dist/primitives/dialog.d.ts +17 -0
- package/dist/primitives/dialog.js +36 -0
- package/dist/primitives/dropdown-menu.d.ts +29 -0
- package/dist/primitives/dropdown-menu.js +50 -0
- package/dist/primitives/field.d.ts +24 -0
- package/dist/primitives/field.js +67 -0
- package/dist/primitives/input-group.d.ts +16 -0
- package/dist/primitives/input-group.js +56 -0
- package/dist/primitives/input.d.ts +3 -0
- package/dist/primitives/input.js +6 -0
- package/dist/primitives/label.d.ts +4 -0
- package/dist/primitives/label.js +7 -0
- package/dist/primitives/progress.d.ts +4 -0
- package/dist/primitives/progress.js +7 -0
- package/dist/primitives/select.d.ts +15 -0
- package/dist/primitives/select.js +35 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.js +8 -0
- package/dist/primitives/sheet.d.ts +14 -0
- package/dist/primitives/sheet.js +36 -0
- package/dist/primitives/skeleton.d.ts +2 -0
- package/dist/primitives/skeleton.js +6 -0
- package/dist/primitives/sonner.d.ts +4 -0
- package/dist/primitives/sonner.js +40 -0
- package/dist/primitives/table.d.ts +10 -0
- package/dist/primitives/table.js +27 -0
- package/dist/primitives/tabs.d.ts +11 -0
- package/dist/primitives/tabs.js +28 -0
- package/dist/primitives/textarea.d.ts +3 -0
- package/dist/primitives/textarea.js +6 -0
- package/dist/primitives/tooltip.d.ts +7 -0
- package/dist/primitives/tooltip.js +17 -0
- package/dist/statusTone.d.ts +37 -0
- package/dist/statusTone.js +38 -0
- package/dist/tokens.d.ts +26 -0
- package/dist/tokens.js +96 -0
- package/gates/canonical.test.mjs +71 -0
- package/gates/extensions.test.mjs +373 -0
- package/gates/index.d.mts +51 -0
- package/gates/index.mjs +514 -0
- package/gates/tokens.test.mjs +276 -0
- package/package.json +60 -0
- package/shadcn/README.md +13 -0
- package/shadcn/alert.tsx +76 -0
- package/shadcn/badge.tsx +49 -0
- package/shadcn/button.tsx +67 -0
- package/shadcn/card.tsx +103 -0
- package/shadcn/combobox.tsx +299 -0
- package/shadcn/dialog.tsx +166 -0
- package/shadcn/dropdown-menu.tsx +267 -0
- package/shadcn/field.tsx +236 -0
- package/shadcn/input-group.tsx +154 -0
- package/shadcn/input.tsx +19 -0
- package/shadcn/label.tsx +22 -0
- package/shadcn/progress.tsx +29 -0
- package/shadcn/select.tsx +190 -0
- package/shadcn/separator.tsx +28 -0
- package/shadcn/sheet.tsx +145 -0
- package/shadcn/skeleton.tsx +13 -0
- package/shadcn/sonner.tsx +49 -0
- package/shadcn/table.tsx +114 -0
- package/shadcn/tabs.tsx +88 -0
- package/shadcn/textarea.tsx +18 -0
- package/shadcn/tooltip.tsx +57 -0
- package/src/harness.test.tsx +37 -0
- package/src/index.ts +119 -0
- package/src/lib/button.ts +4 -0
- package/src/lib/cn.test.ts +33 -0
- package/src/lib/cn.ts +23 -0
- package/src/lib/menu.ts +16 -0
- package/src/lib/progress.ts +6 -0
- package/src/lib/select.ts +4 -0
- package/src/primitives/alert.tsx +76 -0
- package/src/primitives/badge.tsx +57 -0
- package/src/primitives/button.tsx +67 -0
- package/src/primitives/card.tsx +103 -0
- package/src/primitives/combobox.test.tsx +38 -0
- package/src/primitives/combobox.tsx +299 -0
- package/src/primitives/dialog.tsx +166 -0
- package/src/primitives/dropdown-menu.tsx +267 -0
- package/src/primitives/field.tsx +236 -0
- package/src/primitives/input-group.tsx +154 -0
- package/src/primitives/input.tsx +19 -0
- package/src/primitives/label.tsx +22 -0
- package/src/primitives/primitives.test.tsx +440 -0
- package/src/primitives/progress.tsx +29 -0
- package/src/primitives/select.tsx +190 -0
- package/src/primitives/separator.tsx +28 -0
- package/src/primitives/sheet.tsx +145 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/sonner.test.tsx +41 -0
- package/src/primitives/sonner.tsx +65 -0
- package/src/primitives/table.tsx +114 -0
- package/src/primitives/tabs.tsx +88 -0
- package/src/primitives/textarea.tsx +18 -0
- package/src/primitives/tooltip.tsx +57 -0
- package/src/statusTone.test.ts +20 -0
- package/src/statusTone.ts +52 -0
- package/src/styles.css +204 -0
- package/src/tokens.test.ts +285 -0
- package/src/tokens.ts +102 -0
|
@@ -0,0 +1,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,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
|
+
}
|
package/src/lib/menu.ts
ADDED
|
@@ -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
|
+
});
|