@kinetixui/ui 0.23.2 → 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
|
@@ -8,6 +8,34 @@ export interface KinetixDirectionProviderProps {
|
|
|
8
8
|
dir: "ltr" | "rtl";
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Where this provider's overlays portal to: a `<div dir>` directly under `<body>`, one per provider.
|
|
13
|
+
*
|
|
14
|
+
* Overlays (Dialog, Sheet, Popover, Tooltip, menus, …) portal out of the tree that rendered them, so the
|
|
15
|
+
* `dir` attribute around the trigger never reaches them — they inherit from `<body>`, which inherits from
|
|
16
|
+
* `<html>`. Radix's `DirectionContext` fixes its own positioning and, for Select and the menu family, stamps
|
|
17
|
+
* `dir` on the content, but Dialog, Popover, Tooltip and the rest stamp nothing, so their text, logical
|
|
18
|
+
* padding and `rtl:` variants followed the document instead of the section that opened them.
|
|
19
|
+
*
|
|
20
|
+
* Portaling into a host that carries the provider's `dir` fixes all of them in one place, through ordinary
|
|
21
|
+
* DOM inheritance: `direction`, `[dir]` selectors and Tailwind's `rtl:` variant all match an ancestor. The
|
|
22
|
+
* host is a plain block with no styles, so it creates no stacking context or containing block — fixed
|
|
23
|
+
* overlays position against the viewport exactly as they did from `<body>`.
|
|
24
|
+
*
|
|
25
|
+
* Three states, so nothing is ever committed into a host that is not in the document:
|
|
26
|
+
* `undefined` no provider: overlays portal to `<body>` and inherit `<html dir>`, as before.
|
|
27
|
+
* `null` a provider whose host is not attached yet (its first render, and the server): portals render
|
|
28
|
+
* nothing. Descendants' layout effects run before the provider's, so publishing the host
|
|
29
|
+
* earlier would let an overlay that mounts with the provider (an open Dialog, say) commit its DOM
|
|
30
|
+
* into a detached node, where native `autoFocus` and layout measurements are lost.
|
|
31
|
+
* an element attached: portals render into it.
|
|
32
|
+
* The switch from `null` to the element happens in the provider's layout effect, so it is applied before the
|
|
33
|
+
* browser paints.
|
|
34
|
+
*/
|
|
35
|
+
const PortalContainerContext = React.createContext<HTMLElement | null | undefined>(undefined);
|
|
36
|
+
|
|
37
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
|
|
38
|
+
|
|
11
39
|
/**
|
|
12
40
|
* Every Radix primitive this library builds on (Select, DropdownMenu, Popover,
|
|
13
41
|
* Tooltip, ContextMenu, Menubar, NavigationMenu, HoverCard, …) reads direction
|
|
@@ -21,7 +49,63 @@ export interface KinetixDirectionProviderProps {
|
|
|
21
49
|
* <html dir={dir}>
|
|
22
50
|
* <KinetixDirectionProvider dir={dir}>{children}</KinetixDirectionProvider>
|
|
23
51
|
* </html>
|
|
52
|
+
*
|
|
53
|
+
* It also scopes: a provider around one section (with `dir` on that section's element) gives that section
|
|
54
|
+
* its own direction, and the overlays opened from inside it portal with that direction too, without
|
|
55
|
+
* touching `<html>`. The nearest provider wins.
|
|
24
56
|
*/
|
|
25
57
|
export function KinetixDirectionProvider({ dir, children }: KinetixDirectionProviderProps) {
|
|
26
|
-
|
|
58
|
+
// Created on the client's first render, attached in a layout effect.
|
|
59
|
+
const [host] = React.useState(() => (typeof document === "undefined" ? null : document.createElement("div")));
|
|
60
|
+
const [attached, setAttached] = React.useState(false);
|
|
61
|
+
|
|
62
|
+
useIsomorphicLayoutEffect(() => {
|
|
63
|
+
if (!host) return;
|
|
64
|
+
host.setAttribute("data-kinetix-portal", "");
|
|
65
|
+
host.setAttribute("dir", dir);
|
|
66
|
+
document.body.appendChild(host);
|
|
67
|
+
setAttached(true);
|
|
68
|
+
return () => {
|
|
69
|
+
host.remove();
|
|
70
|
+
setAttached(false);
|
|
71
|
+
};
|
|
72
|
+
// `dir` is applied by the effect below; this one only attaches, once.
|
|
73
|
+
}, [host]);
|
|
74
|
+
|
|
75
|
+
useIsomorphicLayoutEffect(() => {
|
|
76
|
+
host?.setAttribute("dir", dir);
|
|
77
|
+
}, [host, dir]);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<RadixDirectionProvider dir={dir}>
|
|
81
|
+
<PortalContainerContext.Provider value={attached ? host : null}>{children}</PortalContainerContext.Provider>
|
|
82
|
+
</RadixDirectionProvider>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The element the nearest `KinetixDirectionProvider` wants overlays portaled into. `undefined` outside one
|
|
88
|
+
* (portal to `<body>` as usual); `null` inside one whose host is not attached yet, in which case render
|
|
89
|
+
* nothing and try again, which happens before paint. Every overlay in this library follows this; use it for a
|
|
90
|
+
* custom portal so it gets the same direction.
|
|
91
|
+
*/
|
|
92
|
+
export function useKinetixPortalContainer(): HTMLElement | null | undefined {
|
|
93
|
+
return React.useContext(PortalContainerContext);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wraps a primitive's `Portal` so it portals into the nearest provider's host (see above). An explicit
|
|
98
|
+
* `container` still wins. Internal: every overlay in this library is built with it, which is what keeps the
|
|
99
|
+
* direction rule in one place instead of in each overlay.
|
|
100
|
+
*/
|
|
101
|
+
export function withDirectionalPortal<P extends { container?: Element | DocumentFragment | null }>(
|
|
102
|
+
Portal: React.ComponentType<P>,
|
|
103
|
+
) {
|
|
104
|
+
function DirectionalPortal({ container, ...props }: P) {
|
|
105
|
+
const host = useKinetixPortalContainer();
|
|
106
|
+
if (host === null && !container) return null;
|
|
107
|
+
return <Portal {...(props as unknown as P)} container={container ?? host} />;
|
|
108
|
+
}
|
|
109
|
+
DirectionalPortal.displayName = `Directional(${Portal.displayName ?? Portal.name ?? "Portal"})`;
|
|
110
|
+
return DirectionalPortal;
|
|
27
111
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Drawer as DrawerPrimitive } from "vaul";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
|
+
import { withDirectionalPortal } from "./direction-provider";
|
|
6
7
|
|
|
7
8
|
const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
|
8
9
|
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
|
|
@@ -10,36 +11,79 @@ const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps
|
|
|
10
11
|
Drawer.displayName = "Drawer";
|
|
11
12
|
|
|
12
13
|
const DrawerTrigger = DrawerPrimitive.Trigger;
|
|
13
|
-
const DrawerPortal = DrawerPrimitive.Portal;
|
|
14
|
+
const DrawerPortal = withDirectionalPortal(DrawerPrimitive.Portal);
|
|
14
15
|
const DrawerClose = DrawerPrimitive.Close;
|
|
15
16
|
|
|
16
17
|
const DrawerOverlay = React.forwardRef<
|
|
17
18
|
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
|
18
19
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
|
19
20
|
>(({ className, ...props }, ref) => (
|
|
20
|
-
<DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-overlay bg-
|
|
21
|
+
<DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-overlay bg-scrim", className)} {...props} />
|
|
21
22
|
));
|
|
22
23
|
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
|
23
24
|
|
|
24
25
|
const DrawerContent = React.forwardRef<
|
|
25
26
|
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
26
27
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
|
27
|
-
>(({ className, children, ...props }, ref) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
>(({ className, children, onOpenAutoFocus, ...props }, ref) => {
|
|
29
|
+
/**
|
|
30
|
+
* Focus entry. Measured in Chromium with the drawer open: focus was still on the trigger — which by
|
|
31
|
+
* then sits inside a subtree `aria-hidden="true"`, because the drawer hides the page behind it. So a
|
|
32
|
+
* screen-reader user opened the drawer and was left on an element their assistive technology had just
|
|
33
|
+
* been told does not exist. Tab recovered on the next keystroke; the destination on open did not.
|
|
34
|
+
*
|
|
35
|
+
* Every other modal surface in the family moves focus into the portal on open. `vaul` suppresses that
|
|
36
|
+
* deliberately, to stop a mobile keyboard appearing whenever a drawer with a field in it opens, and
|
|
37
|
+
* that reason is good — so this focuses the panel itself rather than the first thing inside it. A
|
|
38
|
+
* container is not a text field: it opens no keyboard, it gives the screen reader the drawer's own
|
|
39
|
+
* heading to announce, and Tab from there walks the drawer's contents in order.
|
|
40
|
+
*
|
|
41
|
+
* Notably, axe does *not* report this. Its `aria-hidden-focus` rule excuses an aria-hidden page
|
|
42
|
+
* whenever a modal dialog is open, wherever focus actually is — so the open-state scan that found the
|
|
43
|
+
* Popover naming defect is blind to this one. Focus entry needs its own assertion, which is why
|
|
44
|
+
* a11y-browser now carries one.
|
|
45
|
+
*/
|
|
46
|
+
const panel = React.useRef<HTMLDivElement | null>(null);
|
|
47
|
+
const setRefs = (node: HTMLDivElement | null) => {
|
|
48
|
+
panel.current = node;
|
|
49
|
+
if (typeof ref === "function") ref(node);
|
|
50
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<DrawerPortal>
|
|
55
|
+
<DrawerOverlay />
|
|
56
|
+
<DrawerPrimitive.Content
|
|
57
|
+
ref={setRefs}
|
|
58
|
+
tabIndex={-1}
|
|
59
|
+
// `onOpenAutoFocus` and not a mount effect: the focus scope underneath runs after mount, so an
|
|
60
|
+
// effect that focuses the panel is overwritten a tick later and the fix silently does nothing
|
|
61
|
+
// (measured — the first attempt here was exactly that). This is the moment the primitive offers.
|
|
62
|
+
//
|
|
63
|
+
// Composed with the caller's handler and destructured out of the spread below: with `{...props}`
|
|
64
|
+
// trailing, any `onOpenAutoFocus` a consumer passes — even one that only logs — would replace this
|
|
65
|
+
// one outright, and since vaul suppresses auto-focus otherwise, focus would be left on the
|
|
66
|
+
// now-hidden trigger and the defect would be back. A caller who calls `preventDefault` themselves
|
|
67
|
+
// is taking focus over deliberately, so that decision stands and the panel is left alone.
|
|
68
|
+
onOpenAutoFocus={(event) => {
|
|
69
|
+
onOpenAutoFocus?.(event);
|
|
70
|
+
if (event.defaultPrevented) return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
panel.current?.focus();
|
|
73
|
+
}}
|
|
74
|
+
className={cn(
|
|
75
|
+
"fixed inset-x-0 bottom-0 z-overlay mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background font-sans",
|
|
76
|
+
"outline-none",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
|
82
|
+
{children}
|
|
83
|
+
</DrawerPrimitive.Content>
|
|
84
|
+
</DrawerPortal>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
43
87
|
DrawerContent.displayName = "DrawerContent";
|
|
44
88
|
|
|
45
89
|
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
@@ -4,16 +4,24 @@ import * as React from "react";
|
|
|
4
4
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-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 DropdownMenu = DropdownMenuPrimitive.Root;
|
|
9
10
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
10
11
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
11
|
-
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
12
|
+
const DropdownMenuPortal = withDirectionalPortal(DropdownMenuPrimitive.Portal);
|
|
12
13
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
13
14
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
14
15
|
|
|
15
16
|
const contentCls =
|
|
16
|
-
|
|
17
|
+
// Capped to the room Radix reports it has, so large text cannot push the surface past the window —
|
|
18
|
+
// see the note in popover.tsx for the measurement that prompted it.
|
|
19
|
+
// `min-w` yields to the available room, which is not a detail: `min-width` beats `max-width` in CSS,
|
|
20
|
+
// so the cap above was unenforceable on its own. Measured on a 390px phone with the root font size
|
|
21
|
+
// doubled — 8rem becomes 256px, the cap resolved to 193px, and the menu came out 256px wide with its
|
|
22
|
+
// right edge 63px past the window and those items unreachable. `min(...)` keeps the comfortable
|
|
23
|
+
// minimum whenever there is room for it and gives it up when there is not.
|
|
24
|
+
"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";
|
|
17
25
|
const itemCls =
|
|
18
26
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors 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";
|
|
19
27
|
|
|
@@ -21,9 +29,9 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
21
29
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
22
30
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
23
31
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
24
|
-
<
|
|
32
|
+
<DropdownMenuPortal>
|
|
25
33
|
<DropdownMenuPrimitive.Content ref={ref} sideOffset={sideOffset} className={cn(contentCls, className)} {...props} />
|
|
26
|
-
</
|
|
34
|
+
</DropdownMenuPortal>
|
|
27
35
|
));
|
|
28
36
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
29
37
|
|
|
@@ -139,7 +139,7 @@ function FileUploadItem({ file, onRemove, onRetry }: FileUploadItemProps) {
|
|
|
139
139
|
return (
|
|
140
140
|
<li className={cn("flex items-center gap-2.5 rounded-md border p-2.5 text-body-sm", isError ? "border-destructive bg-destructive/5" : "border-input")}>
|
|
141
141
|
{isLoading ? (
|
|
142
|
-
<Loader2 className="size-4 shrink-0 animate-spin motion-reduce:animate-
|
|
142
|
+
<Loader2 className="size-4 shrink-0 animate-spin motion-reduce:animate-none text-muted-foreground" aria-hidden />
|
|
143
143
|
) : isError ? (
|
|
144
144
|
<TriangleAlert className="size-4 shrink-0 text-destructive" aria-hidden />
|
|
145
145
|
) : (
|
|
@@ -17,7 +17,7 @@ const HoverCardContent = React.forwardRef<
|
|
|
17
17
|
sideOffset={sideOffset}
|
|
18
18
|
className={cn(
|
|
19
19
|
"z-overlay w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none font-sans",
|
|
20
|
-
"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",
|
|
20
|
+
"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",
|
|
21
21
|
className,
|
|
22
22
|
)}
|
|
23
23
|
{...props}
|
|
@@ -40,12 +40,18 @@ export interface InformProps
|
|
|
40
40
|
VariantProps<typeof informVariants> {
|
|
41
41
|
hideIcon?: boolean;
|
|
42
42
|
onDismiss?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Replaces the default dismiss mark (lucide's `X`) with your own icon — any icon library or SVG.
|
|
45
|
+
* The button keeps its accessible name ("Dismiss") and sizes any SVG inside it to 14px, so the icon
|
|
46
|
+
* needs no size, colour or aria attributes of its own. See /docs/icons.
|
|
47
|
+
*/
|
|
48
|
+
dismissIcon?: React.ReactNode;
|
|
43
49
|
/** optional CTA rendered below the message */
|
|
44
50
|
action?: { label: React.ReactNode; onClick?: () => void; icon?: React.ReactNode };
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const Inform = React.forwardRef<HTMLDivElement, InformProps>(
|
|
48
|
-
({ className, variant = "information", hideIcon, onDismiss, action, children, ...props }, ref) => {
|
|
54
|
+
({ className, variant = "information", hideIcon, onDismiss, dismissIcon, action, children, ...props }, ref) => {
|
|
49
55
|
const Icon = ICON[variant ?? "information"];
|
|
50
56
|
return (
|
|
51
57
|
<div ref={ref} role="status" className={cn(informVariants({ variant }), className)} {...props}>
|
|
@@ -68,9 +74,9 @@ const Inform = React.forwardRef<HTMLDivElement, InformProps>(
|
|
|
68
74
|
type="button"
|
|
69
75
|
onClick={onDismiss}
|
|
70
76
|
aria-label="Dismiss"
|
|
71
|
-
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"
|
|
77
|
+
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"
|
|
72
78
|
>
|
|
73
|
-
<X className="size-3.5" />
|
|
79
|
+
{dismissIcon ?? <X className="size-3.5" />}
|
|
74
80
|
</button>
|
|
75
81
|
)}
|
|
76
82
|
</div>
|
|
@@ -10,6 +10,18 @@ import { cn } from "../lib/utils";
|
|
|
10
10
|
* source's "Fixed Add-on" pattern. Border / focus glow / invalid state apply to
|
|
11
11
|
* the whole group.
|
|
12
12
|
*
|
|
13
|
+
* The shell follows the text-entry state contract (TOKENS.md, "Composite fields"): it is ONE field, so it
|
|
14
|
+
* carries the field's edge, hover, focus, invalid, read-only and disabled states, read from the input inside
|
|
15
|
+
* it. Add-ons draw no border of their own except an `InputGroupButton`'s quiet internal divider.
|
|
16
|
+
*
|
|
17
|
+
* rest edge `--muted-foreground` at 80% (3:1 on a card; `--input` was 2.2:1)
|
|
18
|
+
* hover the edge steps to full `--muted-foreground` — never over focus, invalid, read-only or disabled
|
|
19
|
+
* focus the INPUT focused → `--action` edge + `--shadow-focus` on the shell. An add-on button focused
|
|
20
|
+
* → that button's own inset ring, and the shell stays at rest: the ring says which part has focus
|
|
21
|
+
* invalid `aria-invalid="true"` on the input → `--destructive` edge, kept under the pointer and focus
|
|
22
|
+
* read-only `readonly` on the input → the inset `--muted` fill
|
|
23
|
+
* disabled a disabled input → `--opacity-disabled`, inert
|
|
24
|
+
*
|
|
13
25
|
* <InputGroup>
|
|
14
26
|
* <InputGroupText>https://</InputGroupText>
|
|
15
27
|
* <InputGroupInput placeholder="kinetixui.com" />
|
|
@@ -21,9 +33,12 @@ const InputGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
|
|
|
21
33
|
ref={ref}
|
|
22
34
|
data-slot="input-group"
|
|
23
35
|
className={cn(
|
|
24
|
-
"flex w-full items-center overflow-hidden rounded-sm border border-
|
|
25
|
-
"
|
|
26
|
-
"has
|
|
36
|
+
"flex w-full items-center overflow-hidden rounded-sm border border-muted-foreground/80 bg-background font-sans",
|
|
37
|
+
"transition-[border-color,box-shadow,background-color] duration-instant ease-standard",
|
|
38
|
+
"[@media(hover:hover)]:hover:[&:not(:has([data-slot=input-group-input]:is(:focus,:disabled,[readonly],[aria-invalid=true])))]:border-muted-foreground",
|
|
39
|
+
"has-[[data-slot=input-group-input][readonly]]:bg-muted",
|
|
40
|
+
"has-[[data-slot=input-group-input]:focus:not([aria-invalid=true])]:border-action has-[[data-slot=input-group-input]:focus]:shadow-focus",
|
|
41
|
+
"has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:has-[[data-slot=input-group-input]:focus]:shadow-focus-destructive",
|
|
27
42
|
"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-disabled",
|
|
28
43
|
className,
|
|
29
44
|
)}
|
|
@@ -83,9 +98,16 @@ const InputGroupButton = React.forwardRef<
|
|
|
83
98
|
ref={ref}
|
|
84
99
|
type={type}
|
|
85
100
|
className={cn(
|
|
101
|
+
// The divider is internal structure, deliberately quieter than the shell's edge: the shell is the one
|
|
102
|
+
// boundary of the field, the divider only says where the button begins.
|
|
86
103
|
"flex h-full shrink-0 items-center gap-1.5 self-stretch border-s border-input bg-muted px-3 text-label-md text-foreground",
|
|
87
|
-
//
|
|
88
|
-
|
|
104
|
+
// Hover is the selection controls' `foreground` state layer (8%), which reads on `muted` in both themes —
|
|
105
|
+
// `accent` equals `muted` in dark, so an accent fill changed nothing there. Focus is the inset ring, and
|
|
106
|
+
// only focus draws it: a ring on hover made the pointer look exactly like keyboard focus.
|
|
107
|
+
"outline-none transition-[background-color,box-shadow] duration-instant ease-standard",
|
|
108
|
+
// (an inset shadow, so it layers over `muted` and composes with the ring instead of replacing either)
|
|
109
|
+
"[@media(hover:hover)]:enabled:hover:shadow-[inset_0_0_0_100vmax_hsl(var(--foreground)/0.08)]",
|
|
110
|
+
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
89
111
|
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
90
112
|
className,
|
|
91
113
|
)}
|
|
@@ -11,7 +11,9 @@ const InputOTP = React.forwardRef<
|
|
|
11
11
|
>(({ className, containerClassName, ...props }, ref) => (
|
|
12
12
|
<OTPInput
|
|
13
13
|
ref={ref}
|
|
14
|
-
|
|
14
|
+
// `group/otp`: the slots read the field's state from the one real input inside this container — see
|
|
15
|
+
// InputOTPSlot.
|
|
16
|
+
containerClassName={cn("group/otp flex flex-wrap items-center gap-2 has-[:disabled]:opacity-disabled", containerClassName)}
|
|
15
17
|
className={cn("disabled:cursor-not-allowed", className)}
|
|
16
18
|
{...props}
|
|
17
19
|
/>
|
|
@@ -19,7 +21,17 @@ const InputOTP = React.forwardRef<
|
|
|
19
21
|
InputOTP.displayName = "InputOTP";
|
|
20
22
|
|
|
21
23
|
const InputOTPGroup = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
|
|
22
|
-
({ className, ...props }, ref) =>
|
|
24
|
+
({ className, ...props }, ref) => (
|
|
25
|
+
// `flex-wrap` costs nothing at a normal text size — six 36px slots fit the narrowest phone with room
|
|
26
|
+
// to spare — and rescues the case that measured badly: at 2x the default font size on a 390px
|
|
27
|
+
// viewport the slots are 72px each, the row is 432px, and the page gained 106px of horizontal
|
|
28
|
+
// scrolling. A second row of slots is worse-looking than one; a form the reader has to scroll
|
|
29
|
+
// sideways to type a code into is worse than that.
|
|
30
|
+
//
|
|
31
|
+
// Wrapping is only safe because each slot owns its whole border — see InputOTPSlot. A row that
|
|
32
|
+
// borrowed its first edge from `:first-child` would be open at the end it began from.
|
|
33
|
+
<div ref={ref} className={cn("flex flex-wrap items-center", className)} {...props} />
|
|
34
|
+
),
|
|
23
35
|
);
|
|
24
36
|
InputOTPGroup.displayName = "InputOTPGroup";
|
|
25
37
|
|
|
@@ -34,17 +46,48 @@ const InputOTPSlot = React.forwardRef<
|
|
|
34
46
|
<div
|
|
35
47
|
ref={ref}
|
|
36
48
|
className={cn(
|
|
37
|
-
|
|
49
|
+
// Every slot carries its own border, and each one after the first is pulled back by a pixel so the
|
|
50
|
+
// two borders between neighbours collapse into the single divider the design has always shown.
|
|
51
|
+
//
|
|
52
|
+
// The obvious cheaper spelling — block borders on all, an inline-end divider, and the inline-start
|
|
53
|
+
// edge on `:first-child` — is what this replaces, because `:first-child` is the first slot in the
|
|
54
|
+
// DOM and not the first slot in each visual row. Measured at 390px and 2x text, where six slots
|
|
55
|
+
// wrap: the second row began with `border-s: 0`, so its outer start edge was open in LTR and in
|
|
56
|
+
// RTL alike. That is the same missing-outline defect the logical classes were added to fix, and it
|
|
57
|
+
// came back the moment the row could wrap. With a border on every side of every slot, a row is
|
|
58
|
+
// closed wherever it starts and wherever it ends.
|
|
59
|
+
//
|
|
60
|
+
// Two consequences, both deliberate: the rounded corners still belong to the DOM's first and last
|
|
61
|
+
// slot, so a wrapped row has square outer corners; and two wrapped rows meet in a 2px seam, since
|
|
62
|
+
// no selector can pull a row that only exists at one viewport width. A seam is not an open edge.
|
|
63
|
+
//
|
|
64
|
+
// The slots together are one field and follow the text-entry state contract (TOKENS.md, "Composite
|
|
65
|
+
// fields"), reading its state from the hidden input through `group/otp`:
|
|
66
|
+
// rest edge `--muted-foreground` at 80% — 3:1 where `--input` was 2.2:1 — and no elevation: a
|
|
67
|
+
// field is inset, so the per-slot `shadow-sm` is gone
|
|
68
|
+
// hover every slot's edge steps to full `--muted-foreground`; not while focused, invalid or disabled
|
|
69
|
+
// focus the ACTIVE slot (where the next character goes) takes `--action` + `--shadow-focus`, the
|
|
70
|
+
// strongest state, drawn above its neighbours
|
|
71
|
+
// invalid `aria-invalid="true"` on InputOTP → `--destructive` edges, kept under pointer and focus
|
|
72
|
+
// disabled the container's `--opacity-disabled`; no hover
|
|
73
|
+
"relative flex size-9 items-center justify-center border border-muted-foreground/80 bg-background text-sm",
|
|
74
|
+
"[&:not(:first-child)]:-ms-px first:rounded-s-md last:rounded-e-md",
|
|
75
|
+
"transition-[color,border-color,box-shadow] duration-instant ease-standard",
|
|
76
|
+
"[@media(hover:hover)]:group-[:hover:not(:has(input:is(:focus,:disabled,[aria-invalid=true])))]/otp:border-muted-foreground",
|
|
77
|
+
"group-has-[input[aria-invalid=true]]/otp:border-destructive",
|
|
38
78
|
// ring is a box-shadow, which forced-colors mode strips; the outline survives it
|
|
39
|
-
isActive &&
|
|
79
|
+
isActive &&
|
|
80
|
+
"z-docked border-action shadow-focus group-has-[input[aria-invalid=true]]/otp:shadow-focus-destructive forced-colors:[outline:2px_solid] forced-colors:[outline-offset:-2px]",
|
|
40
81
|
className,
|
|
41
82
|
)}
|
|
83
|
+
// A stable hook for the active slot, so tests and gates don't have to read its styling.
|
|
84
|
+
data-active={isActive ? "" : undefined}
|
|
42
85
|
{...props}
|
|
43
86
|
>
|
|
44
87
|
{char}
|
|
45
88
|
{hasFakeCaret && (
|
|
46
89
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
47
|
-
<div className="h-4 w-px animate-caret-blink bg-foreground duration-slower" />
|
|
90
|
+
<div className="h-4 w-px animate-caret-blink motion-reduce:animate-none bg-foreground duration-slower" />
|
|
48
91
|
</div>
|
|
49
92
|
)}
|
|
50
93
|
</div>
|
package/src/components/input.tsx
CHANGED
|
@@ -15,16 +15,33 @@ import { cn } from "../lib/utils";
|
|
|
15
15
|
*
|
|
16
16
|
* Figma's component bundles a label + helper text; those live in the `Field`
|
|
17
17
|
* composition (their colour tracks the control's state). This is the bare
|
|
18
|
-
* control. Tokens: border `--
|
|
18
|
+
* control. Tokens: border `--muted-foreground`, focus `--ring`, error `--destructive`,
|
|
19
19
|
* radius `--radius-sm` (Figma `input` = 4px), padding `--spacing-3`,
|
|
20
20
|
* text = Body Medium (14 / 20, +0.25).
|
|
21
|
+
*
|
|
22
|
+
* It follows the text-entry state contract (TOKENS.md, "Text entry and navigation"):
|
|
23
|
+
*
|
|
24
|
+
* rest edge `--muted-foreground` at 80% — 3.4:1 light / 5.4:1 dark on a card, where `--input` was
|
|
25
|
+
* 2.2:1 / 2.4:1 and was the only thing marking where the field is (SC 1.4.11)
|
|
26
|
+
* hover a pointer that can hover: the edge steps to full `--muted-foreground`. Not on a focused,
|
|
27
|
+
* invalid, read-only or disabled field — hover never takes over a stronger state
|
|
28
|
+
* focus `--action` edge + the `--shadow-focus` ring, the strongest signal, unchanged by the pointer
|
|
29
|
+
* invalid `aria-invalid="true"` → `--destructive` edge and ring, which hover does not replace; the
|
|
30
|
+
* field's error text is the non-colour cue (SC 1.4.1)
|
|
31
|
+
* read-only an inset `--muted` fill, full-strength text, no hover: readable and selectable, not editable
|
|
32
|
+
* disabled `--opacity-disabled`, inert
|
|
33
|
+
*
|
|
34
|
+
* Motion: border-color, box-shadow and background-color over `duration-instant` with `ease-standard`.
|
|
35
|
+
* Reduced motion collapses it (the library floor) and lands on the same end state.
|
|
21
36
|
*/
|
|
22
37
|
const inputVariants = cva(
|
|
23
38
|
[
|
|
24
|
-
"flex w-full border border-
|
|
39
|
+
"flex w-full border border-muted-foreground/80 bg-background px-3 py-3",
|
|
25
40
|
"font-sans text-body-md text-foreground",
|
|
26
41
|
"placeholder:text-muted-foreground",
|
|
27
|
-
"outline-none transition-
|
|
42
|
+
"outline-none transition-[border-color,box-shadow,background-color] duration-instant ease-standard",
|
|
43
|
+
"[@media(hover:hover)]:enabled:hover:[&:not(:focus):not([aria-invalid=true]):not([readonly]):not([data-state=focus])]:border-muted-foreground",
|
|
44
|
+
"[&[readonly]]:bg-muted",
|
|
28
45
|
"focus-visible:border-action focus-visible:shadow-focus",
|
|
29
46
|
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
30
47
|
"aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive aria-[invalid=true]:focus-visible:shadow-focus-destructive",
|
|
@@ -125,7 +125,7 @@ function JsonNode({
|
|
|
125
125
|
aria-label={expanded ? "Collapse" : "Expand"}
|
|
126
126
|
className="mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
127
127
|
>
|
|
128
|
-
<ChevronRight className={cn("size-3 transition-transform", expanded
|
|
128
|
+
<ChevronRight className={cn("size-3 transition-transform", expanded ? "rotate-90" : "rtl:-scale-x-100")} />
|
|
129
129
|
</button>
|
|
130
130
|
<span>
|
|
131
131
|
{name !== undefined && <span className="text-muted-foreground">{JSON.stringify(name)}: </span>}
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
|
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 MenubarMenu = (props: React.ComponentProps<typeof MenubarPrimitive.Menu>) => (
|
|
9
10
|
<MenubarPrimitive.Menu {...props} />
|
|
@@ -11,9 +12,7 @@ const MenubarMenu = (props: React.ComponentProps<typeof MenubarPrimitive.Menu>)
|
|
|
11
12
|
const MenubarGroup = (props: React.ComponentProps<typeof MenubarPrimitive.Group>) => (
|
|
12
13
|
<MenubarPrimitive.Group {...props} />
|
|
13
14
|
);
|
|
14
|
-
const MenubarPortal = (
|
|
15
|
-
<MenubarPrimitive.Portal {...props} />
|
|
16
|
-
);
|
|
15
|
+
const MenubarPortal = withDirectionalPortal(MenubarPrimitive.Portal);
|
|
17
16
|
const MenubarSub = (props: React.ComponentProps<typeof MenubarPrimitive.Sub>) => (
|
|
18
17
|
<MenubarPrimitive.Sub {...props} />
|
|
19
18
|
);
|
|
@@ -24,7 +23,14 @@ const MenubarRadioGroup = (props: React.ComponentProps<typeof MenubarPrimitive.R
|
|
|
24
23
|
const itemCls =
|
|
25
24
|
"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";
|
|
26
25
|
const contentCls =
|
|
27
|
-
|
|
26
|
+
// Capped to the room Radix reports it has, so large text cannot push the surface past the window —
|
|
27
|
+
// see the note in popover.tsx for the measurement that prompted it.
|
|
28
|
+
// `min-w` yields to the available room, which is not a detail: `min-width` beats `max-width` in CSS,
|
|
29
|
+
// so the cap above was unenforceable on its own. Measured on a 390px phone with the root font size
|
|
30
|
+
// doubled — 8rem becomes 256px, the cap resolved to 193px, and the menu came out 256px wide with its
|
|
31
|
+
// right edge 63px past the window and those items unreachable. `min(...)` keeps the comfortable
|
|
32
|
+
// minimum whenever there is room for it and gives it up when there is not.
|
|
33
|
+
"z-overlay max-w-[var(--radix-popper-available-width)] min-w-[min(12rem,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]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:duration-fast data-[state=open]:ease-enter";
|
|
28
34
|
|
|
29
35
|
const Menubar = React.forwardRef<
|
|
30
36
|
React.ElementRef<typeof MenubarPrimitive.Root>,
|
|
@@ -57,9 +63,9 @@ const MenubarSubTrigger = React.forwardRef<
|
|
|
57
63
|
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
|
58
64
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { inset?: boolean }
|
|
59
65
|
>(({ className, inset, children, ...props }, ref) => (
|
|
60
|
-
<MenubarPrimitive.SubTrigger ref={ref} className={cn(itemCls, "data-[state=open]:bg-accent", inset && "
|
|
66
|
+
<MenubarPrimitive.SubTrigger ref={ref} className={cn(itemCls, "data-[state=open]:bg-accent", inset && "ps-8", className)} {...props}>
|
|
61
67
|
{children}
|
|
62
|
-
<ChevronRight className="
|
|
68
|
+
<ChevronRight className="me-0 ms-auto size-4 rtl:-scale-x-100" />
|
|
63
69
|
</MenubarPrimitive.SubTrigger>
|
|
64
70
|
));
|
|
65
71
|
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
|
@@ -76,7 +82,7 @@ const MenubarContent = React.forwardRef<
|
|
|
76
82
|
React.ElementRef<typeof MenubarPrimitive.Content>,
|
|
77
83
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
|
78
84
|
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
|
|
79
|
-
<
|
|
85
|
+
<MenubarPortal>
|
|
80
86
|
<MenubarPrimitive.Content
|
|
81
87
|
ref={ref}
|
|
82
88
|
align={align}
|
|
@@ -85,7 +91,7 @@ const MenubarContent = React.forwardRef<
|
|
|
85
91
|
className={cn(contentCls, className)}
|
|
86
92
|
{...props}
|
|
87
93
|
/>
|
|
88
|
-
</
|
|
94
|
+
</MenubarPortal>
|
|
89
95
|
));
|
|
90
96
|
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
|
91
97
|
|
|
@@ -93,7 +99,7 @@ const MenubarItem = React.forwardRef<
|
|
|
93
99
|
React.ElementRef<typeof MenubarPrimitive.Item>,
|
|
94
100
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { inset?: boolean }
|
|
95
101
|
>(({ className, inset, ...props }, ref) => (
|
|
96
|
-
<MenubarPrimitive.Item ref={ref} className={cn(itemCls, inset && "
|
|
102
|
+
<MenubarPrimitive.Item ref={ref} className={cn(itemCls, inset && "ps-8", className)} {...props} />
|
|
97
103
|
));
|
|
98
104
|
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
|
99
105
|
|
|
@@ -101,8 +107,8 @@ const MenubarCheckboxItem = React.forwardRef<
|
|
|
101
107
|
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
|
102
108
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
|
103
109
|
>(({ className, children, checked, ...props }, ref) => (
|
|
104
|
-
<MenubarPrimitive.CheckboxItem ref={ref} className={cn(itemCls, "
|
|
105
|
-
<span className="absolute
|
|
110
|
+
<MenubarPrimitive.CheckboxItem ref={ref} className={cn(itemCls, "ps-8", className)} checked={checked} {...props}>
|
|
111
|
+
<span className="absolute start-2 flex size-3.5 items-center justify-center">
|
|
106
112
|
<MenubarPrimitive.ItemIndicator>
|
|
107
113
|
<Check className="size-4" />
|
|
108
114
|
</MenubarPrimitive.ItemIndicator>
|
|
@@ -116,8 +122,8 @@ const MenubarRadioItem = React.forwardRef<
|
|
|
116
122
|
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
|
117
123
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
|
118
124
|
>(({ className, children, ...props }, ref) => (
|
|
119
|
-
<MenubarPrimitive.RadioItem ref={ref} className={cn(itemCls, "
|
|
120
|
-
<span className="absolute
|
|
125
|
+
<MenubarPrimitive.RadioItem ref={ref} className={cn(itemCls, "ps-8", className)} {...props}>
|
|
126
|
+
<span className="absolute start-2 flex size-3.5 items-center justify-center">
|
|
121
127
|
<MenubarPrimitive.ItemIndicator>
|
|
122
128
|
<Circle className="size-2 fill-current" />
|
|
123
129
|
</MenubarPrimitive.ItemIndicator>
|
|
@@ -131,7 +137,7 @@ const MenubarLabel = React.forwardRef<
|
|
|
131
137
|
React.ElementRef<typeof MenubarPrimitive.Label>,
|
|
132
138
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { inset?: boolean }
|
|
133
139
|
>(({ className, inset, ...props }, ref) => (
|
|
134
|
-
<MenubarPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "
|
|
140
|
+
<MenubarPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "ps-8", className)} {...props} />
|
|
135
141
|
));
|
|
136
142
|
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
|
137
143
|
|
|
@@ -144,7 +150,7 @@ const MenubarSeparator = React.forwardRef<
|
|
|
144
150
|
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
|
145
151
|
|
|
146
152
|
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
147
|
-
<span className={cn("
|
|
153
|
+
<span className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
|
|
148
154
|
);
|
|
149
155
|
MenubarShortcut.displayName = "MenubarShortcut";
|
|
150
156
|
|
package/src/components/modal.tsx
CHANGED
|
@@ -68,9 +68,10 @@ function Modal({
|
|
|
68
68
|
<DialogOverlay />
|
|
69
69
|
<DialogPrimitive.Content
|
|
70
70
|
className={cn(
|
|
71
|
+
// rtl-ok: centering trick, direction-agnostic by construction — same decision as dialog.tsx
|
|
71
72
|
"fixed left-1/2 top-1/2 z-overlay flex w-full max-w-[480px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl bg-background font-sans",
|
|
72
|
-
"shadow-
|
|
73
|
-
"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",
|
|
73
|
+
"shadow-xl",
|
|
74
|
+
"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",
|
|
74
75
|
className,
|
|
75
76
|
)}
|
|
76
77
|
>
|