@bearmenu/ui 0.8.40 → 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.
@@ -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
- default: "h-11 px-6 py-2",
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",
@@ -1,4 +1,4 @@
1
- import { buttonVariants } from '../chunk-5SGKGFMN.js';
1
+ import { buttonVariants } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,4 +1,4 @@
1
- export { Button, buttonVariants } from '../chunk-5SGKGFMN.js';
1
+ export { Button, buttonVariants } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import '../chunk-FEK5XGZW.js';
@@ -1,4 +1,4 @@
1
- import { buttonVariants, Button } from '../chunk-5SGKGFMN.js';
1
+ import { buttonVariants, Button } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-5SGKGFMN.js';
1
+ import { Button } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-5SGKGFMN.js';
1
+ import { Button } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.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-5SGKGFMN.js';
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';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-5SGKGFMN.js';
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';
@@ -70,7 +70,9 @@ 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
- "mb-4 flex size-16 shrink-0 items-center justify-center rounded-[20px] border border-border shadow-sm sm:size-18 sm:rounded-[22px] [&_svg]:size-7",
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]
@@ -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-5SGKGFMN.js';
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-5SGKGFMN.js';
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,4 +1,4 @@
1
- import { Button } from '../chunk-5SGKGFMN.js';
1
+ import { Button } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import '../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-5SGKGFMN.js';
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-5SGKGFMN.js';
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-5SGKGFMN.js';
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';
@@ -1,4 +1,4 @@
1
- import { buttonVariants } from '../chunk-5SGKGFMN.js';
1
+ import { buttonVariants } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,5 +1,5 @@
1
1
  import { ButtonGroup } from '../chunk-EGU6EG73.js';
2
- import { Button } from '../chunk-5SGKGFMN.js';
2
+ import { Button } from '../chunk-K6ACNU2L.js';
3
3
  import '../chunk-RIH2IS7X.js';
4
4
  import '../chunk-5YNIQ2BL.js';
5
5
  import { cn } from '../chunk-FEK5XGZW.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-5SGKGFMN.js';
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';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-5SGKGFMN.js';
1
+ import { Button } from '../chunk-K6ACNU2L.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.8.40",
3
+ "version": "0.8.41",
4
4
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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
- default: "h-11 px-6 py-2",
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",
@@ -47,6 +47,14 @@ describe("EmptyState", () => {
47
47
  expect(tile).toHaveClass("size-16", "sm:size-18", "[&_svg]:size-7");
48
48
  });
49
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
+
50
58
  it("renders no empty paragraph when there is no body", () => {
51
59
  render(<EmptyState title="Empty" description="" />);
52
60
  expect(screen.getByRole("status").querySelector("p")).toBeNull();
@@ -118,7 +118,9 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
118
118
  className={cn(
119
119
  // The glyph size is the tile's, whatever size the caller's icon
120
120
  // node asked for — a 40px icon in a 64px tile has no margin.
121
- "mb-4 flex size-16 shrink-0 items-center justify-center rounded-[20px] border border-border shadow-sm sm:size-18 sm:rounded-[22px] [&_svg]:size-7",
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",
122
124
  variant === "error" ? "bg-destructive/5" : "bg-card"
123
125
  )}
124
126
  >
@@ -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
  >