@bearmenu/ui 0.8.43 → 0.8.44

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.
Files changed (42) hide show
  1. package/dist/{chunk-CLFE4U5T.js → chunk-6PQINSAA.js} +1 -1
  2. package/dist/{chunk-K6ACNU2L.js → chunk-GHKTVHQG.js} +1 -1
  3. package/dist/{chunk-WHUG6QEI.js → chunk-RYPPN2GZ.js} +12 -2
  4. package/dist/{chunk-RQPUBKFU.js → chunk-VXGIDKXV.js} +1 -1
  5. package/dist/components/alert-dialog.js +1 -1
  6. package/dist/components/badge.js +1 -1
  7. package/dist/components/booking-row.js +1 -1
  8. package/dist/components/button.js +1 -1
  9. package/dist/components/calendar.js +1 -1
  10. package/dist/components/carousel.js +1 -1
  11. package/dist/components/coach-marks.js +1 -1
  12. package/dist/components/combobox.js +1 -1
  13. package/dist/components/concept-convergence.js +1 -1
  14. package/dist/components/empty-state.js +1 -1
  15. package/dist/components/event-detail-panel.js +1 -1
  16. package/dist/components/event-list-row-wide.js +1 -1
  17. package/dist/components/filter-category-row.js +1 -1
  18. package/dist/components/link-button.js +1 -1
  19. package/dist/components/menu-family-block.js +1 -1
  20. package/dist/components/menu-index-list.js +1 -1
  21. package/dist/components/menu-item.js +3 -3
  22. package/dist/components/multi-select-combobox.js +2 -2
  23. package/dist/components/pagination.js +1 -1
  24. package/dist/components/pan-zoom-viewport.js +1 -1
  25. package/dist/components/place-card.js +1 -1
  26. package/dist/components/place-details-mobile.js +1 -1
  27. package/dist/components/responsive-overlay.js +24 -13
  28. package/dist/components/searchable-select.js +1 -1
  29. package/dist/components/signature-chip.js +2 -2
  30. package/dist/components/sliding-panels.js +1 -1
  31. package/dist/components/top-ranking-entry.js +3 -3
  32. package/package.json +1 -1
  33. package/src/components/badge.stories.tsx +20 -0
  34. package/src/components/badge.test.tsx +59 -0
  35. package/src/components/badge.tsx +12 -2
  36. package/src/components/button.test.tsx +2 -1
  37. package/src/components/button.tsx +1 -1
  38. package/src/components/empty-state.test.tsx +3 -3
  39. package/src/components/responsive-overlay.stories.tsx +26 -1
  40. package/src/components/responsive-overlay.test.tsx +49 -0
  41. package/src/components/responsive-overlay.tsx +45 -13
  42. package/src/globals.css +5 -1
@@ -1,4 +1,4 @@
1
- import { Badge } from './chunk-WHUG6QEI.js';
1
+ import { Badge } from './chunk-RYPPN2GZ.js';
2
2
  import { cn } from './chunk-FEK5XGZW.js';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
