@kinetixui/ui 0.23.3 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/dist/index.d.ts +212 -31
- package/dist/index.js +1466 -1118
- package/dist/kx-src-hash.json +3 -0
- package/package.json +7 -7
- package/src/components/accordion.tsx +1 -1
- package/src/components/alert-dialog.tsx +7 -5
- package/src/components/avatar.tsx +4 -1
- package/src/components/banner.tsx +9 -3
- package/src/components/breadcrumb.tsx +1 -1
- package/src/components/button.tsx +1 -1
- package/src/components/card.tsx +77 -6
- package/src/components/checkbox.tsx +42 -8
- package/src/components/collapsible.tsx +41 -1
- package/src/components/command.tsx +2 -2
- package/src/components/context-menu.tsx +21 -13
- package/src/components/dialog.tsx +5 -4
- package/src/components/direction-provider.tsx +85 -1
- package/src/components/drawer.tsx +62 -18
- package/src/components/dropdown-menu.tsx +12 -4
- package/src/components/file-upload.tsx +1 -1
- package/src/components/hover-card.tsx +1 -1
- package/src/components/inform.tsx +9 -3
- package/src/components/input-group.tsx +27 -5
- package/src/components/input-otp.tsx +48 -5
- package/src/components/input.tsx +20 -3
- package/src/components/json-viewer.tsx +1 -1
- package/src/components/menubar.tsx +21 -15
- package/src/components/modal.tsx +3 -2
- package/src/components/multi-select.tsx +14 -5
- package/src/components/native-select.tsx +10 -2
- package/src/components/navigation-bar.tsx +1 -1
- package/src/components/navigation-menu.tsx +6 -6
- package/src/components/number-input.tsx +28 -10
- package/src/components/pagination.tsx +2 -2
- package/src/components/popover.tsx +29 -5
- package/src/components/radio-group.tsx +23 -8
- package/src/components/segmented-control.tsx +20 -4
- package/src/components/select.tsx +25 -8
- package/src/components/sheet.tsx +4 -3
- package/src/components/spinner.tsx +1 -1
- package/src/components/switch.tsx +31 -8
- package/src/components/table.tsx +121 -8
- package/src/components/tabs.tsx +29 -4
- package/src/components/tag.tsx +1 -1
- package/src/components/textarea.tsx +20 -3
- package/src/components/tooltip.tsx +14 -4
- package/src/components/tour.tsx +22 -7
- package/src/components/tree-view.tsx +1 -1
- package/src/components-behavior.test.tsx +74 -0
- package/src/components-direction-portal.test.tsx +482 -0
- package/src/components-form.test.tsx +435 -0
- package/src/components-icons.test.tsx +197 -0
- package/src/components-keyboard.test.tsx +542 -2
- package/src/components-overlay.test.tsx +381 -0
- package/src/components-rtl.test.tsx +258 -0
- package/src/components-selection.test.tsx +207 -0
- package/src/components-table-scroll.test.tsx +363 -0
- package/src/index.ts +2 -0
- package/src/navigation-menu-focus.test.tsx +35 -0
- package/src/stories/Card.stories.tsx +81 -1
- package/src/stories/Checkbox.stories.tsx +23 -1
- package/src/stories/ContextMenu.stories.tsx +1 -1
- package/src/stories/Input.stories.tsx +34 -0
- package/src/stories/InputGroup.stories.tsx +96 -2
- package/src/stories/InputOTP.stories.tsx +24 -1
- package/src/stories/MultiSelect.stories.tsx +25 -1
- package/src/stories/NativeSelect.stories.tsx +24 -1
- package/src/stories/NumberInput.stories.tsx +20 -1
- package/src/stories/Progress.stories.tsx +1 -1
- package/src/stories/RadioGroup.stories.tsx +33 -1
- package/src/stories/SegmentedControl.stories.tsx +21 -1
- package/src/stories/Select.stories.tsx +28 -1
- package/src/stories/Slider.stories.tsx +1 -1
- package/src/stories/Switch.stories.tsx +37 -1
- package/src/stories/Table.stories.tsx +35 -1
- package/src/stories/Tabs.stories.tsx +27 -2
- package/src/stories/Textarea.stories.tsx +34 -0
- package/src/usage/icons.tsx +60 -0
- package/tailwind.config.ts +118 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinetixui/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
|
|
6
6
|
"keywords": [
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@dnd-kit/sortable": "^10.0.0",
|
|
45
45
|
"@dnd-kit/utilities": "^3.2.2",
|
|
46
46
|
"@hookform/resolvers": "^5.9.1",
|
|
47
|
-
"@kinetixui/tokens": "0.
|
|
47
|
+
"@kinetixui/tokens": "0.24.0",
|
|
48
48
|
"@radix-ui/react-accordion": "^1.2.20",
|
|
49
49
|
"@radix-ui/react-alert-dialog": "^1.1.23",
|
|
50
50
|
"@radix-ui/react-aspect-ratio": "^1.1.15",
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"date-fns": "^4.4.0",
|
|
80
80
|
"embla-carousel-react": "^8.6.0",
|
|
81
81
|
"input-otp": "^1.5.0",
|
|
82
|
-
"lucide-react": "^1.
|
|
82
|
+
"lucide-react": "^1.48.0",
|
|
83
83
|
"react-day-picker": "^10.0.1",
|
|
84
84
|
"react-hook-form": "^7.88.0",
|
|
85
|
-
"react-resizable-panels": "^4.
|
|
85
|
+
"react-resizable-panels": "^4.13.3",
|
|
86
86
|
"recharts": "^3.10.1",
|
|
87
87
|
"sonner": "^2.0.8",
|
|
88
88
|
"tailwind-merge": "^3.7.0",
|
|
@@ -111,11 +111,11 @@
|
|
|
111
111
|
"tailwindcss": "^3.4.19",
|
|
112
112
|
"tsup": "^8.5.1",
|
|
113
113
|
"typescript": "^5.9.3",
|
|
114
|
-
"typescript-eslint": "^8.70.
|
|
115
|
-
"vitest": "^5.0.
|
|
114
|
+
"typescript-eslint": "^8.70.1",
|
|
115
|
+
"vitest": "^5.0.2"
|
|
116
116
|
},
|
|
117
117
|
"scripts": {
|
|
118
|
-
"build": "tsup src/index.ts --format esm --dts --external react --external react-dom",
|
|
118
|
+
"build": "tsup src/index.ts --format esm --dts --external react --external react-dom && node ../../scripts/ui-dist-stamp.mjs --write",
|
|
119
119
|
"lint": "eslint src",
|
|
120
120
|
"typecheck": "tsc --noEmit",
|
|
121
121
|
"test": "vitest run",
|
|
@@ -23,7 +23,7 @@ const AccordionTrigger = React.forwardRef<
|
|
|
23
23
|
<AccordionPrimitive.Trigger
|
|
24
24
|
ref={ref}
|
|
25
25
|
className={cn(
|
|
26
|
-
"flex flex-1 items-center justify-between py-4 text-sm font-medium font-sans transition-
|
|
26
|
+
"flex flex-1 items-center justify-between py-4 text-sm font-medium font-sans transition-colors duration-fast hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
27
27
|
className,
|
|
28
28
|
)}
|
|
29
29
|
{...props}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
|
+
import { withDirectionalPortal } from "./direction-provider";
|
|
6
7
|
import { buttonVariants } from "./button";
|
|
7
8
|
|
|
8
9
|
const AlertDialog = AlertDialogPrimitive.Root;
|
|
9
10
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
|
10
|
-
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
|
11
|
+
const AlertDialogPortal = withDirectionalPortal(AlertDialogPrimitive.Portal);
|
|
11
12
|
|
|
12
13
|
const AlertDialogOverlay = React.forwardRef<
|
|
13
14
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
@@ -16,7 +17,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|
|
16
17
|
<AlertDialogPrimitive.Overlay
|
|
17
18
|
ref={ref}
|
|
18
19
|
className={cn(
|
|
19
|
-
"fixed inset-0 z-overlay bg-
|
|
20
|
+
"fixed inset-0 z-overlay bg-scrim backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:duration-fast data-[state=open]:ease-enter data-[state=closed]:duration-instant data-[state=closed]:ease-exit",
|
|
20
21
|
className,
|
|
21
22
|
)}
|
|
22
23
|
{...props}
|
|
@@ -33,8 +34,9 @@ const AlertDialogContent = React.forwardRef<
|
|
|
33
34
|
<AlertDialogPrimitive.Content
|
|
34
35
|
ref={ref}
|
|
35
36
|
className={cn(
|
|
37
|
+
// rtl-ok: centering trick, direction-agnostic by construction — same decision as dialog.tsx
|
|
36
38
|
"fixed left-1/2 top-1/2 z-overlay grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
|
|
37
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
39
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:duration-fast data-[state=open]:ease-enter data-[state=closed]:duration-instant data-[state=closed]:ease-exit",
|
|
38
40
|
className,
|
|
39
41
|
)}
|
|
40
42
|
{...props}
|
|
@@ -44,13 +46,13 @@ const AlertDialogContent = React.forwardRef<
|
|
|
44
46
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
|
45
47
|
|
|
46
48
|
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
47
|
-
<div className={cn("flex flex-col space-y-2 text-center sm:text-
|
|
49
|
+
<div className={cn("flex flex-col space-y-2 text-center sm:text-start", className)} {...props} />
|
|
48
50
|
);
|
|
49
51
|
AlertDialogHeader.displayName = "AlertDialogHeader";
|
|
50
52
|
|
|
51
53
|
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
52
54
|
<div
|
|
53
|
-
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end
|
|
55
|
+
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
54
56
|
{...props}
|
|
55
57
|
/>
|
|
56
58
|
);
|
|
@@ -53,8 +53,11 @@ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
|
53
53
|
const visible = max ? items.slice(0, max) : items;
|
|
54
54
|
const overflow = max ? items.length - max : 0;
|
|
55
55
|
|
|
56
|
+
// rtl-ok: negative spacing IS the stack overlap; `gap` cannot be negative, and it mirrors naturally
|
|
57
|
+
const stack = "flex -space-x-2";
|
|
58
|
+
|
|
56
59
|
return (
|
|
57
|
-
<div ref={ref} className={cn(
|
|
60
|
+
<div ref={ref} className={cn(stack, className)} {...props}>
|
|
58
61
|
{visible.map((child, i) => (
|
|
59
62
|
<div key={i} className="rounded-full ring-2 ring-background">
|
|
60
63
|
{child}
|
|
@@ -48,12 +48,18 @@ export interface BannerProps
|
|
|
48
48
|
VariantProps<typeof bannerVariants> {
|
|
49
49
|
hideIcon?: boolean;
|
|
50
50
|
onDismiss?: () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Replaces the default dismiss mark (lucide's `X`) with your own icon — any icon library or SVG.
|
|
53
|
+
* The button keeps its accessible name ("Dismiss") and sizes any SVG inside it to 14px, so the icon
|
|
54
|
+
* needs no size, colour or aria attributes of its own. See /docs/icons.
|
|
55
|
+
*/
|
|
56
|
+
dismissIcon?: React.ReactNode;
|
|
51
57
|
/** optional CTA rendered inline after the message */
|
|
52
58
|
action?: { label: React.ReactNode; onClick?: () => void };
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
|
|
56
|
-
({ className, variant = "information", sticky, hideIcon, onDismiss, action, children, ...props }, ref) => {
|
|
62
|
+
({ className, variant = "information", sticky, hideIcon, onDismiss, dismissIcon, action, children, ...props }, ref) => {
|
|
57
63
|
const Icon = ICON[variant ?? "information"];
|
|
58
64
|
return (
|
|
59
65
|
<div ref={ref} className={cn(bannerVariants({ variant, sticky }), className)} {...props}>
|
|
@@ -75,9 +81,9 @@ const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
|
|
|
75
81
|
type="button"
|
|
76
82
|
onClick={onDismiss}
|
|
77
83
|
aria-label="Dismiss"
|
|
78
|
-
className="-m-0.5 shrink-0 rounded-[2px] p-0.5 opacity-muted outline-none transition-opacity hover:opacity-visible focus-visible:opacity-visible focus-visible:ring-1 focus-visible:ring-current"
|
|
84
|
+
className="-m-0.5 shrink-0 rounded-[2px] p-0.5 opacity-muted outline-none transition-opacity hover:opacity-visible focus-visible:opacity-visible focus-visible:ring-1 focus-visible:ring-current [&_svg]:size-3.5 [&_svg]:shrink-0"
|
|
79
85
|
>
|
|
80
|
-
<X className="size-3.5" />
|
|
86
|
+
{dismissIcon ?? <X className="size-3.5" />}
|
|
81
87
|
</button>
|
|
82
88
|
)}
|
|
83
89
|
</div>
|
|
@@ -53,7 +53,7 @@ BreadcrumbPage.displayName = "BreadcrumbPage";
|
|
|
53
53
|
|
|
54
54
|
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
|
55
55
|
<li role="presentation" aria-hidden className={cn("[&>svg]:size-3.5", className)} {...props}>
|
|
56
|
-
{children ?? <ChevronRight />}
|
|
56
|
+
{children ?? <ChevronRight className="rtl:-scale-x-100" />}
|
|
57
57
|
</li>
|
|
58
58
|
);
|
|
59
59
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
|
@@ -26,7 +26,7 @@ const buttonVariants = cva(
|
|
|
26
26
|
[
|
|
27
27
|
"inline-flex items-center justify-center gap-1 shrink-0",
|
|
28
28
|
"font-sans font-medium whitespace-nowrap select-none",
|
|
29
|
-
"rounded-md transition-colors outline-none",
|
|
29
|
+
"rounded-md transition-colors duration-instant ease-standard outline-none",
|
|
30
30
|
"focus-visible:shadow-focus",
|
|
31
31
|
"disabled:pointer-events-none disabled:opacity-disabled",
|
|
32
32
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-[18px]",
|
package/src/components/card.tsx
CHANGED
|
@@ -1,17 +1,88 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
5
|
import { cn } from "../lib/utils";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Card — a raised surface (TOKENS.md, "Surface model").
|
|
9
|
+
*
|
|
10
|
+
* At rest it is separated from the page by elevation and a soft edge, not by a heavy stroke: the edge is
|
|
11
|
+
* `--border` at half strength, so the card no longer draws the same line as the inputs and buttons inside
|
|
12
|
+
* it, and the `md` step of the elevation ladder gives it a contact shadow that the old `sm` (5% black)
|
|
13
|
+
* did not visibly have. Both stay on tokens, so Create's surface treatments still reach it — `flat` and
|
|
14
|
+
* `bordered` remove the shadow, `elevated` raises it a rung. On a grouped section (`bg-surface-grouped`)
|
|
15
|
+
* the surface step does most of the work, in dark mode too.
|
|
16
|
+
*
|
|
17
|
+
* A Card is static. It has no hover, no pressed state and no pointer cursor, because a surface that
|
|
18
|
+
* reacts to the pointer reads as clickable, and a static card that does is a false affordance.
|
|
19
|
+
*
|
|
20
|
+
* Interaction is opt-in through semantics, not through a flag: `asChild` renders the card as its single
|
|
21
|
+
* child, and the interactive states below are keyed on that element BEING interactive — an `<a href>` or
|
|
22
|
+
* a `<button>`. So the visual states cannot drift from what the element actually does: a `<div>` never
|
|
23
|
+
* gets them, a link always does, and a consumer cannot style a static card as clickable by accident.
|
|
24
|
+
* One action per card: the element is the action, so it must not contain other controls.
|
|
25
|
+
*
|
|
26
|
+
* hover a pointer that can hover (`@media (hover: hover)`) — edge to full `--border`, lifted to `lg`
|
|
27
|
+
* pressed `:active` — back down to `sm` with a `muted` wash, so it differs from hover in depth AND fill
|
|
28
|
+
* focus the shared `shadow-focus` ring, which replaces the elevation while focused and so stays the
|
|
29
|
+
* strongest edge on the card
|
|
30
|
+
* selected `aria-pressed="true"` (a toggle button) or `aria-current` (a link to where you are) — a
|
|
31
|
+
* 2px `--primary` edge: a change of WEIGHT as well as colour, so it is not carried by hue alone
|
|
32
|
+
*
|
|
33
|
+
* Motion: box-shadow, border-color and background-color over `duration-fast` with `ease-standard`, the
|
|
34
|
+
* catalogue's tier for a state change on a control. Nothing moves or resizes, so there is no layout shift,
|
|
35
|
+
* and under `prefers-reduced-motion` the transition is removed and every state lands immediately.
|
|
36
|
+
*/
|
|
37
|
+
// Every class is written out in full: Tailwind finds classes by scanning source text, so a selector
|
|
38
|
+
// assembled from constants would never be generated.
|
|
39
|
+
const INTERACTIVE = [
|
|
40
|
+
// A link is inline by default and a button centres its text, so the element is made a block that reads
|
|
41
|
+
// from the start edge. These come after plain utilities in the stylesheet, so lay an interactive card out
|
|
42
|
+
// with flex or grid on an inner element (as CardHeader does) rather than on the card itself.
|
|
43
|
+
"[&:where(a[href],button)]:block [&:where(button)]:text-start [&:is(a[href],button)]:cursor-pointer",
|
|
44
|
+
"[&:is(a[href],button)]:outline-none",
|
|
45
|
+
"[&:is(a[href],button)]:transition-[box-shadow,border-color,background-color] [&:is(a[href],button)]:duration-fast [&:is(a[href],button)]:ease-standard motion-reduce:transition-none",
|
|
46
|
+
"[@media(hover:hover)]:[&:is(a[href],button)]:hover:border-border [@media(hover:hover)]:[&:is(a[href],button)]:hover:shadow-lg",
|
|
47
|
+
"[&:is(a[href],button)]:active:shadow-sm [&:is(a[href],button)]:active:bg-muted/40",
|
|
48
|
+
// Selected: an inset 1px of --primary inside the 1px --primary border makes a 2px edge, with the
|
|
49
|
+
// elevation of whichever state it is in alongside, since the ring and the elevation share box-shadow.
|
|
50
|
+
// Restated for hover and pressed, which would otherwise take the border and shadow back. Written as the
|
|
51
|
+
// property, not `shadow-[…]`: Tailwind reads an arbitrary shadow that contains `hsl(` as a shadow COLOUR
|
|
52
|
+
// and the ring silently never rendered — `check:card-visual` caught it.
|
|
53
|
+
"[&:is([aria-pressed=true],[aria-current]:not([aria-current=false]))]:border-primary",
|
|
54
|
+
"[&:is([aria-pressed=true],[aria-current]:not([aria-current=false]))]:[box-shadow:inset_0_0_0_1px_hsl(var(--primary)),var(--shadow-md)]",
|
|
55
|
+
"[@media(hover:hover)]:[&:is([aria-pressed=true],[aria-current]:not([aria-current=false]))]:hover:border-primary",
|
|
56
|
+
"[@media(hover:hover)]:[&:is([aria-pressed=true],[aria-current]:not([aria-current=false]))]:hover:[box-shadow:inset_0_0_0_1px_hsl(var(--primary)),var(--shadow-lg)]",
|
|
57
|
+
"[&:is([aria-pressed=true],[aria-current]:not([aria-current=false]))]:active:[box-shadow:inset_0_0_0_1px_hsl(var(--primary)),var(--shadow-sm)]",
|
|
58
|
+
// Focus replaces the elevation with the shared ring and is the strongest edge the card can show. It is
|
|
59
|
+
// `!important` because hover and selected also set box-shadow with selectors at least as specific, and
|
|
60
|
+
// whichever of those Tailwind happens to emit last must not be able to hide the focus ring.
|
|
61
|
+
"[&:is(a[href],button)]:focus-visible:!shadow-focus",
|
|
62
|
+
"disabled:pointer-events-none disabled:opacity-disabled",
|
|
63
|
+
].join(" ");
|
|
64
|
+
|
|
65
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
66
|
+
/** Render as the single child element (Radix Slot) instead of a `<div>` — e.g.
|
|
67
|
+
* `<Card asChild><a href="/reports/q3">…</a></Card>`. When that element is an `<a href>` or a `<button>`,
|
|
68
|
+
* the card takes the interactive states; anything else stays static. */
|
|
69
|
+
asChild?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, asChild = false, ...props }, ref) => {
|
|
73
|
+
const Comp = asChild ? Slot : "div";
|
|
74
|
+
return (
|
|
75
|
+
<Comp
|
|
9
76
|
ref={ref}
|
|
10
|
-
className={cn(
|
|
77
|
+
className={cn(
|
|
78
|
+
"rounded-xl border border-border/50 bg-card text-card-foreground shadow-md font-sans",
|
|
79
|
+
INTERACTIVE,
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
11
82
|
{...props}
|
|
12
83
|
/>
|
|
13
|
-
)
|
|
14
|
-
);
|
|
84
|
+
);
|
|
85
|
+
});
|
|
15
86
|
Card.displayName = "Card";
|
|
16
87
|
|
|
17
88
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
@@ -6,10 +6,26 @@ import { Check, Minus } from "lucide-react";
|
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Checkbox —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Checkbox — from the KinetixUI design source, node 54863:483: 18px box, 4px radius, 2px border, checked /
|
|
10
|
+
* indeterminate fill = `--action`, focus = 2px `--ring` offset.
|
|
11
|
+
*
|
|
12
|
+
* It follows the selection-control state contract (TOKENS.md, "Selection controls"):
|
|
13
|
+
*
|
|
14
|
+
* rest unchecked boundary `--muted-foreground`, not `--input`. The box is the only thing that says a
|
|
15
|
+
* checkbox is there, so its edge must clear SC 1.4.11's 3:1 (`--input` is 2.2:1 light / 2.7:1
|
|
16
|
+
* dark — a tracked exception for field outlines, where the field's fill and label carry it).
|
|
17
|
+
* hover a pointer that can hover: a `--foreground` state layer at 8% around the box, and the unchecked
|
|
18
|
+
* edge darkens to `--foreground`; checked fills step to `--action`/90
|
|
19
|
+
* pressed the state layer deepens to 14%; checked fills to `--action`/85
|
|
20
|
+
* checked `--action` fill AND a glyph — shape, not only colour
|
|
21
|
+
* invalid `aria-invalid="true"` → `--destructive` edge (and fill when checked); the field's error text
|
|
22
|
+
* is the non-colour cue
|
|
23
|
+
* focus the 2px offset ring, `!important` so no hover or pressed layer can cover it
|
|
24
|
+
* disabled `--opacity-disabled`, and the hover/pressed layers are keyed on `:enabled`, so it never answers
|
|
25
|
+
*
|
|
26
|
+
* Motion: colour and box-shadow over `duration-instant` (100ms, the press/toggle step) with `ease-standard`.
|
|
27
|
+
* The checked state itself does not animate. Reduced motion collapses the transition (the library floor in
|
|
28
|
+
* tailwind.config.ts) and lands on the same end state.
|
|
13
29
|
*/
|
|
14
30
|
const Checkbox = React.forwardRef<
|
|
15
31
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
@@ -18,12 +34,30 @@ const Checkbox = React.forwardRef<
|
|
|
18
34
|
<CheckboxPrimitive.Root
|
|
19
35
|
ref={ref}
|
|
20
36
|
className={cn(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
// 1.125rem, not the 18px it replaces: identical at the default root size, but a reader who raises
|
|
38
|
+
// their browser's default font size scales rem and not px. At 200% the box stayed 18x18 beside a
|
|
39
|
+
// label that doubled — the control shrank by half relative to its own text, and its hit target with it.
|
|
40
|
+
"peer size-[1.125rem] shrink-0 rounded-sm border-2 border-muted-foreground outline-none",
|
|
41
|
+
"transition-[color,background-color,border-color,box-shadow] duration-instant ease-standard",
|
|
24
42
|
"data-[state=checked]:border-action data-[state=checked]:bg-action data-[state=checked]:text-action-foreground",
|
|
25
43
|
"data-[state=indeterminate]:border-action data-[state=indeterminate]:bg-action data-[state=indeterminate]:text-action-foreground",
|
|
26
|
-
|
|
44
|
+
// `aria-[invalid=true]:`, not `aria-invalid:` — Tailwind 3 has no `invalid` in its aria variants, so
|
|
45
|
+
// the short form compiled to nothing and an invalid checkbox looked exactly like a valid one.
|
|
46
|
+
"aria-[invalid=true]:border-destructive aria-[invalid=true]:data-[state=checked]:bg-destructive aria-[invalid=true]:data-[state=indeterminate]:bg-destructive",
|
|
47
|
+
// The state layer: the same for every value, so hover and press read the same way checked or not.
|
|
48
|
+
"[@media(hover:hover)]:enabled:hover:[box-shadow:0_0_0_5px_hsl(var(--foreground)/0.08)]",
|
|
49
|
+
// Pressed is written twice: bare for touch, and again inside the hover media query, because Tailwind
|
|
50
|
+
// emits media-wrapped utilities after plain ones and the hover layer would otherwise outrank the press.
|
|
51
|
+
"enabled:active:[box-shadow:0_0_0_5px_hsl(var(--foreground)/0.14)]",
|
|
52
|
+
"[@media(hover:hover)]:enabled:active:[box-shadow:0_0_0_5px_hsl(var(--foreground)/0.14)]",
|
|
53
|
+
// ...and the box itself answers in the colour it already has. Valid only: an invalid box keeps its
|
|
54
|
+
// `--destructive` edge under the pointer, or hovering would erase the one cue the box carries.
|
|
55
|
+
"[@media(hover:hover)]:enabled:hover:[&[data-state=unchecked]:not([aria-invalid=true])]:border-foreground",
|
|
56
|
+
"[@media(hover:hover)]:enabled:hover:[&:is([data-state=checked],[data-state=indeterminate]):not([aria-invalid=true])]:bg-action/90",
|
|
57
|
+
"enabled:active:[&:is([data-state=checked],[data-state=indeterminate]):not([aria-invalid=true])]:bg-action/85",
|
|
58
|
+
"[@media(hover:hover)]:enabled:active:[&:is([data-state=checked],[data-state=indeterminate]):not([aria-invalid=true])]:bg-action/85",
|
|
59
|
+
"focus-visible:!ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
60
|
+
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
27
61
|
className,
|
|
28
62
|
)}
|
|
29
63
|
{...props}
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
4
6
|
|
|
5
7
|
const Collapsible = CollapsiblePrimitive.Root;
|
|
6
8
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The disclosure transition, which this component shipped without.
|
|
12
|
+
*
|
|
13
|
+
* Collapsible was a bare re-export: three primitives passed through untouched. Its content appeared
|
|
14
|
+
* and vanished in a single frame while `Accordion` — the same disclosure gesture on a sibling Radix
|
|
15
|
+
* primitive — animated its height. Measured before this change, the content went 0px to 84px with no
|
|
16
|
+
* animation at all, against the accordion's 0px → 24.64px → 36px over 200ms.
|
|
17
|
+
*
|
|
18
|
+
* Disclosure is the case where motion carries meaning rather than decorating it: the content growing
|
|
19
|
+
* out of the trigger is what says it belongs to the thing you just pressed, and where it will go when
|
|
20
|
+
* you press it again. Appearing instantly makes the page look like it was replaced instead.
|
|
21
|
+
*
|
|
22
|
+
* `overflow-hidden` is load-bearing, not cosmetic — the height animation clips its content as it
|
|
23
|
+
* runs, and without it the text is laid out at full height from the first frame and simply spills
|
|
24
|
+
* out of the collapsing box.
|
|
25
|
+
*
|
|
26
|
+
* Reduced motion needs nothing here. The package's base layer collapses `animation-duration` for
|
|
27
|
+
* everything it ships, and the content still lands at its full height because the keyframe's end
|
|
28
|
+
* state is unchanged — proven in both directions by `scripts/motion.mjs`.
|
|
29
|
+
*
|
|
30
|
+
* Wrapping the primitive rather than re-exporting it adds a `className` that merges with the
|
|
31
|
+
* caller's, which is the convention every other component here follows. The props, the ref and the
|
|
32
|
+
* data attributes are the primitive's own, so nothing a consumer passes today behaves differently.
|
|
33
|
+
*/
|
|
34
|
+
const CollapsibleContent = React.forwardRef<
|
|
35
|
+
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleContent>,
|
|
36
|
+
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent>
|
|
37
|
+
>(({ className, ...props }, ref) => (
|
|
38
|
+
<CollapsiblePrimitive.CollapsibleContent
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn(
|
|
41
|
+
"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
));
|
|
47
|
+
CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName;
|
|
8
48
|
|
|
9
49
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
|
@@ -38,7 +38,7 @@ const CommandInput = React.forwardRef<
|
|
|
38
38
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
39
39
|
{/* Resting-dim icon, no hover/disabled state — numerically the disabled
|
|
40
40
|
value (0.5) but not that role; left as a literal, see calendar.tsx */}
|
|
41
|
-
<Search className="
|
|
41
|
+
<Search className="me-2 size-4 shrink-0 opacity-50" />
|
|
42
42
|
<CommandPrimitive.Input
|
|
43
43
|
ref={ref}
|
|
44
44
|
className={cn(
|
|
@@ -108,7 +108,7 @@ const CommandItem = React.forwardRef<
|
|
|
108
108
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
|
109
109
|
|
|
110
110
|
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
111
|
-
<span className={cn("
|
|
111
|
+
<span className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
|
|
112
112
|
);
|
|
113
113
|
CommandShortcut.displayName = "CommandShortcut";
|
|
114
114
|
|
|
@@ -4,18 +4,26 @@ import * as React from "react";
|
|
|
4
4
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
|
5
5
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
|
+
import { withDirectionalPortal } from "./direction-provider";
|
|
7
8
|
|
|
8
9
|
const ContextMenu = ContextMenuPrimitive.Root;
|
|
9
10
|
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
|
10
11
|
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
|
11
|
-
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
|
12
|
+
const ContextMenuPortal = withDirectionalPortal(ContextMenuPrimitive.Portal);
|
|
12
13
|
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
|
13
14
|
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
|
14
15
|
|
|
15
16
|
const itemCls =
|
|
16
17
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-disabled";
|
|
17
18
|
const contentCls =
|
|
18
|
-
|
|
19
|
+
// Capped to the room Radix reports it has, so large text cannot push the surface past the window —
|
|
20
|
+
// see the note in popover.tsx for the measurement that prompted it.
|
|
21
|
+
// `min-w` yields to the available room, which is not a detail: `min-width` beats `max-width` in CSS,
|
|
22
|
+
// so the cap above was unenforceable on its own. Measured on a 390px phone with the root font size
|
|
23
|
+
// doubled — 8rem becomes 256px, the cap resolved to 193px, and the menu came out 256px wide with its
|
|
24
|
+
// right edge 63px past the window and those items unreachable. `min(...)` keeps the comfortable
|
|
25
|
+
// minimum whenever there is room for it and gives it up when there is not.
|
|
26
|
+
"z-overlay max-w-[var(--radix-popper-available-width)] min-w-[min(8rem,var(--radix-popper-available-width))] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:duration-fast data-[state=open]:ease-enter data-[state=closed]:duration-instant data-[state=closed]:ease-exit";
|
|
19
27
|
|
|
20
28
|
const ContextMenuSubTrigger = React.forwardRef<
|
|
21
29
|
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
|
@@ -23,11 +31,11 @@ const ContextMenuSubTrigger = React.forwardRef<
|
|
|
23
31
|
>(({ className, inset, children, ...props }, ref) => (
|
|
24
32
|
<ContextMenuPrimitive.SubTrigger
|
|
25
33
|
ref={ref}
|
|
26
|
-
className={cn(itemCls, "data-[state=open]:bg-accent", inset && "
|
|
34
|
+
className={cn(itemCls, "data-[state=open]:bg-accent", inset && "ps-8", className)}
|
|
27
35
|
{...props}
|
|
28
36
|
>
|
|
29
37
|
{children}
|
|
30
|
-
<ChevronRight className="
|
|
38
|
+
<ChevronRight className="me-0 ms-auto size-4 rtl:-scale-x-100" />
|
|
31
39
|
</ContextMenuPrimitive.SubTrigger>
|
|
32
40
|
));
|
|
33
41
|
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
|
@@ -44,9 +52,9 @@ const ContextMenuContent = React.forwardRef<
|
|
|
44
52
|
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
45
53
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
46
54
|
>(({ className, ...props }, ref) => (
|
|
47
|
-
<
|
|
55
|
+
<ContextMenuPortal>
|
|
48
56
|
<ContextMenuPrimitive.Content ref={ref} className={cn(contentCls, className)} {...props} />
|
|
49
|
-
</
|
|
57
|
+
</ContextMenuPortal>
|
|
50
58
|
));
|
|
51
59
|
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
|
52
60
|
|
|
@@ -54,7 +62,7 @@ const ContextMenuItem = React.forwardRef<
|
|
|
54
62
|
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
|
55
63
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { inset?: boolean }
|
|
56
64
|
>(({ className, inset, ...props }, ref) => (
|
|
57
|
-
<ContextMenuPrimitive.Item ref={ref} className={cn(itemCls, inset && "
|
|
65
|
+
<ContextMenuPrimitive.Item ref={ref} className={cn(itemCls, inset && "ps-8", className)} {...props} />
|
|
58
66
|
));
|
|
59
67
|
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
|
60
68
|
|
|
@@ -62,8 +70,8 @@ const ContextMenuCheckboxItem = React.forwardRef<
|
|
|
62
70
|
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
|
63
71
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
64
72
|
>(({ className, children, checked, ...props }, ref) => (
|
|
65
|
-
<ContextMenuPrimitive.CheckboxItem ref={ref} className={cn(itemCls, "
|
|
66
|
-
<span className="absolute
|
|
73
|
+
<ContextMenuPrimitive.CheckboxItem ref={ref} className={cn(itemCls, "ps-8", className)} checked={checked} {...props}>
|
|
74
|
+
<span className="absolute start-2 flex size-3.5 items-center justify-center">
|
|
67
75
|
<ContextMenuPrimitive.ItemIndicator>
|
|
68
76
|
<Check className="size-4" />
|
|
69
77
|
</ContextMenuPrimitive.ItemIndicator>
|
|
@@ -77,8 +85,8 @@ const ContextMenuRadioItem = React.forwardRef<
|
|
|
77
85
|
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
|
78
86
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
79
87
|
>(({ className, children, ...props }, ref) => (
|
|
80
|
-
<ContextMenuPrimitive.RadioItem ref={ref} className={cn(itemCls, "
|
|
81
|
-
<span className="absolute
|
|
88
|
+
<ContextMenuPrimitive.RadioItem ref={ref} className={cn(itemCls, "ps-8", className)} {...props}>
|
|
89
|
+
<span className="absolute start-2 flex size-3.5 items-center justify-center">
|
|
82
90
|
<ContextMenuPrimitive.ItemIndicator>
|
|
83
91
|
<Circle className="size-2 fill-current" />
|
|
84
92
|
</ContextMenuPrimitive.ItemIndicator>
|
|
@@ -94,7 +102,7 @@ const ContextMenuLabel = React.forwardRef<
|
|
|
94
102
|
>(({ className, inset, ...props }, ref) => (
|
|
95
103
|
<ContextMenuPrimitive.Label
|
|
96
104
|
ref={ref}
|
|
97
|
-
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "
|
|
105
|
+
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "ps-8", className)}
|
|
98
106
|
{...props}
|
|
99
107
|
/>
|
|
100
108
|
));
|
|
@@ -109,7 +117,7 @@ const ContextMenuSeparator = React.forwardRef<
|
|
|
109
117
|
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
|
110
118
|
|
|
111
119
|
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
112
|
-
<span className={cn("
|
|
120
|
+
<span className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
|
|
113
121
|
);
|
|
114
122
|
ContextMenuShortcut.displayName = "ContextMenuShortcut";
|
|
115
123
|
|
|
@@ -4,10 +4,11 @@ import * as React from "react";
|
|
|
4
4
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
|
+
import { withDirectionalPortal } from "./direction-provider";
|
|
7
8
|
|
|
8
9
|
const Dialog = DialogPrimitive.Root;
|
|
9
10
|
const DialogTrigger = DialogPrimitive.Trigger;
|
|
10
|
-
const DialogPortal = DialogPrimitive.Portal;
|
|
11
|
+
const DialogPortal = withDirectionalPortal(DialogPrimitive.Portal);
|
|
11
12
|
const DialogClose = DialogPrimitive.Close;
|
|
12
13
|
|
|
13
14
|
const DialogOverlay = React.forwardRef<
|
|
@@ -17,7 +18,7 @@ const DialogOverlay = React.forwardRef<
|
|
|
17
18
|
<DialogPrimitive.Overlay
|
|
18
19
|
ref={ref}
|
|
19
20
|
className={cn(
|
|
20
|
-
"fixed inset-0 z-overlay bg-
|
|
21
|
+
"fixed inset-0 z-overlay bg-scrim backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:duration-fast data-[state=open]:ease-enter data-[state=closed]:duration-instant data-[state=closed]:ease-exit",
|
|
21
22
|
className,
|
|
22
23
|
)}
|
|
23
24
|
{...props}
|
|
@@ -36,7 +37,7 @@ const DialogContent = React.forwardRef<
|
|
|
36
37
|
className={cn(
|
|
37
38
|
// rtl-ok: centering trick (left-1/2 + -translate-x-1/2), direction-agnostic by construction
|
|
38
39
|
"fixed left-1/2 top-1/2 z-overlay grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
|
|
39
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
40
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:duration-fast data-[state=open]:ease-enter data-[state=closed]:duration-instant data-[state=closed]:ease-exit",
|
|
40
41
|
className,
|
|
41
42
|
)}
|
|
42
43
|
{...props}
|
|
@@ -58,7 +59,7 @@ DialogHeader.displayName = "DialogHeader";
|
|
|
58
59
|
|
|
59
60
|
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
60
61
|
<div
|
|
61
|
-
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end
|
|
62
|
+
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
62
63
|
{...props}
|
|
63
64
|
/>
|
|
64
65
|
);
|