@bearmenu/ui 0.8.39 → 0.8.41
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/dist/{chunk-FVSKGCK3.js → chunk-DZHKUUYV.js} +15 -1
- package/dist/{chunk-5SGKGFMN.js → chunk-K6ACNU2L.js} +6 -1
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +1 -1
- package/dist/components/carousel.js +1 -1
- package/dist/components/coach-marks.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command.d.ts +3 -3
- package/dist/components/empty-state.d.ts +14 -2
- package/dist/components/empty-state.js +15 -7
- package/dist/components/event-detail-panel.js +1 -1
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-family-block.js +1 -1
- package/dist/components/menu-index-list.js +1 -1
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/pagination.js +1 -1
- package/dist/components/pan-zoom-viewport.js +1 -1
- package/dist/components/responsive-overlay.d.ts +11 -0
- package/dist/components/responsive-overlay.js +16 -4
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +2 -2
- package/dist/components/venue-events-block.js +1 -1
- package/package.json +1 -1
- package/src/components/button.test.tsx +10 -0
- package/src/components/button.tsx +6 -1
- package/src/components/card.stories.tsx +20 -1
- package/src/components/card.test.tsx +27 -0
- package/src/components/card.tsx +14 -0
- package/src/components/empty-state.stories.tsx +19 -2
- package/src/components/empty-state.test.tsx +28 -2
- package/src/components/empty-state.tsx +21 -5
- package/src/components/responsive-overlay.test.tsx +45 -0
- package/src/components/responsive-overlay.tsx +27 -0
|
@@ -36,7 +36,16 @@ var cardVariants = cva(
|
|
|
36
36
|
// several-hundred-pixel slab that translates under the cursor is a
|
|
37
37
|
// wobble rather than a lift. A panel that size delegates interaction to
|
|
38
38
|
// its children.
|
|
39
|
-
elevatedPanel: "bg-card border border-border shadow-[var(--shadow-panel)]"
|
|
39
|
+
elevatedPanel: "bg-card border border-border shadow-[var(--shadow-panel)]",
|
|
40
|
+
// The booking frames' settings/report SECTION card (Settings Rules U0,
|
|
41
|
+
// Reports P0): a smaller, page-content-scale radius than every other
|
|
42
|
+
// variant's `rounded-3xl` — 14px on a phone, 16px from `sm` — carried
|
|
43
|
+
// entirely by `--shadow-card` with no border at all. Not a bigger
|
|
44
|
+
// `elevated`: that variant is fixed at 24px and keeps its hairline in
|
|
45
|
+
// both themes on purpose (see its note); this one is for the specific
|
|
46
|
+
// page-section shape the booking design draws, not a general
|
|
47
|
+
// border-dropping replacement for `elevated`.
|
|
48
|
+
section: "rounded-[14px] bg-card shadow-[var(--shadow-card)] sm:rounded-[16px]"
|
|
40
49
|
},
|
|
41
50
|
size: {
|
|
42
51
|
sm: "[&_[data-slot=card-header]]:p-4 [&_[data-slot=card-content]]:p-4 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-4 [&_[data-slot=card-footer]]:pt-0",
|
|
@@ -68,6 +77,11 @@ var cardVariants = cva(
|
|
|
68
77
|
variant: "elevated",
|
|
69
78
|
interactive: true,
|
|
70
79
|
class: "hover:shadow-[var(--shadow-card-hover)]"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
variant: "section",
|
|
83
|
+
interactive: true,
|
|
84
|
+
class: "hover:shadow-[var(--shadow-card-hover)]"
|
|
71
85
|
}
|
|
72
86
|
],
|
|
73
87
|
defaultVariants: {
|
|
@@ -70,7 +70,12 @@ var buttonVariants = cva(
|
|
|
70
70
|
size: {
|
|
71
71
|
xs: "h-8 rounded-lg px-3 text-xs [&_svg]:size-3.5",
|
|
72
72
|
sm: "h-9 rounded-lg px-4",
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The booking frames' 44px controls (Reports/02-p4 "Try again" and its
|
|
75
|
+
* report-range trigger, Calendar/08-r0 "New reservation") all draw
|
|
76
|
+
* 10px, not the base `rounded-xl` (12px) this size otherwise inherits.
|
|
77
|
+
*/
|
|
78
|
+
default: "h-11 rounded-[10px] px-6 py-2",
|
|
74
79
|
lg: "h-12 rounded-2xl px-8",
|
|
75
80
|
/** The `ink` action's shape. Owns the radius so no variant fights it. */
|
|
76
81
|
pill: "h-11 rounded-full px-5 text-[13.5px] font-bold",
|
|
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
import { VariantProps } from 'class-variance-authority';
|
|
5
5
|
|
|
6
6
|
declare const badgeVariants: (props?: ({
|
|
7
|
-
variant?: "
|
|
7
|
+
variant?: "secondary" | "default" | "success" | "warning" | "destructive" | "outline" | "glass" | "overlay" | "brown" | "code" | "onPrimary" | "confirmed" | "pending" | "completed" | "assignee" | "softSuccess" | "softWarning" | "softDestructive" | "softNeutral" | null | undefined;
|
|
8
8
|
size?: "status" | "chip" | null | undefined;
|
|
9
9
|
live?: boolean | null | undefined;
|
|
10
10
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
@@ -3,8 +3,8 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
7
|
-
size?: "
|
|
6
|
+
variant?: "secondary" | "default" | "link" | "warning" | "destructive" | "outline" | "ghost" | "glass" | "overlay" | "ink" | "brown" | "cta" | null | undefined;
|
|
7
|
+
size?: "default" | "icon" | "cta" | "xs" | "sm" | "lg" | "pill" | "cta-sm" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
8
8
|
fullWidth?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
@@ -3,12 +3,12 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const cardVariants: (props?: ({
|
|
6
|
-
variant?: "outline" | "glass" | "muted" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
6
|
+
variant?: "outline" | "glass" | "section" | "muted" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
interactive?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
11
|
-
variant?: "outline" | "glass" | "muted" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
11
|
+
variant?: "outline" | "glass" | "section" | "muted" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
12
12
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
13
13
|
interactive?: boolean | null | undefined;
|
|
14
14
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
|
package/dist/components/card.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants } from '../chunk-
|
|
1
|
+
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants } from '../chunk-DZHKUUYV.js';
|
|
2
2
|
import '../chunk-ZLMRVBD7.js';
|
|
3
3
|
import '../chunk-FEK5XGZW.js';
|
|
4
4
|
import '../chunk-MMUBH76A.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-2ADCZXZJ.js';
|
|
2
2
|
import { Command, CommandInput, CommandList, CommandLoading, CommandEmpty, CommandGroup, CommandItem } from '../chunk-PGQXVJXW.js';
|
|
3
3
|
import '../chunk-IQIJCMG2.js';
|
|
4
|
-
import { Button } from '../chunk-
|
|
4
|
+
import { Button } from '../chunk-K6ACNU2L.js';
|
|
5
5
|
import '../chunk-RIH2IS7X.js';
|
|
6
6
|
import '../chunk-5YNIQ2BL.js';
|
|
7
7
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
@@ -28,7 +28,7 @@ declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<
|
|
|
28
28
|
ref?: React.Ref<HTMLInputElement>;
|
|
29
29
|
} & {
|
|
30
30
|
asChild?: boolean;
|
|
31
|
-
}, "asChild" | "key" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "
|
|
31
|
+
}, "asChild" | "key" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "onChange" | "value" | "type"> & {
|
|
32
32
|
value?: string;
|
|
33
33
|
onValueChange?: (search: string) => void;
|
|
34
34
|
} & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -64,7 +64,7 @@ declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
|
|
|
64
64
|
ref?: React.Ref<HTMLDivElement>;
|
|
65
65
|
} & {
|
|
66
66
|
asChild?: boolean;
|
|
67
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "
|
|
67
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "heading" | "value"> & {
|
|
68
68
|
heading?: React.ReactNode;
|
|
69
69
|
value?: string;
|
|
70
70
|
forceMount?: boolean;
|
|
@@ -82,7 +82,7 @@ declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
|
|
|
82
82
|
ref?: React.Ref<HTMLDivElement>;
|
|
83
83
|
} & {
|
|
84
84
|
asChild?: boolean;
|
|
85
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "
|
|
85
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "onSelect" | "value" | "disabled"> & {
|
|
86
86
|
disabled?: boolean;
|
|
87
87
|
onSelect?: (value: string) => void;
|
|
88
88
|
value?: string;
|
|
@@ -3,6 +3,12 @@ import * as React from 'react';
|
|
|
3
3
|
type EmptyStateAction = {
|
|
4
4
|
label: string;
|
|
5
5
|
onClick: () => void;
|
|
6
|
+
/**
|
|
7
|
+
* Rendered before the label, sized like the DS button's own icons (the
|
|
8
|
+
* `Button` base class forces every child `<svg>` to 16px) — "Try again"
|
|
9
|
+
* carries a refresh glyph, "New reservation" a plus.
|
|
10
|
+
*/
|
|
11
|
+
icon?: React.ReactNode;
|
|
6
12
|
};
|
|
7
13
|
type EmptyStateVariant = "default" | "search" | "error";
|
|
8
14
|
interface EmptyStateProps {
|
|
@@ -23,8 +29,9 @@ interface EmptyStateProps {
|
|
|
23
29
|
size?: "sm" | "md" | "lg";
|
|
24
30
|
/**
|
|
25
31
|
* `card` draws the booking pages' whole-body state card (reports/02-p4,
|
|
26
|
-
* calendar/02-c2): a
|
|
27
|
-
* `none` (the default) leaves the
|
|
32
|
+
* calendar/02-c2): a 14px-radius card surface (16px from `sm`) with the
|
|
33
|
+
* resting card shadow and no border. `none` (the default) leaves the
|
|
34
|
+
* surface to the container it sits in.
|
|
28
35
|
*/
|
|
29
36
|
surface?: "none" | "card";
|
|
30
37
|
className?: string;
|
|
@@ -36,6 +43,11 @@ interface EmptyStateProps {
|
|
|
36
43
|
* title to body, 16px from body to the action. The action is full-width on a
|
|
37
44
|
* phone and hugs its label from `sm`.
|
|
38
45
|
*
|
|
46
|
+
* The tile is 20px-radius (22px from `sm`); the title is 16px on a phone,
|
|
47
|
+
* 18px from `sm`. On `surface="card"` the card itself is 14px-radius (16px
|
|
48
|
+
* from `sm`), no border, with 20px side padding to the action row — measured
|
|
49
|
+
* off the frames' DOM (reports/02-p4, calendar/02-c2), not eyeballed.
|
|
50
|
+
*
|
|
39
51
|
* The error variant says "error" with its glyph and its tile's wash, not by
|
|
40
52
|
* painting the title and body red: red body copy is both harder to read and
|
|
41
53
|
* louder than a load failure the user can retry.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Button } from '../chunk-
|
|
1
|
+
import { Button } from '../chunk-K6ACNU2L.js';
|
|
2
2
|
import '../chunk-RIH2IS7X.js';
|
|
3
3
|
import '../chunk-5YNIQ2BL.js';
|
|
4
4
|
import { Text } from '../chunk-ZLMRVBD7.js';
|
|
@@ -58,7 +58,7 @@ var EmptyState = React.forwardRef(
|
|
|
58
58
|
className: cn(
|
|
59
59
|
"flex flex-col items-center justify-center text-center",
|
|
60
60
|
sizeStyles[size],
|
|
61
|
-
surface === "card" && "rounded-
|
|
61
|
+
surface === "card" && "rounded-[14px] bg-card px-5 shadow-[var(--shadow-card)] sm:rounded-[16px]",
|
|
62
62
|
className
|
|
63
63
|
)
|
|
64
64
|
}, props), {
|
|
@@ -70,25 +70,33 @@ var EmptyState = React.forwardRef(
|
|
|
70
70
|
className: cn(
|
|
71
71
|
// The glyph size is the tile's, whatever size the caller's icon
|
|
72
72
|
// node asked for — a 40px icon in a 64px tile has no margin.
|
|
73
|
-
|
|
73
|
+
// No border: the frames (reports/02-p4, calendar/02-c2) draw the
|
|
74
|
+
// tile with the shadow alone.
|
|
75
|
+
"mb-4 flex size-16 shrink-0 items-center justify-center rounded-[20px] shadow-sm sm:size-18 sm:rounded-[22px] [&_svg]:size-7",
|
|
74
76
|
variant === "error" ? "bg-destructive/5" : "bg-card"
|
|
75
77
|
),
|
|
76
78
|
children: icon != null ? icon : DEFAULT_ICON[variant]
|
|
77
79
|
}
|
|
78
80
|
),
|
|
79
|
-
/* @__PURE__ */ jsx(Text, { as: "h3", weight: "semibold", className: "text-lg", children: title }),
|
|
81
|
+
/* @__PURE__ */ jsx(Text, { as: "h3", weight: "semibold", className: "text-base sm:text-lg", children: title }),
|
|
80
82
|
description ? /* @__PURE__ */ jsx(Text, { as: "p", color: "muted", className: "mt-3 max-w-md text-sm", children: description }) : null,
|
|
81
83
|
(action || secondaryAction) && /* @__PURE__ */ jsxs("div", { className: "mt-4 flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center", children: [
|
|
82
|
-
action && /* @__PURE__ */
|
|
84
|
+
action && /* @__PURE__ */ jsxs(
|
|
83
85
|
Button,
|
|
84
86
|
{
|
|
85
87
|
onClick: action.onClick,
|
|
86
88
|
variant: actionVariant != null ? actionVariant : DEFAULT_ACTION_VARIANT[variant],
|
|
87
89
|
className: "glass-hover",
|
|
88
|
-
children:
|
|
90
|
+
children: [
|
|
91
|
+
action.icon,
|
|
92
|
+
action.label
|
|
93
|
+
]
|
|
89
94
|
}
|
|
90
95
|
),
|
|
91
|
-
secondaryAction && /* @__PURE__ */
|
|
96
|
+
secondaryAction && /* @__PURE__ */ jsxs(Button, { onClick: secondaryAction.onClick, variant: "outline", className: "glass-hover", children: [
|
|
97
|
+
secondaryAction.icon,
|
|
98
|
+
secondaryAction.label
|
|
99
|
+
] })
|
|
92
100
|
] })
|
|
93
101
|
]
|
|
94
102
|
})
|
|
@@ -2,7 +2,7 @@ import { resolveEventEntryFee } from '../chunk-DNSXMHYX.js';
|
|
|
2
2
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
3
3
|
import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
|
|
4
4
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
5
|
-
import { Button } from '../chunk-
|
|
5
|
+
import { Button } from '../chunk-K6ACNU2L.js';
|
|
6
6
|
import '../chunk-RIH2IS7X.js';
|
|
7
7
|
import '../chunk-5YNIQ2BL.js';
|
|
8
8
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
@@ -3,7 +3,7 @@ import { Tag } from '../chunk-MWB7S54O.js';
|
|
|
3
3
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
4
4
|
import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
|
|
5
5
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
6
|
-
import { buttonVariants } from '../chunk-
|
|
6
|
+
import { buttonVariants } from '../chunk-K6ACNU2L.js';
|
|
7
7
|
import '../chunk-RIH2IS7X.js';
|
|
8
8
|
import '../chunk-5YNIQ2BL.js';
|
|
9
9
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
@@ -3,7 +3,7 @@ import { HueChip } from '../chunk-6VOWT4YR.js';
|
|
|
3
3
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
4
4
|
import { PhotoGround, PHOTO_GROUND } from '../chunk-SWD6CJ35.js';
|
|
5
5
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
6
|
-
import { buttonVariants } from '../chunk-
|
|
6
|
+
import { buttonVariants } from '../chunk-K6ACNU2L.js';
|
|
7
7
|
import '../chunk-RIH2IS7X.js';
|
|
8
8
|
import '../chunk-5YNIQ2BL.js';
|
|
9
9
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
2
2
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
3
|
-
import { Button } from '../chunk-
|
|
3
|
+
import { Button } from '../chunk-K6ACNU2L.js';
|
|
4
4
|
import '../chunk-RIH2IS7X.js';
|
|
5
5
|
import '../chunk-5YNIQ2BL.js';
|
|
6
6
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
@@ -2,7 +2,7 @@ import { Checkbox } from '../chunk-L3CDXVEP.js';
|
|
|
2
2
|
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-2ADCZXZJ.js';
|
|
3
3
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-PGQXVJXW.js';
|
|
4
4
|
import '../chunk-IQIJCMG2.js';
|
|
5
|
-
import { Button } from '../chunk-
|
|
5
|
+
import { Button } from '../chunk-K6ACNU2L.js';
|
|
6
6
|
import '../chunk-RIH2IS7X.js';
|
|
7
7
|
import '../chunk-5YNIQ2BL.js';
|
|
8
8
|
import { Badge } from '../chunk-FQAIYKAS.js';
|
|
@@ -53,6 +53,17 @@ type ResponsiveOverlayProps = {
|
|
|
53
53
|
className?: string;
|
|
54
54
|
/** Classes for the scrolling body. */
|
|
55
55
|
bodyClassName?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Focus this element on open instead of Radix's default — the first
|
|
58
|
+
* tabbable in DOM order, which in every mode here is the header's round
|
|
59
|
+
* Close button, since the header renders before the body. That is the
|
|
60
|
+
* right target for most overlays, but a confirm-style body (a primary
|
|
61
|
+
* action, a field that should be pre-filled) wants focus lower down.
|
|
62
|
+
* Passing a ref calls `event.preventDefault()` on Radix's
|
|
63
|
+
* `onOpenAutoFocus` and focuses this element instead; omitting the prop
|
|
64
|
+
* keeps today's default in every mode (dialog, sheet, drawer).
|
|
65
|
+
*/
|
|
66
|
+
initialFocusRef?: React.RefObject<HTMLElement | null>;
|
|
56
67
|
};
|
|
57
68
|
/**
|
|
58
69
|
* ResponsiveOverlay — one overlay authored once: a `Drawer` under md, a
|
|
@@ -55,11 +55,20 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
55
55
|
modal = true,
|
|
56
56
|
wideFrom = "md",
|
|
57
57
|
className,
|
|
58
|
-
bodyClassName
|
|
58
|
+
bodyClassName,
|
|
59
|
+
initialFocusRef
|
|
59
60
|
}, ref) => {
|
|
60
61
|
const isWide = useMediaQuery(wideFrom === "lg" ? DESKTOP_MEDIA_QUERY : MD_MEDIA_QUERY);
|
|
61
62
|
const container = isWide ? desktop : "drawer";
|
|
62
63
|
useFocusReturn(open);
|
|
64
|
+
const handleOpenAutoFocus = React.useCallback(
|
|
65
|
+
(event) => {
|
|
66
|
+
if (!(initialFocusRef == null ? void 0 : initialFocusRef.current)) return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
initialFocusRef.current.focus();
|
|
69
|
+
},
|
|
70
|
+
[initialFocusRef]
|
|
71
|
+
);
|
|
63
72
|
const describedBy = subtitle ? {} : { "aria-describedby": void 0 };
|
|
64
73
|
const Title = CONTAINER_TITLE[container];
|
|
65
74
|
const Description = CONTAINER_DESCRIPTION[container];
|
|
@@ -106,7 +115,8 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
106
115
|
side: "right",
|
|
107
116
|
attachment: "inset",
|
|
108
117
|
hideClose: true,
|
|
109
|
-
overlayClassName: "bg-scrim-overlay backdrop-blur-none"
|
|
118
|
+
overlayClassName: "bg-scrim-overlay backdrop-blur-none",
|
|
119
|
+
onOpenAutoFocus: handleOpenAutoFocus
|
|
110
120
|
}, describedBy), {
|
|
111
121
|
"data-slot": "responsive-overlay-sheet",
|
|
112
122
|
"data-modal": modal ? "true" : "false"
|
|
@@ -130,7 +140,8 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
130
140
|
ref,
|
|
131
141
|
size,
|
|
132
142
|
showCloseButton: false,
|
|
133
|
-
overlayClassName: "bg-scrim-overlay backdrop-blur-none"
|
|
143
|
+
overlayClassName: "bg-scrim-overlay backdrop-blur-none",
|
|
144
|
+
onOpenAutoFocus: handleOpenAutoFocus
|
|
134
145
|
}, describedBy), {
|
|
135
146
|
className: cn(
|
|
136
147
|
// The dialog's own `grid gap-4 p-6` would put air between the
|
|
@@ -150,7 +161,8 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
150
161
|
DrawerContent,
|
|
151
162
|
__spreadProps(__spreadValues({
|
|
152
163
|
ref,
|
|
153
|
-
overlayClassName: "bg-scrim-overlay"
|
|
164
|
+
overlayClassName: "bg-scrim-overlay",
|
|
165
|
+
onOpenAutoFocus: handleOpenAutoFocus
|
|
154
166
|
}, describedBy), {
|
|
155
167
|
className: cn("flex max-h-[85vh] flex-col p-0", className),
|
|
156
168
|
children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-2ADCZXZJ.js';
|
|
2
2
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-PGQXVJXW.js';
|
|
3
3
|
import '../chunk-IQIJCMG2.js';
|
|
4
|
-
import { Button } from '../chunk-
|
|
4
|
+
import { Button } from '../chunk-K6ACNU2L.js';
|
|
5
5
|
import '../chunk-RIH2IS7X.js';
|
|
6
6
|
import '../chunk-5YNIQ2BL.js';
|
|
7
7
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
@@ -4,11 +4,11 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
|
|
5
5
|
declare const spinnerVariants: (props?: ({
|
|
6
6
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
7
|
-
tone?: "
|
|
7
|
+
tone?: "primary" | "default" | "inherit" | "destructive" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Spinner: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
10
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
11
|
-
tone?: "
|
|
11
|
+
tone?: "primary" | "default" | "inherit" | "destructive" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
/** Visually-hidden label for screen readers. Defaults to "Loading". */
|
|
14
14
|
label?: string;
|
package/dist/components/tag.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const tagVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "primary" | "default" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Tag: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
|
-
variant?: "
|
|
10
|
+
variant?: "primary" | "default" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
|
|
11
11
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
onRemove?: () => void;
|
|
@@ -22,8 +22,8 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
22
22
|
* Weights are 400 / 600 / 700. 500 is not in the system.
|
|
23
23
|
*/
|
|
24
24
|
declare const textVariants: (props?: ({
|
|
25
|
-
variant?: "
|
|
26
|
-
color?: "
|
|
25
|
+
variant?: "small" | "body" | "display" | "h2" | "h3" | "label" | "caption" | "h1" | "h4" | "body-lg" | "body-sm" | "overline" | null | undefined;
|
|
26
|
+
color?: "primary" | "default" | "inherit" | "destructive" | "muted" | "primary-foreground" | null | undefined;
|
|
27
27
|
weight?: "bold" | "medium" | "normal" | "semibold" | null | undefined;
|
|
28
28
|
leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
|
|
29
29
|
tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
2
2
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
3
|
-
import { Card } from '../chunk-
|
|
3
|
+
import { Card } from '../chunk-DZHKUUYV.js';
|
|
4
4
|
import '../chunk-ZLMRVBD7.js';
|
|
5
5
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
6
6
|
import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
|
package/package.json
CHANGED
|
@@ -158,6 +158,16 @@ describe("Button component", () => {
|
|
|
158
158
|
expect(screen.getByRole("button").className).toContain("h-9");
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
// The booking frames' 44px controls (Reports/02-p4 "Try again" + its report-
|
|
162
|
+
// range trigger, Calendar/08-r0 "New reservation") draw a 10px radius, not
|
|
163
|
+
// the base `rounded-xl` (12px) this size otherwise inherits.
|
|
164
|
+
it("draws the default size at a 10px radius, not the base rounded-xl", () => {
|
|
165
|
+
render(<Button>x</Button>);
|
|
166
|
+
const classes = screen.getByRole("button").className;
|
|
167
|
+
expect(classes).toContain("rounded-[10px]");
|
|
168
|
+
expect(classes).not.toMatch(/(^|\s)rounded-xl(\s|$)/);
|
|
169
|
+
});
|
|
170
|
+
|
|
161
171
|
it("applies fullWidth class when fullWidth is true", () => {
|
|
162
172
|
render(<Button fullWidth>x</Button>);
|
|
163
173
|
expect(screen.getByRole("button").className).toContain("w-full");
|
|
@@ -75,7 +75,12 @@ const buttonVariants = cva(
|
|
|
75
75
|
size: {
|
|
76
76
|
xs: "h-8 rounded-lg px-3 text-xs [&_svg]:size-3.5",
|
|
77
77
|
sm: "h-9 rounded-lg px-4",
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The booking frames' 44px controls (Reports/02-p4 "Try again" and its
|
|
80
|
+
* report-range trigger, Calendar/08-r0 "New reservation") all draw
|
|
81
|
+
* 10px, not the base `rounded-xl` (12px) this size otherwise inherits.
|
|
82
|
+
*/
|
|
83
|
+
default: "h-11 rounded-[10px] px-6 py-2",
|
|
79
84
|
lg: "h-12 rounded-2xl px-8",
|
|
80
85
|
/** The `ink` action's shape. Owns the radius so no variant fights it. */
|
|
81
86
|
pill: "h-11 rounded-full px-5 text-[13.5px] font-bold",
|
|
@@ -11,7 +11,7 @@ const meta = {
|
|
|
11
11
|
argTypes: {
|
|
12
12
|
variant: {
|
|
13
13
|
control: "select",
|
|
14
|
-
options: ["glass", "surface"],
|
|
14
|
+
options: ["glass", "surface", "outline", "muted", "elevated", "elevatedPanel", "section"],
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
17
|
} satisfies Meta<typeof Card>;
|
|
@@ -47,6 +47,25 @@ export const Surface: Story = {
|
|
|
47
47
|
),
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The booking frames' settings/report section shape (Settings Rules U0,
|
|
52
|
+
* Reports P0): 14px radius on a phone, 16px from `sm`, shadow only, no
|
|
53
|
+
* border. Distinct from `elevated`, which is a fixed 24px with a hairline.
|
|
54
|
+
*/
|
|
55
|
+
export const Section: Story = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<Card variant="section" className="w-[350px]">
|
|
58
|
+
<CardHeader>
|
|
59
|
+
<CardTitle>Booking rules</CardTitle>
|
|
60
|
+
<CardDescription>How far ahead guests can book, and the slot length.</CardDescription>
|
|
61
|
+
</CardHeader>
|
|
62
|
+
<CardContent>
|
|
63
|
+
<p className="text-sm text-muted-foreground">Section content goes here.</p>
|
|
64
|
+
</CardContent>
|
|
65
|
+
</Card>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
|
|
50
69
|
export const WithForm: Story = {
|
|
51
70
|
render: () => (
|
|
52
71
|
<Card className="w-[350px]">
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Card } from "./card";
|
|
4
|
+
|
|
5
|
+
describe("Card", () => {
|
|
6
|
+
it("draws the section variant borderless, at the booking frames' radius", () => {
|
|
7
|
+
render(<Card variant="section">Content</Card>);
|
|
8
|
+
const card = screen.getByText("Content");
|
|
9
|
+
expect(card).toHaveClass("rounded-[14px]", "sm:rounded-[16px]", "bg-card");
|
|
10
|
+
expect(card).not.toHaveClass("border", "border-border");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("keeps elevated's fixed radius and hairline, unaffected by the new variant", () => {
|
|
14
|
+
render(<Card variant="elevated">Content</Card>);
|
|
15
|
+
const card = screen.getByText("Content");
|
|
16
|
+
expect(card).toHaveClass("rounded-3xl", "border", "border-border");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("steps section's shadow up on hover when interactive", () => {
|
|
20
|
+
render(
|
|
21
|
+
<Card variant="section" interactive>
|
|
22
|
+
Content
|
|
23
|
+
</Card>
|
|
24
|
+
);
|
|
25
|
+
expect(screen.getByText("Content")).toHaveClass("hover:shadow-[var(--shadow-card-hover)]");
|
|
26
|
+
});
|
|
27
|
+
});
|
package/src/components/card.tsx
CHANGED
|
@@ -35,6 +35,15 @@ const cardVariants = cva(
|
|
|
35
35
|
// wobble rather than a lift. A panel that size delegates interaction to
|
|
36
36
|
// its children.
|
|
37
37
|
elevatedPanel: "bg-card border border-border shadow-[var(--shadow-panel)]",
|
|
38
|
+
// The booking frames' settings/report SECTION card (Settings Rules U0,
|
|
39
|
+
// Reports P0): a smaller, page-content-scale radius than every other
|
|
40
|
+
// variant's `rounded-3xl` — 14px on a phone, 16px from `sm` — carried
|
|
41
|
+
// entirely by `--shadow-card` with no border at all. Not a bigger
|
|
42
|
+
// `elevated`: that variant is fixed at 24px and keeps its hairline in
|
|
43
|
+
// both themes on purpose (see its note); this one is for the specific
|
|
44
|
+
// page-section shape the booking design draws, not a general
|
|
45
|
+
// border-dropping replacement for `elevated`.
|
|
46
|
+
section: "rounded-[14px] bg-card shadow-[var(--shadow-card)] sm:rounded-[16px]",
|
|
38
47
|
},
|
|
39
48
|
size: {
|
|
40
49
|
sm: "[&_[data-slot=card-header]]:p-4 [&_[data-slot=card-content]]:p-4 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-4 [&_[data-slot=card-footer]]:pt-0",
|
|
@@ -68,6 +77,11 @@ const cardVariants = cva(
|
|
|
68
77
|
interactive: true,
|
|
69
78
|
class: "hover:shadow-[var(--shadow-card-hover)]",
|
|
70
79
|
},
|
|
80
|
+
{
|
|
81
|
+
variant: "section",
|
|
82
|
+
interactive: true,
|
|
83
|
+
class: "hover:shadow-[var(--shadow-card-hover)]",
|
|
84
|
+
},
|
|
71
85
|
],
|
|
72
86
|
defaultVariants: {
|
|
73
87
|
variant: "surface",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { RefreshCw, Plus } from "lucide-react";
|
|
2
3
|
import { EmptyState, SearchEmptyState, ErrorEmptyState } from "./empty-state";
|
|
3
4
|
|
|
4
5
|
const meta: Meta<typeof EmptyState> = {
|
|
@@ -49,7 +50,10 @@ export const Error: Story = {
|
|
|
49
50
|
),
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The booking pages' whole-body failure (reports/02-p4, calendar/02-c2): a
|
|
55
|
+
* 14px/16px-radius borderless card and the recovery action's refresh glyph.
|
|
56
|
+
*/
|
|
53
57
|
export const ErrorCard: Story = {
|
|
54
58
|
render: () => (
|
|
55
59
|
<div className="bg-background p-4">
|
|
@@ -57,7 +61,20 @@ export const ErrorCard: Story = {
|
|
|
57
61
|
surface="card"
|
|
58
62
|
title="Couldn't load this report"
|
|
59
63
|
description="This is on our side, not an empty week. Try again in a moment."
|
|
60
|
-
action={{ label: "Try again", onClick: () => {} }}
|
|
64
|
+
action={{ label: "Try again", onClick: () => {}, icon: <RefreshCw /> }}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** The calendar day's failure, with the pinned footer's "New reservation" action alongside — both actions carry a leading glyph. */
|
|
71
|
+
export const ActionWithIcon: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<div className="bg-background p-4">
|
|
74
|
+
<EmptyState
|
|
75
|
+
title="No reservations yet"
|
|
76
|
+
description="This day is open. Start the first booking."
|
|
77
|
+
action={{ label: "New reservation", onClick: () => {}, icon: <Plus /> }}
|
|
61
78
|
/>
|
|
62
79
|
</div>
|
|
63
80
|
),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { RefreshCw } from "lucide-react";
|
|
4
5
|
import { EmptyState, ErrorEmptyState, SearchEmptyState } from "./empty-state";
|
|
5
6
|
|
|
6
7
|
const retry = { label: "Try again", onClick: () => {} };
|
|
@@ -46,15 +47,40 @@ describe("EmptyState", () => {
|
|
|
46
47
|
expect(tile).toHaveClass("size-16", "sm:size-18", "[&_svg]:size-7");
|
|
47
48
|
});
|
|
48
49
|
|
|
50
|
+
// reports/02-p4, calendar/02-c2: the tile draws the shadow alone, no border.
|
|
51
|
+
it("draws the icon tile borderless, with the shadow alone", () => {
|
|
52
|
+
render(<EmptyState title="Empty" description="" />);
|
|
53
|
+
const tile = document.querySelector('[data-slot="empty-state-icon"]');
|
|
54
|
+
expect(tile).toHaveClass("shadow-sm");
|
|
55
|
+
expect(tile).not.toHaveClass("border", "border-border");
|
|
56
|
+
});
|
|
57
|
+
|
|
49
58
|
it("renders no empty paragraph when there is no body", () => {
|
|
50
59
|
render(<EmptyState title="Empty" description="" />);
|
|
51
60
|
expect(screen.getByRole("status").querySelector("p")).toBeNull();
|
|
52
61
|
});
|
|
53
62
|
|
|
54
|
-
it("draws the card surface on request only", () => {
|
|
63
|
+
it("draws the card surface on request only, borderless with the frames' radius", () => {
|
|
55
64
|
const { rerender } = render(<EmptyState title="Empty" description="Nothing." />);
|
|
56
65
|
expect(screen.getByRole("status")).not.toHaveClass("bg-card");
|
|
57
66
|
rerender(<EmptyState title="Empty" description="Nothing." surface="card" />);
|
|
58
|
-
|
|
67
|
+
const card = screen.getByRole("status");
|
|
68
|
+
expect(card).toHaveClass("rounded-[14px]", "sm:rounded-[16px]", "bg-card", "px-5");
|
|
69
|
+
expect(card).not.toHaveClass("border", "border-border");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders the action's icon before its label", () => {
|
|
73
|
+
render(
|
|
74
|
+
<ErrorEmptyState
|
|
75
|
+
title="Couldn't load this report"
|
|
76
|
+
description="On our side."
|
|
77
|
+
action={{ label: "Try again", onClick: () => {}, icon: <RefreshCw data-testid="retry-icon" /> }}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
const button = screen.getByRole("button", { name: "Try again" });
|
|
81
|
+
const icon = screen.getByTestId("retry-icon");
|
|
82
|
+
expect(button).toContainElement(icon);
|
|
83
|
+
expect(button.firstElementChild).toBe(icon);
|
|
84
|
+
expect(button.textContent).toBe("Try again");
|
|
59
85
|
});
|
|
60
86
|
});
|
|
@@ -9,6 +9,12 @@ import { Search, AlertCircle, FileX } from "lucide-react";
|
|
|
9
9
|
type EmptyStateAction = {
|
|
10
10
|
label: string;
|
|
11
11
|
onClick: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Rendered before the label, sized like the DS button's own icons (the
|
|
14
|
+
* `Button` base class forces every child `<svg>` to 16px) — "Try again"
|
|
15
|
+
* carries a refresh glyph, "New reservation" a plus.
|
|
16
|
+
*/
|
|
17
|
+
icon?: React.ReactNode;
|
|
12
18
|
};
|
|
13
19
|
|
|
14
20
|
type EmptyStateVariant = "default" | "search" | "error";
|
|
@@ -31,8 +37,9 @@ interface EmptyStateProps {
|
|
|
31
37
|
size?: "sm" | "md" | "lg";
|
|
32
38
|
/**
|
|
33
39
|
* `card` draws the booking pages' whole-body state card (reports/02-p4,
|
|
34
|
-
* calendar/02-c2): a
|
|
35
|
-
* `none` (the default) leaves the
|
|
40
|
+
* calendar/02-c2): a 14px-radius card surface (16px from `sm`) with the
|
|
41
|
+
* resting card shadow and no border. `none` (the default) leaves the
|
|
42
|
+
* surface to the container it sits in.
|
|
36
43
|
*/
|
|
37
44
|
surface?: "none" | "card";
|
|
38
45
|
className?: string;
|
|
@@ -63,6 +70,11 @@ const DEFAULT_ICON: Record<EmptyStateVariant, React.ReactNode> = {
|
|
|
63
70
|
* title to body, 16px from body to the action. The action is full-width on a
|
|
64
71
|
* phone and hugs its label from `sm`.
|
|
65
72
|
*
|
|
73
|
+
* The tile is 20px-radius (22px from `sm`); the title is 16px on a phone,
|
|
74
|
+
* 18px from `sm`. On `surface="card"` the card itself is 14px-radius (16px
|
|
75
|
+
* from `sm`), no border, with 20px side padding to the action row — measured
|
|
76
|
+
* off the frames' DOM (reports/02-p4, calendar/02-c2), not eyeballed.
|
|
77
|
+
*
|
|
66
78
|
* The error variant says "error" with its glyph and its tile's wash, not by
|
|
67
79
|
* painting the title and body red: red body copy is both harder to read and
|
|
68
80
|
* louder than a load failure the user can retry.
|
|
@@ -93,7 +105,7 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
93
105
|
"flex flex-col items-center justify-center text-center",
|
|
94
106
|
sizeStyles[size],
|
|
95
107
|
surface === "card" &&
|
|
96
|
-
"rounded-
|
|
108
|
+
"rounded-[14px] bg-card px-5 shadow-[var(--shadow-card)] sm:rounded-[16px]",
|
|
97
109
|
className
|
|
98
110
|
)}
|
|
99
111
|
{...props}
|
|
@@ -106,7 +118,9 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
106
118
|
className={cn(
|
|
107
119
|
// The glyph size is the tile's, whatever size the caller's icon
|
|
108
120
|
// node asked for — a 40px icon in a 64px tile has no margin.
|
|
109
|
-
|
|
121
|
+
// No border: the frames (reports/02-p4, calendar/02-c2) draw the
|
|
122
|
+
// tile with the shadow alone.
|
|
123
|
+
"mb-4 flex size-16 shrink-0 items-center justify-center rounded-[20px] shadow-sm sm:size-18 sm:rounded-[22px] [&_svg]:size-7",
|
|
110
124
|
variant === "error" ? "bg-destructive/5" : "bg-card"
|
|
111
125
|
)}
|
|
112
126
|
>
|
|
@@ -114,7 +128,7 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
114
128
|
</span>
|
|
115
129
|
)}
|
|
116
130
|
|
|
117
|
-
<Text as="h3" weight="semibold" className="text-lg">
|
|
131
|
+
<Text as="h3" weight="semibold" className="text-base sm:text-lg">
|
|
118
132
|
{title}
|
|
119
133
|
</Text>
|
|
120
134
|
|
|
@@ -132,11 +146,13 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
132
146
|
variant={actionVariant ?? DEFAULT_ACTION_VARIANT[variant]}
|
|
133
147
|
className="glass-hover"
|
|
134
148
|
>
|
|
149
|
+
{action.icon}
|
|
135
150
|
{action.label}
|
|
136
151
|
</Button>
|
|
137
152
|
)}
|
|
138
153
|
{secondaryAction && (
|
|
139
154
|
<Button onClick={secondaryAction.onClick} variant="outline" className="glass-hover">
|
|
155
|
+
{secondaryAction.icon}
|
|
140
156
|
{secondaryAction.label}
|
|
141
157
|
</Button>
|
|
142
158
|
)}
|
|
@@ -295,6 +295,51 @@ describe("ResponsiveOverlay", () => {
|
|
|
295
295
|
});
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
+
describe("initialFocusRef", () => {
|
|
299
|
+
function WithTarget(props: Partial<React.ComponentProps<typeof ResponsiveOverlay>> = {}) {
|
|
300
|
+
const targetRef = React.useRef<HTMLButtonElement>(null);
|
|
301
|
+
return (
|
|
302
|
+
<ResponsiveOverlay
|
|
303
|
+
open
|
|
304
|
+
onOpenChange={() => {}}
|
|
305
|
+
title="Add an item"
|
|
306
|
+
closeLabel="Close"
|
|
307
|
+
initialFocusRef={targetRef}
|
|
308
|
+
{...props}
|
|
309
|
+
>
|
|
310
|
+
<button ref={targetRef} type="button">
|
|
311
|
+
Confirm
|
|
312
|
+
</button>
|
|
313
|
+
</ResponsiveOverlay>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
it("dialog: focuses the given element instead of the Close button", async () => {
|
|
318
|
+
setWidth(true);
|
|
319
|
+
render(<WithTarget />);
|
|
320
|
+
await waitFor(() => expect(screen.getByRole("button", { name: "Confirm" })).toHaveFocus());
|
|
321
|
+
expect(screen.getByRole("button", { name: "Close" })).not.toHaveFocus();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("sheet: focuses the given element instead of the Close button", async () => {
|
|
325
|
+
setWidth(true);
|
|
326
|
+
render(<WithTarget desktop="sheet" />);
|
|
327
|
+
await waitFor(() => expect(screen.getByRole("button", { name: "Confirm" })).toHaveFocus());
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("drawer: focuses the given element instead of the Close button", async () => {
|
|
331
|
+
setWidth(false);
|
|
332
|
+
render(<WithTarget />);
|
|
333
|
+
await waitFor(() => expect(screen.getByRole("button", { name: "Confirm" })).toHaveFocus());
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("keeps today's default (first tabbable) when the prop is omitted", async () => {
|
|
337
|
+
setWidth(true);
|
|
338
|
+
open();
|
|
339
|
+
await waitFor(() => expect(screen.getByRole("button", { name: "Close" })).toHaveFocus());
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
298
343
|
it("stays out of the DOM while closed", () => {
|
|
299
344
|
render(
|
|
300
345
|
<ResponsiveOverlay open={false} onOpenChange={() => {}} title="Add an item">
|
|
@@ -113,6 +113,17 @@ export type ResponsiveOverlayProps = {
|
|
|
113
113
|
className?: string;
|
|
114
114
|
/** Classes for the scrolling body. */
|
|
115
115
|
bodyClassName?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Focus this element on open instead of Radix's default — the first
|
|
118
|
+
* tabbable in DOM order, which in every mode here is the header's round
|
|
119
|
+
* Close button, since the header renders before the body. That is the
|
|
120
|
+
* right target for most overlays, but a confirm-style body (a primary
|
|
121
|
+
* action, a field that should be pre-filled) wants focus lower down.
|
|
122
|
+
* Passing a ref calls `event.preventDefault()` on Radix's
|
|
123
|
+
* `onOpenAutoFocus` and focuses this element instead; omitting the prop
|
|
124
|
+
* keeps today's default in every mode (dialog, sheet, drawer).
|
|
125
|
+
*/
|
|
126
|
+
initialFocusRef?: React.RefObject<HTMLElement | null>;
|
|
116
127
|
};
|
|
117
128
|
|
|
118
129
|
/**
|
|
@@ -159,6 +170,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
159
170
|
wideFrom = "md",
|
|
160
171
|
className,
|
|
161
172
|
bodyClassName,
|
|
173
|
+
initialFocusRef,
|
|
162
174
|
},
|
|
163
175
|
ref
|
|
164
176
|
) => {
|
|
@@ -166,6 +178,18 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
166
178
|
const container: "drawer" | "dialog" | "sheet" = isWide ? desktop : "drawer";
|
|
167
179
|
useFocusReturn(open);
|
|
168
180
|
|
|
181
|
+
// Radix (and vaul, underneath the drawer) call this before moving focus
|
|
182
|
+
// themselves; preventing the default there is what stops the first-
|
|
183
|
+
// tabbable fallback from ever running.
|
|
184
|
+
const handleOpenAutoFocus = React.useCallback(
|
|
185
|
+
(event: Event) => {
|
|
186
|
+
if (!initialFocusRef?.current) return;
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
initialFocusRef.current.focus();
|
|
189
|
+
},
|
|
190
|
+
[initialFocusRef]
|
|
191
|
+
);
|
|
192
|
+
|
|
169
193
|
// The name and the description are Radix's own: its `Title` and
|
|
170
194
|
// `Description` register ids in the dialog context and `Content` points
|
|
171
195
|
// `aria-labelledby` / `aria-describedby` at them. Passing our own ids
|
|
@@ -232,6 +256,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
232
256
|
attachment="inset"
|
|
233
257
|
hideClose
|
|
234
258
|
overlayClassName="bg-scrim-overlay backdrop-blur-none"
|
|
259
|
+
onOpenAutoFocus={handleOpenAutoFocus}
|
|
235
260
|
{...describedBy}
|
|
236
261
|
data-slot="responsive-overlay-sheet"
|
|
237
262
|
data-modal={modal ? "true" : "false"}
|
|
@@ -263,6 +288,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
263
288
|
size={size}
|
|
264
289
|
showCloseButton={false}
|
|
265
290
|
overlayClassName="bg-scrim-overlay backdrop-blur-none"
|
|
291
|
+
onOpenAutoFocus={handleOpenAutoFocus}
|
|
266
292
|
{...describedBy}
|
|
267
293
|
className={cn(
|
|
268
294
|
// The dialog's own `grid gap-4 p-6` would put air between the
|
|
@@ -284,6 +310,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
284
310
|
<DrawerContent
|
|
285
311
|
ref={ref}
|
|
286
312
|
overlayClassName="bg-scrim-overlay"
|
|
313
|
+
onOpenAutoFocus={handleOpenAutoFocus}
|
|
287
314
|
{...describedBy}
|
|
288
315
|
className={cn("flex max-h-[85vh] flex-col p-0", className)}
|
|
289
316
|
>
|