@@ -25,7 +25,7 @@ var buttonVariants = cva(
25
25
  * surface: the fill darkens a notch, the border firms up, the type
26
26
  * stays put. `--accent-subtle` is that step; `active:` goes one more.
27
27
  */
28
- outline: "border border-border bg-background hover:bg-accent-subtle hover:border-accent/40 active:bg-muted",
28
+ outline: "border border-input bg-card hover:bg-accent-subtle hover:border-accent/40 active:bg-muted",
29
29
  secondary: "bg-secondary text-secondary-foreground shadow-sm border border-border hover:bg-accent-subtle hover:border-accent/40 active:bg-muted",
30
30
  ghost: "hover:bg-accent-subtle active:bg-muted",
31
31
  link: "text-primary underline-offset-4 hover:underline",
@@ -79,8 +79,9 @@ var badgeVariants = cva(
79
79
  * coming after the variant classes.
80
80
  */
81
81
  size: {
82
- chip: "",
83
- status: "gap-1 px-2 py-[3px] text-xs leading-[18px] font-semibold"
82
+ chip: "rounded-sm",
83
+ // measured: status badge radius 7px, no token
84
+ status: "gap-1 px-2 py-[3px] text-xs leading-[18px] font-semibold rounded-[7px]"
84
85
  },
85
86
  // v1.0 motion: "live" surface signal — applies `live-pulse` archetype.
86
87
  // Pair with a colored variant (e.g. variant="destructive" live).
@@ -89,6 +90,15 @@ var badgeVariants = cva(
89
90
  false: ""
90
91
  }
91
92
  },
93
+ // `size` sets a radius meant for the plain variants (default, outline,
94
+ // success, …), which otherwise leak the base `rounded-lg` (10px). The
95
+ // pill (`rounded-full`) and soft (`rounded-md`) variants already draw
96
+ // their own measured radius — reassert it here so `cn`'s tailwind-merge
97
+ // doesn't let the size-level radius clobber it.
98
+ compoundVariants: [
99
+ { variant: ["confirmed", "pending", "code", "completed", "assignee"], class: "rounded-full" },
100
+ { variant: ["softSuccess", "softWarning", "softDestructive", "softNeutral"], class: "rounded-md" }
101
+ ],
92
102
  defaultVariants: {
93
103
  variant: "default",
94
104
  size: "chip",
@@ -1,4 +1,4 @@
1
- import { SignatureChip } from './chunk-CLFE4U5T.js';
1
+ import { SignatureChip } from './chunk-6PQINSAA.js';
2
2
  import { cn } from './chunk-FEK5XGZW.js';
3
3
  import { __objRest, __spreadValues } from './chunk-MMUBH76A.js';
4
4
  import { memo } from 'react';
@@ -1,4 +1,4 @@
1
- import { buttonVariants } from '../chunk-K6ACNU2L.js';
1
+ import { buttonVariants } from '../chunk-GHKTVHQG.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,3 +1,3 @@
1
- export { Badge, badgeVariants } from '../chunk-WHUG6QEI.js';
1
+ export { Badge, badgeVariants } from '../chunk-RYPPN2GZ.js';
2
2
  import '../chunk-FEK5XGZW.js';
3
3
  import '../chunk-MMUBH76A.js';
@@ -1,4 +1,4 @@
1
- import { Badge } from '../chunk-WHUG6QEI.js';
1
+ import { Badge } from '../chunk-RYPPN2GZ.js';
2
2
  import { cn } from '../chunk-FEK5XGZW.js';
3
3
  import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
4
4
  import * as React from 'react';
@@ -1,4 +1,4 @@
1
- export { Button, buttonVariants } from '../chunk-K6ACNU2L.js';
1
+ export { Button, buttonVariants } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
1
+ import { buttonVariants, Button } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
1
+ import { Button } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
1
+ import { Button } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
4
+ import { Button } from '../chunk-GHKTVHQG.js';
5
5
  import '../chunk-RIH2IS7X.js';
6
6
  import '../chunk-5YNIQ2BL.js';
7
7
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,7 +1,7 @@
1
1
  import { PhotoGround } from '../chunk-SWD6CJ35.js';
2
2
  import { Convergence } from '../chunk-4XDJXMLF.js';
3
3
  import '../chunk-OIV7B44M.js';
4
- import { Badge } from '../chunk-WHUG6QEI.js';
4
+ import { Badge } from '../chunk-RYPPN2GZ.js';
5
5
  import { cn } from '../chunk-FEK5XGZW.js';
6
6
  import '../chunk-MMUBH76A.js';
7
7
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-K6ACNU2L.js';
1
+ import { Button } from '../chunk-GHKTVHQG.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { Text } from '../chunk-ZLMRVBD7.js';
@@ -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-K6ACNU2L.js';
5
+ import { Button } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
6
+ import { buttonVariants } from '../chunk-GHKTVHQG.js';
7
7
  import '../chunk-RIH2IS7X.js';
8
8
  import '../chunk-5YNIQ2BL.js';
9
9
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,5 +1,5 @@
1
1
  import { Text } from '../chunk-ZLMRVBD7.js';
2
- import { Badge } from '../chunk-WHUG6QEI.js';
2
+ import { Badge } from '../chunk-RYPPN2GZ.js';
3
3
  import '../chunk-FEK5XGZW.js';
4
4
  import '../chunk-MMUBH76A.js';
5
5
  import { ChevronRight } from 'lucide-react';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-K6ACNU2L.js';
1
+ import { Button } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
6
+ import { buttonVariants } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
3
+ import { Button } from '../chunk-GHKTVHQG.js';
4
4
  import '../chunk-RIH2IS7X.js';
5
5
  import '../chunk-5YNIQ2BL.js';
6
6
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,5 +1,5 @@
1
- export { MentionSignal, MenuItem } from '../chunk-RQPUBKFU.js';
2
- import '../chunk-CLFE4U5T.js';
3
- import '../chunk-WHUG6QEI.js';
1
+ export { MentionSignal, MenuItem } from '../chunk-VXGIDKXV.js';
2
+ import '../chunk-6PQINSAA.js';
3
+ import '../chunk-RYPPN2GZ.js';
4
4
  import '../chunk-FEK5XGZW.js';
5
5
  import '../chunk-MMUBH76A.js';
@@ -2,10 +2,10 @@ 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-K6ACNU2L.js';
5
+ import { Button } from '../chunk-GHKTVHQG.js';
6
6
  import '../chunk-RIH2IS7X.js';
7
7
  import '../chunk-5YNIQ2BL.js';
8
- import { Badge } from '../chunk-WHUG6QEI.js';
8
+ import { Badge } from '../chunk-RYPPN2GZ.js';
9
9
  import { cn } from '../chunk-FEK5XGZW.js';
10
10
  import { __spreadValues } from '../chunk-MMUBH76A.js';
11
11
  import * as React from 'react';
@@ -1,4 +1,4 @@
1
- import { buttonVariants } from '../chunk-K6ACNU2L.js';
1
+ import { buttonVariants } from '../chunk-GHKTVHQG.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-K6ACNU2L.js';
2
+ import { Button } from '../chunk-GHKTVHQG.js';
3
3
  import '../chunk-RIH2IS7X.js';
4
4
  import '../chunk-5YNIQ2BL.js';
5
5
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -6,7 +6,7 @@ import { showsGradeAtom } from '../chunk-I5V446BX.js';
6
6
  import { Separator } from '../chunk-NWZGSLPU.js';
7
7
  import { springSnappy } from '../chunk-OIV7B44M.js';
8
8
  import { Skeleton } from '../chunk-XBBEX4SF.js';
9
- import { Badge } from '../chunk-WHUG6QEI.js';
9
+ import { Badge } from '../chunk-RYPPN2GZ.js';
10
10
  import { cn } from '../chunk-FEK5XGZW.js';
11
11
  import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
12
12
  import { forwardRef, memo, useState, useMemo } from 'react';
@@ -3,7 +3,7 @@ import '../chunk-FMTJGO76.js';
3
3
  import '../chunk-MWB7S54O.js';
4
4
  import '../chunk-NWZGSLPU.js';
5
5
  import { Text } from '../chunk-ZLMRVBD7.js';
6
- import { Badge } from '../chunk-WHUG6QEI.js';
6
+ import { Badge } from '../chunk-RYPPN2GZ.js';
7
7
  import { cn } from '../chunk-FEK5XGZW.js';
8
8
  import '../chunk-MMUBH76A.js';
9
9
  import { useState } from 'react';
@@ -81,6 +81,29 @@ var ResponsiveOverlay = React.forwardRef(
81
81
  const Title = CONTAINER_TITLE[container];
82
82
  const Description = CONTAINER_DESCRIPTION[container];
83
83
  const isDrawer = container === "drawer";
84
+ const closeButton = isDrawer ? /* @__PURE__ */ jsx(
85
+ "button",
86
+ {
87
+ type: "button",
88
+ "data-slot": "responsive-overlay-close",
89
+ "aria-label": closeLabel,
90
+ onClick: () => onOpenChange(false),
91
+ className: "sr-only rounded-full text-muted-foreground transition-colors focus-visible:not-sr-only focus-visible:-m-1.5 focus-visible:grid focus-visible:size-11 focus-visible:shrink-0 focus-visible:place-items-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
92
+ children: /* @__PURE__ */ jsx("span", { className: "grid size-8 place-items-center rounded-full bg-surface-2 transition-colors hover:bg-muted", children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": true }) })
93
+ }
94
+ ) : /* @__PURE__ */ jsxs(
95
+ "button",
96
+ {
97
+ type: "button",
98
+ "data-slot": "responsive-overlay-close",
99
+ onClick: () => onOpenChange(false),
100
+ className: "-m-1.5 grid size-11 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
101
+ children: [
102
+ /* @__PURE__ */ jsx("span", { className: "grid size-8 place-items-center rounded-full bg-surface-2 transition-colors hover:bg-muted", children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": true }) }),
103
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: closeLabel })
104
+ ]
105
+ }
106
+ );
84
107
  const header = /* @__PURE__ */ jsxs(
85
108
  "div",
86
109
  {
@@ -91,19 +114,7 @@ var ResponsiveOverlay = React.forwardRef(
91
114
  /* @__PURE__ */ jsx(Title, { className: cn("font-bold leading-tight", isDrawer ? "text-[22px]" : "text-[19px]"), children: title }),
92
115
  subtitle ? /* @__PURE__ */ jsx(Description, { className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : null
93
116
  ] }),
94
- /* @__PURE__ */ jsxs(
95
- "button",
96
- {
97
- type: "button",
98
- "data-slot": "responsive-overlay-close",
99
- onClick: () => onOpenChange(false),
100
- className: "-m-1.5 grid size-11 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
101
- children: [
102
- /* @__PURE__ */ jsx("span", { className: "grid size-8 place-items-center rounded-full bg-surface-2 transition-colors hover:bg-muted", children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": true }) }),
103
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: closeLabel })
104
- ]
105
- }
106
- )
117
+ closeButton
107
118
  ]
108
119
  }
109
120
  );
@@ -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-K6ACNU2L.js';
4
+ import { Button } from '../chunk-GHKTVHQG.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
- export { SignatureChip } from '../chunk-CLFE4U5T.js';
2
- import '../chunk-WHUG6QEI.js';
1
+ export { SignatureChip } from '../chunk-6PQINSAA.js';
2
+ import '../chunk-RYPPN2GZ.js';
3
3
  import '../chunk-FEK5XGZW.js';
4
4
  import '../chunk-MMUBH76A.js';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-K6ACNU2L.js';
1
+ import { Button } from '../chunk-GHKTVHQG.js';
2
2
  import '../chunk-RIH2IS7X.js';
3
3
  import '../chunk-5YNIQ2BL.js';
4
4
  import { cn } from '../chunk-FEK5XGZW.js';
@@ -1,8 +1,8 @@
1
- import { MenuItem } from '../chunk-RQPUBKFU.js';
2
- import '../chunk-CLFE4U5T.js';
1
+ import { MenuItem } from '../chunk-VXGIDKXV.js';
2
+ import '../chunk-6PQINSAA.js';
3
3
  import { Separator } from '../chunk-NWZGSLPU.js';
4
4
  import { Skeleton } from '../chunk-XBBEX4SF.js';
5
- import '../chunk-WHUG6QEI.js';
5
+ import '../chunk-RYPPN2GZ.js';
6
6
  import { cn } from '../chunk-FEK5XGZW.js';
7
7
  import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
8
8
  import { MapPin, Star, MessageSquareQuote } from 'lucide-react';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.8.43",
3
+ "version": "0.8.44",
4
4
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -112,6 +112,26 @@ export const StatusSize: Story = {
112
112
  ),
113
113
  };
114
114
 
115
+ /**
116
+ * The two plain-variant radii: `size="chip"` (the inline default) draws
117
+ * `rounded-sm` (6px), `size="status"` draws the measured 7px the frames use
118
+ * for the taller pill. Pill (`confirmed`/`pending`/…) and soft
119
+ * (`softSuccess`/…) variants keep their own `rounded-full` / `rounded-md`
120
+ * regardless of size — see `Assignee` and `SoftTones` below.
121
+ */
122
+ export const Radius: Story = {
123
+ render: () => (
124
+ <div className="flex flex-wrap items-center gap-3">
125
+ <Badge variant="outline" size="chip">
126
+ chip · 6px
127
+ </Badge>
128
+ <Badge variant="outline" size="status">
129
+ status · 7px
130
+ </Badge>
131
+ </div>
132
+ ),
133
+ };
134
+
115
135
  /**
116
136
  * `assignee` — who an item belongs to, in the shared table's organise view. It
117
137
  * is a control, not a status: the whole pill opens "Who had this?", so it keeps
@@ -79,4 +79,63 @@ describe("Badge", () => {
79
79
  expect(badge).toHaveClass("py-[3px]", "text-[10.5px]", "font-bold");
80
80
  });
81
81
  });
82
+
83
+ describe("radius", () => {
84
+ // A plain variant (no radius of its own) leaks the base rounded-lg
85
+ // (10px) without a size-level radius. `chip` measures 6px, `status` 7px
86
+ // — the frames' status pill has no token for it.
87
+ it('size="chip" (the default) draws 6px, not the base 10px', () => {
88
+ render(<Badge variant="outline">Outline</Badge>);
89
+ const badge = screen.getByText("Outline");
90
+ expect(badge).toHaveClass("rounded-sm");
91
+ expect(badge.className).not.toContain("rounded-lg");
92
+ });
93
+
94
+ it('size="status" draws the measured 7px, not the base 10px', () => {
95
+ render(
96
+ <Badge variant="outline" size="status">
97
+ Outline
98
+ </Badge>
99
+ );
100
+ const badge = screen.getByText("Outline");
101
+ expect(badge).toHaveClass("rounded-[7px]");
102
+ expect(badge.className).not.toContain("rounded-lg");
103
+ });
104
+
105
+ it.each(["confirmed", "pending", "code", "completed", "assignee"] as const)(
106
+ "%s keeps its own rounded-full at both sizes",
107
+ (variant) => {
108
+ render(<Badge variant={variant}>Label</Badge>);
109
+ expect(screen.getByText("Label")).toHaveClass("rounded-full");
110
+
111
+ render(
112
+ <Badge variant={variant} size="status">
113
+ Label
114
+ </Badge>
115
+ );
116
+ const withStatus = screen.getAllByText("Label")[1];
117
+ expect(withStatus).toHaveClass("rounded-full");
118
+ expect(withStatus.className).not.toContain("rounded-sm");
119
+ expect(withStatus.className).not.toContain("rounded-[7px]");
120
+ }
121
+ );
122
+
123
+ it.each(["softSuccess", "softWarning", "softDestructive", "softNeutral"] as const)(
124
+ "%s keeps its own rounded-md at both sizes",
125
+ (variant) => {
126
+ render(<Badge variant={variant}>Label</Badge>);
127
+ expect(screen.getByText("Label")).toHaveClass("rounded-md");
128
+
129
+ render(
130
+ <Badge variant={variant} size="status">
131
+ Label
132
+ </Badge>
133
+ );
134
+ const withStatus = screen.getAllByText("Label")[1];
135
+ expect(withStatus).toHaveClass("rounded-md");
136
+ expect(withStatus.className).not.toContain("rounded-sm");
137
+ expect(withStatus.className).not.toContain("rounded-[7px]");
138
+ }
139
+ );
140
+ });
82
141
  });
@@ -87,8 +87,9 @@ const badgeVariants = cva(
87
87
  * coming after the variant classes.
88
88
  */
89
89
  size: {
90
- chip: "",
91
- status: "gap-1 px-2 py-[3px] text-xs leading-[18px] font-semibold",
90
+ chip: "rounded-sm",
91
+ // measured: status badge radius 7px, no token
92
+ status: "gap-1 px-2 py-[3px] text-xs leading-[18px] font-semibold rounded-[7px]",
92
93
  },
93
94
  // v1.0 motion: "live" surface signal — applies `live-pulse` archetype.
94
95
  // Pair with a colored variant (e.g. variant="destructive" live).
@@ -97,6 +98,15 @@ const badgeVariants = cva(
97
98
  false: "",
98
99
  },
99
100
  },
101
+ // `size` sets a radius meant for the plain variants (default, outline,
102
+ // success, …), which otherwise leak the base `rounded-lg` (10px). The
103
+ // pill (`rounded-full`) and soft (`rounded-md`) variants already draw
104
+ // their own measured radius — reassert it here so `cn`'s tailwind-merge
105
+ // doesn't let the size-level radius clobber it.
106
+ compoundVariants: [
107
+ { variant: ["confirmed", "pending", "code", "completed", "assignee"], class: "rounded-full" },
108
+ { variant: ["softSuccess", "softWarning", "softDestructive", "softNeutral"], class: "rounded-md" },
109
+ ],
100
110
  defaultVariants: {
101
111
  variant: "default",
102
112
  size: "chip",
@@ -16,7 +16,8 @@ describe("buttonVariants", () => {
16
16
  it("maps variant prop to correct classes", () => {
17
17
  expect(buttonVariants({ variant: "default" })).toContain("bg-primary");
18
18
  expect(buttonVariants({ variant: "destructive" })).toContain("bg-destructive");
19
- expect(buttonVariants({ variant: "outline" })).toContain("border-border");
19
+ expect(buttonVariants({ variant: "outline" })).toContain("border-input");
20
+ expect(buttonVariants({ variant: "outline" })).toContain("bg-card");
20
21
  expect(buttonVariants({ variant: "secondary" })).toContain("bg-secondary");
21
22
  expect(buttonVariants({ variant: "ghost" })).toContain("hover:bg-accent");
22
23
  expect(buttonVariants({ variant: "link" })).toContain("text-primary");
@@ -26,7 +26,7 @@ const buttonVariants = cva(
26
26
  * stays put. `--accent-subtle` is that step; `active:` goes one more.
27
27
  */
28
28
  outline:
29
- "border border-border bg-background hover:bg-accent-subtle hover:border-accent/40 active:bg-muted",
29
+ "border border-input bg-card hover:bg-accent-subtle hover:border-accent/40 active:bg-muted",
30
30
  secondary:
31
31
  "bg-secondary text-secondary-foreground shadow-sm border border-border hover:bg-accent-subtle hover:border-accent/40 active:bg-muted",
32
32
  ghost: "hover:bg-accent-subtle active:bg-muted",
@@ -9,12 +9,12 @@ const retry = { label: "Try again", onClick: () => {} };
9
9
  describe("EmptyState", () => {
10
10
  it("draws a retry on an error as an outline button", () => {
11
11
  render(<ErrorEmptyState title="Couldn't load this report" description="On our side." action={retry} />);
12
- expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-border", "bg-background");
12
+ expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-input", "bg-card");
13
13
  });
14
14
 
15
15
  it("draws a clear-filters on a search as an outline button", () => {
16
16
  render(<SearchEmptyState title="No results" description="Try another name." action={retry} />);
17
- expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-border", "bg-background");
17
+ expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-input", "bg-card");
18
18
  });
19
19
 
20
20
  it("keeps the plain empty state's create action filled", () => {
@@ -24,7 +24,7 @@ describe("EmptyState", () => {
24
24
 
25
25
  it("lets the caller choose the action style", () => {
26
26
  render(<EmptyState title="Nothing" description="Here." action={retry} actionVariant="outline" />);
27
- expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-border", "bg-background");
27
+ expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-input", "bg-card");
28
28
  });
29
29
 
30
30
  it("calls the action", async () => {
@@ -13,7 +13,7 @@ const meta: Meta<typeof ResponsiveOverlay> = {
13
13
  docs: {
14
14
  description: {
15
15
  component:
16
- "One overlay authored once: a Drawer under md, a Dialog from md. The chrome is the same at both widths — title, one-line subtitle, round close, body, footer band. Resize the preview across 768px to see it change container. The scrim is `--scrim-overlay`, never `--scrim`.",
16
+ "One overlay authored once: a Drawer under md, a Dialog from md. The chrome is the same at both widths — title, one-line subtitle, round close, body, footer band. Resize the preview across 768px to see it change container. The scrim is `--scrim-overlay`, never `--scrim`. The drawer's round close button is keyboard-only: it stays `sr-only` (in the a11y tree and the tab order, never `display:none`) until it is `:focus-visible`, since the drawer already closes on a drag-down or a backdrop tap. Tab into the drawer below to see it appear. Dialog and sheet keep the always-visible close button.",
17
17
  },
18
18
  },
19
19
  },
@@ -58,6 +58,31 @@ export const AddAnItem: Story = {
58
58
  },
59
59
  };
60
60
 
61
+ /**
62
+ * The drawer's close button (below 768px) has no visible circular X by
63
+ * default — a drag down or a backdrop tap already dismiss it. Tab through
64
+ * the open drawer to reveal the keyboard-only close button, drawn as the
65
+ * same disc dialog/sheet show at all times, with a visible focus ring.
66
+ */
67
+ export const DrawerCloseIsKeyboardOnly: Story = {
68
+ render: () => {
69
+ const [open, setOpen] = React.useState(true);
70
+ return (
71
+ <ResponsiveOverlay
72
+ open={open}
73
+ onOpenChange={setOpen}
74
+ title="Add an item"
75
+ subtitle="Table 18 · from the menu"
76
+ closeLabel="Close"
77
+ >
78
+ <p className="text-[14px] text-muted-foreground">
79
+ Press Tab to move focus onto the close button — it is invisible until then.
80
+ </p>
81
+ </ResponsiveOverlay>
82
+ );
83
+ },
84
+ };
85
+
61
86
  /**
62
87
  * floor/14-w1 — a working rail. `desktop="sheet"` with `modal={false}` and
63
88
  * `wideFrom="lg"`: from 1024 the page stays live beside it (click a table and
@@ -474,6 +474,55 @@ describe("ResponsiveOverlay", () => {
474
474
  });
475
475
  });
476
476
 
477
+ describe("close button", () => {
478
+ // The drawer already closes on a drag-down and a backdrop tap; the
479
+ // circular X is a mouse-only affordance the frames never draw there, so
480
+ // it stays out of view until a keyboard user tabs to it.
481
+ it("drawer: has no visible close — sr-only, not display:none", () => {
482
+ open();
483
+ const close = screen.getByRole("button", { name: "Close" });
484
+ expect(close).toHaveClass("sr-only");
485
+ expect(close.className).not.toMatch(/\bhidden\b/);
486
+ expect(close).not.toHaveAttribute("hidden");
487
+ expect(close.style.display).not.toBe("none");
488
+ });
489
+
490
+ it("drawer: the close button is in the a11y tree with its label", () => {
491
+ open();
492
+ expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
493
+ });
494
+
495
+ it("drawer: the close button is tabbable", () => {
496
+ open();
497
+ const close = screen.getByRole("button", { name: "Close" });
498
+ expect(close).not.toHaveAttribute("tabindex", "-1");
499
+ expect(close).not.toBeDisabled();
500
+ });
501
+
502
+ it("drawer: clicking the close button closes it", async () => {
503
+ const onOpenChange = vi.fn();
504
+ open({ onOpenChange });
505
+ await userEvent.click(screen.getByRole("button", { name: "Close" }));
506
+ expect(onOpenChange).toHaveBeenCalledWith(false);
507
+ });
508
+
509
+ it("dialog: keeps the always-visible close button", () => {
510
+ setWidth(true);
511
+ open();
512
+ const close = screen.getByRole("button", { name: "Close" });
513
+ expect(close.className).not.toContain("sr-only");
514
+ expect(close).toHaveClass("size-11");
515
+ });
516
+
517
+ it("sheet: keeps the always-visible close button", () => {
518
+ setWidth(true);
519
+ open({ desktop: "sheet" });
520
+ const close = screen.getByRole("button", { name: "Close" });
521
+ expect(close.className).not.toContain("sr-only");
522
+ expect(close).toHaveClass("size-11");
523
+ });
524
+ });
525
+
477
526
  it("stays out of the DOM while closed", () => {
478
527
  render(
479
528
  <ResponsiveOverlay open={false} onOpenChange={() => {}} title="Add an item">
@@ -249,6 +249,50 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
249
249
  // the desktop containers (already frames-checked elsewhere) don't move.
250
250
  const isDrawer = container === "drawer";
251
251
 
252
+ /**
253
+ * The drawer already has two ways to dismiss it that don't need a visible
254
+ * target — a drag down (vaul's `dismissible`) and the backdrop tap — and
255
+ * the frames never draw a circular X on the phone sheet. Drawing one
256
+ * anyway put a mouse-only affordance in a mode designed around touch and
257
+ * gravity.
258
+ *
259
+ * A keyboard user still needs one: `sr-only` (`clip-path`, not
260
+ * `display:none`, so the element stays in the a11y tree and the tab
261
+ * order) keeps it invisible until `:focus-visible` flips it back with
262
+ * `not-sr-only` — at which point it draws as the same 44×44 disc, in the
263
+ * same trailing header slot, that dialog and sheet show all the time.
264
+ * The disc's own box classes (`size-11`, `-m-1.5`, …) are gated behind
265
+ * `focus-visible:` too, not left unconditional, so nothing here fights
266
+ * `sr-only`'s 1px box for specificity while it is hidden.
267
+ */
268
+ const closeButton = isDrawer ? (
269
+ <button
270
+ type="button"
271
+ data-slot="responsive-overlay-close"
272
+ aria-label={closeLabel}
273
+ onClick={() => onOpenChange(false)}
274
+ className="sr-only rounded-full text-muted-foreground transition-colors focus-visible:not-sr-only focus-visible:-m-1.5 focus-visible:grid focus-visible:size-11 focus-visible:shrink-0 focus-visible:place-items-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
275
+ >
276
+ <span className="grid size-8 place-items-center rounded-full bg-surface-2 transition-colors hover:bg-muted">
277
+ <X className="size-4" aria-hidden />
278
+ </span>
279
+ </button>
280
+ ) : (
281
+ <button
282
+ type="button"
283
+ data-slot="responsive-overlay-close"
284
+ onClick={() => onOpenChange(false)}
285
+ // 32px disc, 44px of tap target around it — the frames draw the disc,
286
+ // the accessibility floor asks for the target.
287
+ className="-m-1.5 grid size-11 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
288
+ >
289
+ <span className="grid size-8 place-items-center rounded-full bg-surface-2 transition-colors hover:bg-muted">
290
+ <X className="size-4" aria-hidden />
291
+ </span>
292
+ <span className="sr-only">{closeLabel}</span>
293
+ </button>
294
+ );
295
+
252
296
  const header = (
253
297
  <div
254
298
  data-slot="responsive-overlay-header"
@@ -264,19 +308,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
264
308
  </Description>
265
309
  ) : null}
266
310
  </div>
267
- <button
268
- type="button"
269
- data-slot="responsive-overlay-close"
270
- onClick={() => onOpenChange(false)}
271
- // 32px disc, 44px of tap target around it — the frames draw the disc,
272
- // the accessibility floor asks for the target.
273
- className="-m-1.5 grid size-11 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
274
- >
275
- <span className="grid size-8 place-items-center rounded-full bg-surface-2 transition-colors hover:bg-muted">
276
- <X className="size-4" aria-hidden />
277
- </span>
278
- <span className="sr-only">{closeLabel}</span>
279
- </button>
311
+ {closeButton}
280
312
  </div>
281
313
  );
282
314
 
package/src/globals.css CHANGED
@@ -108,7 +108,11 @@ h6 {
108
108
  */
109
109
  --warning-deep: oklch(0.62 0.15 70);
110
110
  --border: oklch(0.895 0 0);
111
- --input: oklch(0.895 0 0);
111
+ /* Measured off the boards' edge: the outline button and inputs share this
112
+ line. Slightly darker and warmer than --border on purpose — a hairline
113
+ that only separates (border) can sit lighter than one that also has to
114
+ read as an interactive edge (input). */
115
+ --input: oklch(0.86 0.015 60);
112
116
  --ring: oklch(0.25 0.05 35);
113
117
  --chart-1: oklch(0.646 0.222 41.116);
114
118
  --chart-2: oklch(0.6 0.118 184.704);