@tangle-network/ui 11.34.0 → 11.36.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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/lib/control-presentation.d.ts +34 -1
- package/dist/lib/control-presentation.js +1 -1
- package/dist/lib/focus.d.ts +2 -2
- package/dist/lib/focus.js +3 -2
- package/dist/lib/motion.d.ts +4 -0
- package/dist/lib/motion.js +7 -0
- package/dist/lib/surface-motion.js +84 -0
- package/dist/presentation.d.ts +88 -0
- package/dist/presentation.js +183 -0
- package/dist/primitives/alert-dialog.js +1 -1
- package/dist/primitives/alert.d.ts +1 -2
- package/dist/primitives/button.d.ts +2 -5
- package/dist/primitives/button.js +3 -30
- package/dist/primitives/card.js +3 -7
- package/dist/primitives/date-picker.d.ts +1 -1
- package/dist/primitives/dialog.js +29 -16
- package/dist/primitives/empty-state.js +6 -5
- package/dist/primitives/heading.d.ts +1 -0
- package/dist/primitives/heading.js +3 -2
- package/dist/primitives/help-text.js +2 -1
- package/dist/primitives/input.d.ts +1 -4
- package/dist/primitives/input.js +3 -24
- package/dist/primitives/label.js +1 -2
- package/dist/primitives/metric-strip.js +7 -12
- package/dist/primitives/page-header.d.ts +1 -0
- package/dist/primitives/page-header.js +7 -6
- package/dist/primitives/page-shell.d.ts +8 -2
- package/dist/primitives/page-shell.js +21 -2
- package/dist/primitives/pagination.js +1 -1
- package/dist/primitives/segmented-control.d.ts +6 -3
- package/dist/primitives/segmented-control.js +105 -56
- package/dist/primitives/sheet.js +29 -19
- package/dist/primitives/skeleton.js +1 -1
- package/dist/primitives/status-pill.d.ts +3 -1
- package/dist/primitives/status-pill.js +12 -3
- package/dist/primitives/table.js +10 -9
- package/dist/primitives/tabs.js +3 -12
- package/dist/primitives/tooltip.js +11 -3
- package/dist/primitives.d.ts +3 -2
- package/dist/primitives.js +2 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +15 -5
- package/src/lib/control-presentation.ts +2 -4
- package/src/lib/focus.ts +3 -2
- package/src/lib/motion.ts +7 -0
- package/src/lib/surface-motion.tsx +78 -0
- package/src/presentation.ts +205 -0
- package/src/primitives/alert.tsx +1 -2
- package/src/primitives/button.tsx +4 -40
- package/src/primitives/card.tsx +5 -10
- package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +2 -1
- package/src/primitives/control-presentation.test.tsx +2 -7
- package/src/primitives/dialog.tsx +19 -18
- package/src/primitives/empty-state.tsx +6 -5
- package/src/primitives/heading.tsx +4 -2
- package/src/primitives/help-text.tsx +3 -2
- package/src/primitives/input.tsx +3 -31
- package/src/primitives/label.tsx +2 -4
- package/src/primitives/metric-strip.tsx +8 -17
- package/src/primitives/page-header.tsx +8 -5
- package/src/primitives/page-shell.tsx +13 -2
- package/src/primitives/segmented-control.stories.tsx +89 -0
- package/src/primitives/segmented-control.test.tsx +20 -7
- package/src/primitives/segmented-control.tsx +90 -29
- package/src/primitives/sheet.tsx +16 -11
- package/src/primitives/skeleton.tsx +1 -1
- package/src/primitives/status-pill.test.tsx +1 -0
- package/src/primitives/status-pill.tsx +10 -4
- package/src/primitives/table.tsx +10 -9
- package/src/primitives/tabs.tsx +5 -17
- package/src/primitives/tooltip.tsx +6 -5
- package/src/styles/compat.css +12 -0
- package/src/styles/presentation.css +30 -0
- package/src/primitives/page-shell.test.tsx +0 -24
package/src/lib/focus.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { controlMotion } from "./motion";
|
|
1
2
|
import { ixField } from "./interaction";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -30,7 +31,7 @@ import { ixField } from "./interaction";
|
|
|
30
31
|
* Tailwind emits `focus:` after `hover:`.
|
|
31
32
|
*/
|
|
32
33
|
export const focusField =
|
|
33
|
-
|
|
34
|
+
`border-border ${controlMotion} hover:border-[var(--border-strong)] focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)]`;
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* The field treatment for a container that wraps a borderless field, such as a
|
|
@@ -50,7 +51,7 @@ export const focusField =
|
|
|
50
51
|
* borderless field inside stays transparent.
|
|
51
52
|
*/
|
|
52
53
|
export const focusFieldWithin =
|
|
53
|
-
`focus-field-within
|
|
54
|
+
`focus-field-within ${controlMotion} ${ixField} has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:border-[var(--focus-border)] has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-3 has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-[var(--focus-halo)]`;
|
|
54
55
|
|
|
55
56
|
/** Error state for a field: a danger hairline and the same focus shape in danger. Compose after `focusField`. */
|
|
56
57
|
export const focusFieldInvalid =
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Brand motion roles contain duration AND easing; consume complete shorthands. */
|
|
2
|
+
export const controlMotion =
|
|
3
|
+
"[transition:color_var(--motion-control),background-color_var(--motion-control),border-color_var(--motion-control),box-shadow_var(--motion-control),transform_var(--motion-control),scale_var(--motion-control),opacity_var(--motion-control)] motion-reduce:transition-none";
|
|
4
|
+
|
|
5
|
+
/** Radix presence markers carry the scoped role; SurfaceMotion owns interruptible geometry. */
|
|
6
|
+
export const surfaceMotion =
|
|
7
|
+
"data-[state=open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=delayed-open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=instant-open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=closed]:[animation:tangle-surface-absent_var(--motion-dismiss)_both] motion-reduce:animate-none!";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { animate } from "motion";
|
|
6
|
+
|
|
7
|
+
type Side = "top" | "right" | "bottom" | "left";
|
|
8
|
+
type SurfaceProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
9
|
+
kind: "dialog" | "sheet" | "overlay" | "tooltip";
|
|
10
|
+
side?: Side;
|
|
11
|
+
asChild?: boolean;
|
|
12
|
+
"data-state"?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Radix owns state, focus and presence; Motion only retargets the visible object. */
|
|
16
|
+
export const SurfaceMotion = React.forwardRef<HTMLDivElement, SurfaceProps>(
|
|
17
|
+
({ kind, side = "right", asChild, "data-state": state, ...props }, forwardedRef) => {
|
|
18
|
+
const element = React.useRef<HTMLDivElement | null>(null);
|
|
19
|
+
const initialized = React.useRef(false);
|
|
20
|
+
const controls = React.useRef<ReturnType<typeof animate> | undefined>(undefined);
|
|
21
|
+
const ref = React.useCallback<React.RefCallback<HTMLDivElement>>((node) => {
|
|
22
|
+
element.current = node;
|
|
23
|
+
if (typeof forwardedRef === "function") {
|
|
24
|
+
const cleanup: unknown = forwardedRef(node);
|
|
25
|
+
if (typeof cleanup === "function") return () => { element.current = null; cleanup(); };
|
|
26
|
+
} else if (forwardedRef) forwardedRef.current = node;
|
|
27
|
+
}, [forwardedRef]);
|
|
28
|
+
|
|
29
|
+
React.useLayoutEffect(() => {
|
|
30
|
+
const node = element.current;
|
|
31
|
+
if (!node) return;
|
|
32
|
+
const preference = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
|
|
33
|
+
const run = () => {
|
|
34
|
+
const closed = state === "closed";
|
|
35
|
+
const bounds = node.getBoundingClientRect();
|
|
36
|
+
const hidden = {
|
|
37
|
+
opacity: kind === "sheet" ? 1 : 0,
|
|
38
|
+
scale: kind === "dialog" || kind === "tooltip" ? 0.98 : 1,
|
|
39
|
+
x: kind === "sheet" ? side === "left" ? -bounds.width : side === "right" ? bounds.width : 0 : 0,
|
|
40
|
+
y: kind === "sheet" ? side === "top" ? -bounds.height : side === "bottom" ? bounds.height : 0 : 0,
|
|
41
|
+
};
|
|
42
|
+
const target = closed ? hidden : { opacity: 1, scale: 1, x: 0, y: 0 };
|
|
43
|
+
const style = getComputedStyle(node);
|
|
44
|
+
// The nonvisual marker resolves the full role, including scoped calc().
|
|
45
|
+
const time = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(style.animationDuration.split(",")[0]!.trim());
|
|
46
|
+
const seconds = time ? Number(time[1]) / (time[2] === "ms" ? 1000 : 1) : 0;
|
|
47
|
+
const place = (value: typeof target) => {
|
|
48
|
+
node.style.opacity = String(value.opacity);
|
|
49
|
+
node.style.transform = `translateX(${value.x}px) translateY(${value.y}px) scale(${value.scale})`;
|
|
50
|
+
};
|
|
51
|
+
// Missing styles and reduced motion settle synchronously. No semantic
|
|
52
|
+
// transition waits for a JavaScript completion callback.
|
|
53
|
+
if (preference?.matches || seconds <= 0 || style.animationName === "none") {
|
|
54
|
+
controls.current?.stop();
|
|
55
|
+
place(target);
|
|
56
|
+
if (initialized.current) controls.current = animate(node, target, { duration: 0 });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!initialized.current) { place(hidden); initialized.current = true; }
|
|
60
|
+
// Critically damped response: <0.01% residual at the role's deadline.
|
|
61
|
+
// Reusing this element's Motion values preserves position and velocity
|
|
62
|
+
// on reversal; a duration-based spring would discard velocity.
|
|
63
|
+
const frequency = 12 / seconds;
|
|
64
|
+
controls.current = animate(node, target, {
|
|
65
|
+
type: "spring", stiffness: frequency * frequency, damping: 2 * frequency,
|
|
66
|
+
restDelta: 0.001, restSpeed: 0.01,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
run();
|
|
70
|
+
preference?.addEventListener("change", run);
|
|
71
|
+
return () => preference?.removeEventListener("change", run);
|
|
72
|
+
}, [state, kind, side]);
|
|
73
|
+
React.useEffect(() => () => controls.current?.stop(), []);
|
|
74
|
+
const Component = asChild ? Slot : "div";
|
|
75
|
+
return <Component {...props} data-state={state} ref={ref} />;
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
SurfaceMotion.displayName = "SurfaceMotion";
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/** React-free presentation. Consumers scan this published source with Tailwind. */
|
|
2
|
+
import { surfaceMotion } from "./lib/motion";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { controlHeight, controlMotion, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation";
|
|
5
|
+
import { focusRing, focusFieldInvalid } from "./lib/focus";
|
|
6
|
+
import { ixControl, ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction";
|
|
7
|
+
import { cn } from "./lib/utils";
|
|
8
|
+
export { controlHeight, controlMotion, controlRadius, controlSizes, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation";
|
|
9
|
+
|
|
10
|
+
export const buttonVariants = cva(
|
|
11
|
+
`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
default:
|
|
16
|
+
`bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97] ${ixControl}`,
|
|
17
|
+
destructive:
|
|
18
|
+
`bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97] ${ixControl}`,
|
|
19
|
+
outline:
|
|
20
|
+
`border border-border bg-card hover:bg-muted active:scale-[0.97] text-foreground ${ixControl}`,
|
|
21
|
+
secondary:
|
|
22
|
+
`bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97] ${ixControl}`,
|
|
23
|
+
ghost:
|
|
24
|
+
`hover:text-foreground text-muted-foreground border border-transparent ${ixControlSoft}`,
|
|
25
|
+
link: `text-[var(--accent-text)] underline-offset-4 hover:underline ${ixControlSoft}`,
|
|
26
|
+
sandbox:
|
|
27
|
+
`bg-[var(--btn-primary-bg)] text-[var(--btn-primary-text)] border border-[var(--border-accent)] hover:bg-[var(--btn-primary-hover)] active:scale-[0.97] ${ixControl}`,
|
|
28
|
+
},
|
|
29
|
+
size: {
|
|
30
|
+
default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
|
|
31
|
+
md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
|
|
32
|
+
sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} px-3`,
|
|
33
|
+
lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} px-7`,
|
|
34
|
+
display: "tangle-control-display",
|
|
35
|
+
xl: "h-13 rounded-xl px-9 text-base",
|
|
36
|
+
icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md}`,
|
|
37
|
+
"icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm}`,
|
|
38
|
+
"icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg}`,
|
|
39
|
+
...controlSizes,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: {
|
|
43
|
+
variant: "default",
|
|
44
|
+
size: "default",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export const inputVariants = cva(
|
|
51
|
+
cn(
|
|
52
|
+
"flex w-full border py-1 placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium",
|
|
53
|
+
fieldPresentation,
|
|
54
|
+
fieldTouchText,
|
|
55
|
+
),
|
|
56
|
+
{
|
|
57
|
+
variants: {
|
|
58
|
+
// `default` and `sandbox` render the same field; both names stay valid.
|
|
59
|
+
variant: {
|
|
60
|
+
default: "",
|
|
61
|
+
sandbox: "",
|
|
62
|
+
error: focusFieldInvalid,
|
|
63
|
+
},
|
|
64
|
+
size: {
|
|
65
|
+
default: fieldSizes.md,
|
|
66
|
+
md: fieldSizes.md,
|
|
67
|
+
sm: fieldSizes.sm,
|
|
68
|
+
lg: fieldSizes.lg,
|
|
69
|
+
compact: cn(fieldSizes.md, controlSizes.compact),
|
|
70
|
+
touch: cn(fieldSizes.md, controlSizes.touch),
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
defaultVariants: {
|
|
74
|
+
variant: "default",
|
|
75
|
+
size: "default",
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
export const tablePresentation = {
|
|
82
|
+
root: "w-full caption-bottom text-sm",
|
|
83
|
+
wrapper: "relative w-full overflow-auto",
|
|
84
|
+
head: "[&_tr]:border-b",
|
|
85
|
+
body: "[&_tr:last-child]:border-0",
|
|
86
|
+
footer: "border-t bg-card font-medium [&>tr]:last:border-b-0",
|
|
87
|
+
row: `border-border border-b ${controlMotion} hover:bg-muted/50 data-[state=selected]:bg-muted/50`,
|
|
88
|
+
columnHeader: "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
89
|
+
cell: "p-4 align-middle [&:has([role=checkbox])]:pr-0",
|
|
90
|
+
caption: "mt-4 text-muted-foreground text-sm",
|
|
91
|
+
} as const;
|
|
92
|
+
|
|
93
|
+
export const emptyStatePresentation = {
|
|
94
|
+
root: "flex flex-col items-center justify-center px-4 py-16 text-center",
|
|
95
|
+
icon: "mb-4 rounded-full bg-muted p-4 text-muted-foreground",
|
|
96
|
+
title: "font-semibold text-lg",
|
|
97
|
+
description: "mt-2 max-w-sm text-muted-foreground text-sm",
|
|
98
|
+
action: "mt-6",
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
export const metricPresentation = {
|
|
102
|
+
strip: "grid min-w-0 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]",
|
|
103
|
+
item: "min-w-0 border-border p-4 sm:p-5",
|
|
104
|
+
value: "mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl",
|
|
105
|
+
label: "flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm",
|
|
106
|
+
labelText: "min-w-0 whitespace-normal [overflow-wrap:anywhere]",
|
|
107
|
+
hint: "mt-0.5 whitespace-normal [overflow-wrap:anywhere] text-[var(--text-dim)] text-xs",
|
|
108
|
+
} as const;
|
|
109
|
+
|
|
110
|
+
export const metricLayouts = {
|
|
111
|
+
3: "grid-cols-2 sm:grid-cols-3 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(3n+1))]:border-l",
|
|
112
|
+
4: "grid-cols-2 sm:grid-cols-4 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(4n+1))]:border-l",
|
|
113
|
+
5: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(5n+1))]:border-l",
|
|
114
|
+
6: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(6n+1))]:border-l",
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
export const dialogPresentation = {
|
|
118
|
+
overlay: "fixed inset-0 z-50 bg-black/40",
|
|
119
|
+
transition: surfaceMotion,
|
|
120
|
+
position: "fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
|
|
121
|
+
surface: "gap-4 rounded-lg border bg-card p-6 shadow-lg",
|
|
122
|
+
close: `absolute top-4 right-4 rounded-sm opacity-70 ${controlMotion} hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground`,
|
|
123
|
+
header: "flex flex-col space-y-1.5 text-center sm:text-left",
|
|
124
|
+
footer: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
125
|
+
title: "font-semibold text-lg leading-none tracking-tight",
|
|
126
|
+
description: "text-muted-foreground text-sm",
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
129
|
+
/** Native-dialog adapter for Hono/static markup. Radix remains the React Dialog/Sheet owner. */
|
|
130
|
+
export function bindDialog(dialog: HTMLDialogElement, trigger: HTMLElement): () => void {
|
|
131
|
+
const sync = () => trigger.setAttribute("aria-expanded", String(dialog.open));
|
|
132
|
+
const show = () => { if (!dialog.open) dialog.showModal(); sync(); };
|
|
133
|
+
const close = () => { sync(); trigger.focus(); };
|
|
134
|
+
const click = (event: MouseEvent) => {
|
|
135
|
+
const target = event.target;
|
|
136
|
+
if (target instanceof Element && target.closest("[data-dialog-close]")) dialog.close();
|
|
137
|
+
};
|
|
138
|
+
// Native modal inertness blocks page controls, but Tab can still visit browser
|
|
139
|
+
// chrome at a boundary. Keep this reading surface's keyboard cycle local.
|
|
140
|
+
const key = (event: KeyboardEvent) => {
|
|
141
|
+
if (event.key !== "Tab" || !dialog.open) return;
|
|
142
|
+
const candidates = [...dialog.querySelectorAll<HTMLElement>("*")]
|
|
143
|
+
.filter(element => (element.tabIndex >= 0 || (element.isContentEditable && !element.hasAttribute("tabindex")))
|
|
144
|
+
&& !element.matches(":disabled") && !element.closest("[inert]")
|
|
145
|
+
&& element.getClientRects().length > 0 && getComputedStyle(element).visibility === "visible");
|
|
146
|
+
const radio = (element: Element): element is HTMLInputElement => element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
147
|
+
const sameRadioGroup = (a: Element, b: Element) => radio(a) && radio(b) && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
|
|
148
|
+
const stops = candidates.filter(element => {
|
|
149
|
+
if (!radio(element)) return true;
|
|
150
|
+
const group = candidates.filter(candidate => sameRadioGroup(element, candidate));
|
|
151
|
+
const checked = group.find(candidate => (candidate as HTMLInputElement).checked);
|
|
152
|
+
return element === (checked ?? (event.shiftKey ? group.at(-1) : group[0]));
|
|
153
|
+
}).sort((a, b) => (a.tabIndex > 0 ? a.tabIndex : Infinity) - (b.tabIndex > 0 ? b.tabIndex : Infinity));
|
|
154
|
+
const first = stops[0], last = stops.at(-1), active = document.activeElement;
|
|
155
|
+
if (!first) { event.preventDefault(); dialog.focus(); return; }
|
|
156
|
+
const atBoundary = (boundary: HTMLElement) => active === boundary || (active instanceof Element && sameRadioGroup(active, boundary));
|
|
157
|
+
if (active === dialog || (event.shiftKey ? atBoundary(first) : atBoundary(last!))) {
|
|
158
|
+
event.preventDefault(); (event.shiftKey ? last! : first).focus();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
trigger.setAttribute("aria-haspopup", "dialog");
|
|
162
|
+
if (dialog.id) trigger.setAttribute("aria-controls", dialog.id);
|
|
163
|
+
sync();trigger.addEventListener("click",show);dialog.addEventListener("close",close);dialog.addEventListener("click",click);dialog.addEventListener("keydown",key);
|
|
164
|
+
return () => {trigger.removeEventListener("click",show);dialog.removeEventListener("close",close);dialog.removeEventListener("click",click);dialog.removeEventListener("keydown",key);};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const labelVariants = cva(
|
|
168
|
+
"font-medium text-[length:var(--font-size-label,0.875rem)] leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
export const helpPresentation = {
|
|
172
|
+
root: "text-[length:var(--font-size-help,0.75rem)] leading-4",
|
|
173
|
+
error: "font-medium text-[var(--surface-danger-text)]",
|
|
174
|
+
hint: "text-[var(--text-dim)]",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const cardPresentation = {
|
|
178
|
+
root: `rounded-[var(--radius-lg)] border text-card-foreground ${controlMotion}`,
|
|
179
|
+
padding: "p-4",
|
|
180
|
+
variants: {
|
|
181
|
+
default: "bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)]",
|
|
182
|
+
elevated: "bg-muted/50 border-border shadow-[var(--shadow-card)]",
|
|
183
|
+
glass: "bg-card/80 backdrop-blur-xl border-border shadow-[var(--shadow-card)]",
|
|
184
|
+
sandbox: "bg-muted/50 border-primary/20 shadow-[var(--shadow-accent)]",
|
|
185
|
+
},
|
|
186
|
+
} as const;
|
|
187
|
+
|
|
188
|
+
/** Canonical segmented, pill and underline navigation presentation. */
|
|
189
|
+
export const tabsPresentation = {
|
|
190
|
+
list: {
|
|
191
|
+
default:
|
|
192
|
+
"inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
|
193
|
+
pills: "inline-flex items-center gap-2",
|
|
194
|
+
underline: "inline-flex items-center gap-4 border-b border-border",
|
|
195
|
+
},
|
|
196
|
+
trigger: {
|
|
197
|
+
default:
|
|
198
|
+
`inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 ${ixControlSoft} ${ixSelected}`,
|
|
199
|
+
pills:
|
|
200
|
+
`inline-flex items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 data-[state=inactive]:text-muted-foreground ${ixControlSoft} ${ixSelected}`,
|
|
201
|
+
underline:
|
|
202
|
+
`inline-flex items-center justify-center whitespace-nowrap rounded-t-md pb-3 text-sm font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 border-b-2 border-transparent data-[state=inactive]:text-muted-foreground ${ixTabUnderline}`,
|
|
203
|
+
}
|
|
204
|
+
} as const;
|
|
205
|
+
export { ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction";
|
package/src/primitives/alert.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
|
-
import type { StatusTone } from "./status-pill";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* An inline message that stays on the page: a policy note above a form, a
|
|
@@ -14,7 +13,7 @@ import type { StatusTone } from "./status-pill";
|
|
|
14
13
|
* `danger` and `warning` announce themselves (`role="alert"`); the quieter
|
|
15
14
|
* tones are `role="status"` so a screen reader is not interrupted by a note.
|
|
16
15
|
*/
|
|
17
|
-
export type AlertTone =
|
|
16
|
+
export type AlertTone = "neutral" | "info" | "success" | "warning" | "danger";
|
|
18
17
|
|
|
19
18
|
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
20
19
|
tone?: AlertTone;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
-
import {
|
|
2
|
+
import { buttonVariants } from "../presentation";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { controlHeight, controlMotion, controlRadius, controlSizes, controlSquare, controlText } from "../lib/control-presentation";
|
|
5
4
|
import { focusRing } from "../lib/focus";
|
|
6
5
|
import { ixControl, ixControlSoft, ixSelected } from "../lib/interaction";
|
|
7
6
|
import { cn } from "../lib/utils";
|
|
@@ -14,43 +13,6 @@ import { cn } from "../lib/utils";
|
|
|
14
13
|
// `ixControl` (green edge and glow on hover, press and after a click); ghost,
|
|
15
14
|
// link and bare take `ixControlSoft`, a wash that never glows. A toggle button
|
|
16
15
|
// (any `aria-pressed`) adds `ixControl` + `ixSelected` whatever its variant.
|
|
17
|
-
const buttonVariants = cva(
|
|
18
|
-
`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium transition-[color,background-color,border-color,box-shadow,transform,scale] ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
|
|
19
|
-
{
|
|
20
|
-
variants: {
|
|
21
|
-
variant: {
|
|
22
|
-
default:
|
|
23
|
-
`bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97] ${ixControl}`,
|
|
24
|
-
destructive:
|
|
25
|
-
`bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97] ${ixControl}`,
|
|
26
|
-
outline:
|
|
27
|
-
`border border-border bg-card hover:bg-muted active:scale-[0.97] text-foreground ${ixControl}`,
|
|
28
|
-
secondary:
|
|
29
|
-
`bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97] ${ixControl}`,
|
|
30
|
-
ghost:
|
|
31
|
-
`hover:text-foreground text-muted-foreground border border-transparent ${ixControlSoft}`,
|
|
32
|
-
link: `text-[var(--accent-text)] underline-offset-4 hover:underline ${ixControlSoft}`,
|
|
33
|
-
sandbox:
|
|
34
|
-
`bg-[var(--btn-primary-bg)] text-[var(--btn-primary-text)] border border-[var(--border-accent)] hover:bg-[var(--btn-primary-hover)] active:scale-[0.97] ${ixControl}`,
|
|
35
|
-
},
|
|
36
|
-
size: {
|
|
37
|
-
default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
|
|
38
|
-
md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
|
|
39
|
-
sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} px-3`,
|
|
40
|
-
lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} px-7`,
|
|
41
|
-
xl: "h-13 rounded-xl px-9 text-base",
|
|
42
|
-
icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md}`,
|
|
43
|
-
"icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm}`,
|
|
44
|
-
"icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg}`,
|
|
45
|
-
...controlSizes,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
defaultVariants: {
|
|
49
|
-
variant: "default",
|
|
50
|
-
size: "default",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
16
|
|
|
55
17
|
// `bare` keeps the shared focus ring, disabled and loading behavior and drops
|
|
56
18
|
// every surface, size and type class. It is for pressable rows, tiles and pills
|
|
@@ -75,7 +37,7 @@ export interface ButtonProps
|
|
|
75
37
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
76
38
|
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "sandbox" | "bare" | null;
|
|
77
39
|
/** sm 32px, default/md 36px (`--control-height`), lg 44px; icon sizes are squares on the same heights. */
|
|
78
|
-
size?: "default" | "md" | "sm" | "lg" | "xl" | "icon" | "icon-sm" | "icon-lg" | "compact" | "touch" | null;
|
|
40
|
+
size?: "default" | "md" | "sm" | "lg" | "xl" | "display" | "icon" | "icon-sm" | "icon-lg" | "compact" | "touch" | null;
|
|
79
41
|
asChild?: boolean;
|
|
80
42
|
loading?: boolean;
|
|
81
43
|
children?: React.ReactNode;
|
|
@@ -136,6 +98,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
136
98
|
<Slot
|
|
137
99
|
className={buttonClassName(variant, size, props["aria-pressed"], className)}
|
|
138
100
|
ref={ref}
|
|
101
|
+
data-variant={size === "display" ? variant ?? "default" : undefined}
|
|
139
102
|
{...props}
|
|
140
103
|
{...(isDisabled ? disabledHandlers(props) : {})}
|
|
141
104
|
>
|
|
@@ -161,6 +124,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
161
124
|
<button
|
|
162
125
|
className={buttonClassName(variant, size, props["aria-pressed"], className)}
|
|
163
126
|
ref={ref}
|
|
127
|
+
data-variant={size === "display" ? variant ?? "default" : undefined}
|
|
164
128
|
{...props}
|
|
165
129
|
disabled={disabled || loading}
|
|
166
130
|
aria-busy={loading || props["aria-busy"]}
|
package/src/primitives/card.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { ixControl } from "../lib/interaction";
|
|
3
|
+
import { cardPresentation } from "../presentation";
|
|
3
4
|
import { cn } from "../lib/utils";
|
|
4
5
|
import { Heading } from "./heading";
|
|
5
6
|
|
|
@@ -24,20 +25,14 @@ const Card = React.forwardRef<
|
|
|
24
25
|
interactive?: boolean;
|
|
25
26
|
}
|
|
26
27
|
>(({ className, variant = "default", hover = false, interactive = false, ...props }, ref) => {
|
|
27
|
-
|
|
28
|
-
default: "bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)]",
|
|
29
|
-
elevated: "bg-muted/50 border-border shadow-[var(--shadow-card)]",
|
|
30
|
-
glass: "bg-card/80 backdrop-blur-xl border-border shadow-[var(--shadow-card)]",
|
|
31
|
-
sandbox: "bg-muted/50 border-primary/20 shadow-[var(--shadow-accent)]",
|
|
32
|
-
};
|
|
28
|
+
|
|
33
29
|
|
|
34
30
|
return (
|
|
35
31
|
<div
|
|
36
32
|
ref={ref}
|
|
37
33
|
className={cn(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
variants[variant],
|
|
34
|
+
cardPresentation.root,
|
|
35
|
+
cardPresentation.variants[variant],
|
|
41
36
|
(hover || interactive) && ixControl,
|
|
42
37
|
className,
|
|
43
38
|
)}
|
|
@@ -98,7 +93,7 @@ const CardContent = React.forwardRef<
|
|
|
98
93
|
>(({ className, ...props }, ref) => (
|
|
99
94
|
<div
|
|
100
95
|
ref={ref}
|
|
101
|
-
className={cn("
|
|
96
|
+
className={cn(cardPresentation.padding, "[&:not(:first-child)]:pt-0", className)}
|
|
102
97
|
{...props}
|
|
103
98
|
/>
|
|
104
99
|
));
|
|
@@ -71,7 +71,8 @@ describe("Sheet", () => {
|
|
|
71
71
|
);
|
|
72
72
|
fireEvent.click(screen.getByText("Open chat"));
|
|
73
73
|
const dialog = await screen.findByRole("dialog", { name: "Assistant" });
|
|
74
|
-
expect(dialog
|
|
74
|
+
expect(dialog).toHaveAttribute("data-state", "open");
|
|
75
|
+
expect(dialog.style.opacity).toBe("1");
|
|
75
76
|
fireEvent.click(screen.getByRole("button", { name: "Close" }));
|
|
76
77
|
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
|
77
78
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cleanup, render, screen } from "@testing-library/react";
|
|
3
3
|
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
-
import { controlHeight, controlText,
|
|
4
|
+
import { controlHeight, controlText, fieldPresentation } from "../lib/control-presentation";
|
|
5
5
|
import { HelpText } from "./help-text";
|
|
6
6
|
import { Label } from "./label";
|
|
7
7
|
import { Button, buttonVariants } from "./button";
|
|
@@ -11,7 +11,7 @@ import { Select, SelectTrigger, SelectValue } from "./select";
|
|
|
11
11
|
afterEach(cleanup);
|
|
12
12
|
|
|
13
13
|
describe("shared control presentation", () => {
|
|
14
|
-
it("uses the field well, not the border/track alias, with scoped foreground
|
|
14
|
+
it("uses the field well, not the border/track alias, with scoped foreground", () => {
|
|
15
15
|
// The well is the brand's --well context variable; Toolbar can still pick the fill.
|
|
16
16
|
expect(fieldPresentation).toContain("bg-[var(--field-surface,var(--well))]");
|
|
17
17
|
expect(fieldPresentation.split(" ")).not.toContain("bg-input");
|
|
@@ -20,10 +20,6 @@ describe("shared control presentation", () => {
|
|
|
20
20
|
expect(fieldPresentation.split(" ")).toContain("border-[var(--field-border)]");
|
|
21
21
|
expect(fieldPresentation.split(" ")).not.toContain("border-border");
|
|
22
22
|
expect(fieldPresentation).toContain("text-foreground");
|
|
23
|
-
expect(fieldPresentation).not.toContain("duration-150");
|
|
24
|
-
expect(controlMotion).toContain("duration-[var(--duration-fast)]");
|
|
25
|
-
expect(controlMotion).not.toContain("--transition-fast");
|
|
26
|
-
expect(controlMotion).toContain("motion-reduce:duration-0");
|
|
27
23
|
});
|
|
28
24
|
|
|
29
25
|
it("puts Button, Input, Textarea and SelectTrigger on one height and text scale", () => {
|
|
@@ -116,7 +112,6 @@ describe("shared control presentation", () => {
|
|
|
116
112
|
expect(buttonVariants({ variant: "destructive" })).toContain("text-destructive-foreground");
|
|
117
113
|
expect(buttonVariants({ variant: "sandbox" })).toContain("text-[var(--btn-primary-text)]");
|
|
118
114
|
expect(buttonVariants()).not.toContain("transition-all");
|
|
119
|
-
expect(buttonVariants()).toContain("box-shadow,transform,scale");
|
|
120
115
|
expect(buttonVariants()).toContain("motion-reduce:active:scale-100");
|
|
121
116
|
});
|
|
122
117
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SurfaceMotion } from "../lib/surface-motion";
|
|
2
|
+
import { dialogPresentation } from "../presentation";
|
|
1
3
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
2
4
|
import { X } from "lucide-react";
|
|
3
5
|
import * as React from "react";
|
|
@@ -15,17 +17,17 @@ const DialogClose = DialogPrimitive.Close;
|
|
|
15
17
|
const DialogOverlay = React.forwardRef<
|
|
16
18
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
17
19
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
18
|
-
>(({ className, ...props }, ref) => (
|
|
20
|
+
>(({ className, asChild, children, ...props }, ref) => (
|
|
19
21
|
<DialogPrimitive.Overlay
|
|
20
22
|
ref={ref}
|
|
21
23
|
className={cn(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
24
|
+
dialogPresentation.overlay,
|
|
25
|
+
dialogPresentation.transition,
|
|
25
26
|
className,
|
|
26
27
|
)}
|
|
27
28
|
{...props}
|
|
28
|
-
|
|
29
|
+
asChild
|
|
30
|
+
><SurfaceMotion kind="overlay" asChild={asChild}>{children}</SurfaceMotion></DialogPrimitive.Overlay>
|
|
29
31
|
));
|
|
30
32
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
31
33
|
|
|
@@ -40,7 +42,7 @@ const DialogContent = React.forwardRef<
|
|
|
40
42
|
*/
|
|
41
43
|
hideCloseButton?: boolean;
|
|
42
44
|
}
|
|
43
|
-
>(({ className, children, variant = "default", hideCloseButton = false, ...props }, ref) => {
|
|
45
|
+
>(({ className, children, variant = "default", hideCloseButton = false, asChild, ...props }, ref) => {
|
|
44
46
|
const variants = {
|
|
45
47
|
default: "border-border",
|
|
46
48
|
sandbox: "border-[var(--border-accent)] shadow-[var(--shadow-accent)]",
|
|
@@ -52,23 +54,21 @@ const DialogContent = React.forwardRef<
|
|
|
52
54
|
<DialogPrimitive.Content
|
|
53
55
|
ref={ref}
|
|
54
56
|
className={cn(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
59
|
-
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
60
|
-
"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
|
|
61
|
-
"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
57
|
+
dialogPresentation.position,
|
|
58
|
+
dialogPresentation.surface,
|
|
59
|
+
dialogPresentation.transition,
|
|
62
60
|
variants[variant],
|
|
63
61
|
className,
|
|
64
62
|
)}
|
|
65
63
|
{...props}
|
|
64
|
+
asChild
|
|
66
65
|
>
|
|
66
|
+
<SurfaceMotion kind="dialog" asChild={asChild}>
|
|
67
67
|
{children}
|
|
68
68
|
{!hideCloseButton && (
|
|
69
69
|
<DialogPrimitive.Close
|
|
70
70
|
className={cn(
|
|
71
|
-
|
|
71
|
+
dialogPresentation.close,
|
|
72
72
|
focusRing,
|
|
73
73
|
)}
|
|
74
74
|
>
|
|
@@ -76,6 +76,7 @@ const DialogContent = React.forwardRef<
|
|
|
76
76
|
<span className="sr-only">Close</span>
|
|
77
77
|
</DialogPrimitive.Close>
|
|
78
78
|
)}
|
|
79
|
+
</SurfaceMotion>
|
|
79
80
|
</DialogPrimitive.Content>
|
|
80
81
|
</DialogPortal>
|
|
81
82
|
);
|
|
@@ -88,7 +89,7 @@ const DialogHeader = ({
|
|
|
88
89
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
89
90
|
<div
|
|
90
91
|
className={cn(
|
|
91
|
-
|
|
92
|
+
dialogPresentation.header,
|
|
92
93
|
className,
|
|
93
94
|
)}
|
|
94
95
|
{...props}
|
|
@@ -102,7 +103,7 @@ const DialogFooter = ({
|
|
|
102
103
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
103
104
|
<div
|
|
104
105
|
className={cn(
|
|
105
|
-
|
|
106
|
+
dialogPresentation.footer,
|
|
106
107
|
className,
|
|
107
108
|
)}
|
|
108
109
|
{...props}
|
|
@@ -117,7 +118,7 @@ const DialogTitle = React.forwardRef<
|
|
|
117
118
|
<DialogPrimitive.Title
|
|
118
119
|
ref={ref}
|
|
119
120
|
className={cn(
|
|
120
|
-
|
|
121
|
+
dialogPresentation.title,
|
|
121
122
|
className,
|
|
122
123
|
)}
|
|
123
124
|
{...props}
|
|
@@ -131,7 +132,7 @@ const DialogDescription = React.forwardRef<
|
|
|
131
132
|
>(({ className, ...props }, ref) => (
|
|
132
133
|
<DialogPrimitive.Description
|
|
133
134
|
ref={ref}
|
|
134
|
-
className={cn(
|
|
135
|
+
className={cn(dialogPresentation.description, className)}
|
|
135
136
|
{...props}
|
|
136
137
|
/>
|
|
137
138
|
));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { emptyStatePresentation } from "../presentation";
|
|
1
2
|
import * as React from "react";
|
|
2
3
|
import { cn } from "../lib/utils";
|
|
3
4
|
|
|
@@ -17,23 +18,23 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
17
18
|
<div
|
|
18
19
|
ref={ref}
|
|
19
20
|
className={cn(
|
|
20
|
-
|
|
21
|
+
emptyStatePresentation.root,
|
|
21
22
|
className,
|
|
22
23
|
)}
|
|
23
24
|
{...props}
|
|
24
25
|
>
|
|
25
26
|
{icon && (
|
|
26
|
-
<div className=
|
|
27
|
+
<div className={emptyStatePresentation.icon}>
|
|
27
28
|
{icon}
|
|
28
29
|
</div>
|
|
29
30
|
)}
|
|
30
|
-
<Title className=
|
|
31
|
+
<Title className={emptyStatePresentation.title}>{title}</Title>
|
|
31
32
|
{description && (
|
|
32
|
-
<p className=
|
|
33
|
+
<p className={emptyStatePresentation.description}>
|
|
33
34
|
{description}
|
|
34
35
|
</p>
|
|
35
36
|
)}
|
|
36
|
-
{action && <div className=
|
|
37
|
+
{action && <div className={emptyStatePresentation.action}>{action}</div>}
|
|
37
38
|
</div>
|
|
38
39
|
);
|
|
39
40
|
},
|