@bearmenu/ui 0.8.39 → 0.8.40

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.
@@ -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: {
@@ -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?: "default" | "secondary" | "success" | "warning" | "destructive" | "outline" | "glass" | "overlay" | "brown" | "code" | "onPrimary" | "confirmed" | "pending" | "completed" | "assignee" | "softSuccess" | "softWarning" | "softDestructive" | "softNeutral" | null | undefined;
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?: "default" | "link" | "secondary" | "warning" | "destructive" | "outline" | "ghost" | "glass" | "overlay" | "ink" | "brown" | "cta" | null | undefined;
7
- size?: "icon" | "default" | "cta" | "xs" | "sm" | "lg" | "pill" | "cta-sm" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
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>>;
@@ -1,4 +1,4 @@
1
- export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants } from '../chunk-FVSKGCK3.js';
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';
@@ -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>>, "value" | "onChange" | "type"> & {
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">, "value" | "heading"> & {
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">, "value" | "onSelect" | "disabled"> & {
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 16px-radius card surface with the resting card shadow.
27
- * `none` (the default) leaves the surface to the container it sits in.
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.
@@ -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-2xl border border-border bg-card shadow-[var(--shadow-card)]",
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,31 @@ 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-2xl border border-border shadow-sm sm:size-18 [&_svg]:size-7",
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",
74
74
  variant === "error" ? "bg-destructive/5" : "bg-card"
75
75
  ),
76
76
  children: icon != null ? icon : DEFAULT_ICON[variant]
77
77
  }
78
78
  ),
79
- /* @__PURE__ */ jsx(Text, { as: "h3", weight: "semibold", className: "text-lg", children: title }),
79
+ /* @__PURE__ */ jsx(Text, { as: "h3", weight: "semibold", className: "text-base sm:text-lg", children: title }),
80
80
  description ? /* @__PURE__ */ jsx(Text, { as: "p", color: "muted", className: "mt-3 max-w-md text-sm", children: description }) : null,
81
81
  (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__ */ jsx(
82
+ action && /* @__PURE__ */ jsxs(
83
83
  Button,
84
84
  {
85
85
  onClick: action.onClick,
86
86
  variant: actionVariant != null ? actionVariant : DEFAULT_ACTION_VARIANT[variant],
87
87
  className: "glass-hover",
88
- children: action.label
88
+ children: [
89
+ action.icon,
90
+ action.label
91
+ ]
89
92
  }
90
93
  ),
91
- secondaryAction && /* @__PURE__ */ jsx(Button, { onClick: secondaryAction.onClick, variant: "outline", className: "glass-hover", children: secondaryAction.label })
94
+ secondaryAction && /* @__PURE__ */ jsxs(Button, { onClick: secondaryAction.onClick, variant: "outline", className: "glass-hover", children: [
95
+ secondaryAction.icon,
96
+ secondaryAction.label
97
+ ] })
92
98
  ] })
93
99
  ]
94
100
  })
@@ -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?: "default" | "inherit" | "primary" | "destructive" | null | undefined;
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?: "default" | "inherit" | "primary" | "destructive" | null | undefined;
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;
@@ -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?: "default" | "primary" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
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?: "default" | "primary" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
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?: "body" | "small" | "display" | "h2" | "h3" | "label" | "caption" | "h1" | "h4" | "body-lg" | "body-sm" | "overline" | null | undefined;
26
- color?: "default" | "inherit" | "primary" | "destructive" | "muted" | "primary-foreground" | null | undefined;
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-FVSKGCK3.js';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.8.39",
3
+ "version": "0.8.40",
4
4
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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
+ });
@@ -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
- /** The booking pages' whole-body failure (reports/02-p4, calendar/02-c2). */
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: () => {} };
@@ -51,10 +52,27 @@ describe("EmptyState", () => {
51
52
  expect(screen.getByRole("status").querySelector("p")).toBeNull();
52
53
  });
53
54
 
54
- it("draws the card surface on request only", () => {
55
+ it("draws the card surface on request only, borderless with the frames' radius", () => {
55
56
  const { rerender } = render(<EmptyState title="Empty" description="Nothing." />);
56
57
  expect(screen.getByRole("status")).not.toHaveClass("bg-card");
57
58
  rerender(<EmptyState title="Empty" description="Nothing." surface="card" />);
58
- expect(screen.getByRole("status")).toHaveClass("rounded-2xl", "bg-card", "border");
59
+ const card = screen.getByRole("status");
60
+ expect(card).toHaveClass("rounded-[14px]", "sm:rounded-[16px]", "bg-card", "px-5");
61
+ expect(card).not.toHaveClass("border", "border-border");
62
+ });
63
+
64
+ it("renders the action's icon before its label", () => {
65
+ render(
66
+ <ErrorEmptyState
67
+ title="Couldn't load this report"
68
+ description="On our side."
69
+ action={{ label: "Try again", onClick: () => {}, icon: <RefreshCw data-testid="retry-icon" /> }}
70
+ />
71
+ );
72
+ const button = screen.getByRole("button", { name: "Try again" });
73
+ const icon = screen.getByTestId("retry-icon");
74
+ expect(button).toContainElement(icon);
75
+ expect(button.firstElementChild).toBe(icon);
76
+ expect(button.textContent).toBe("Try again");
59
77
  });
60
78
  });
@@ -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 16px-radius card surface with the resting card shadow.
35
- * `none` (the default) leaves the surface to the container it sits in.
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-2xl border border-border bg-card shadow-[var(--shadow-card)]",
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,7 @@ 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
- "mb-4 flex size-16 shrink-0 items-center justify-center rounded-2xl border border-border shadow-sm sm:size-18 [&_svg]:size-7",
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",
110
122
  variant === "error" ? "bg-destructive/5" : "bg-card"
111
123
  )}
112
124
  >
@@ -114,7 +126,7 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
114
126
  </span>
115
127
  )}
116
128
 
117
- <Text as="h3" weight="semibold" className="text-lg">
129
+ <Text as="h3" weight="semibold" className="text-base sm:text-lg">
118
130
  {title}
119
131
  </Text>
120
132
 
@@ -132,11 +144,13 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
132
144
  variant={actionVariant ?? DEFAULT_ACTION_VARIANT[variant]}
133
145
  className="glass-hover"
134
146
  >
147
+ {action.icon}
135
148
  {action.label}
136
149
  </Button>
137
150
  )}
138
151
  {secondaryAction && (
139
152
  <Button onClick={secondaryAction.onClick} variant="outline" className="glass-hover">
153
+ {secondaryAction.icon}
140
154
  {secondaryAction.label}
141
155
  </Button>
142
156
  )}