@bearmenu/ui 0.8.35 → 0.8.36

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 (50) hide show
  1. package/dist/chunk-EGU6EG73.js +39 -0
  2. package/dist/{chunk-NIAL4ZU2.js → chunk-FQAIYKAS.js} +45 -8
  3. package/dist/{chunk-6W25S5CF.js → chunk-KGXTBTCJ.js} +1 -1
  4. package/dist/chunk-LAEVMNJN.js +135 -0
  5. package/dist/{chunk-XNBDACL7.js → chunk-ZUBBMLBD.js} +1 -1
  6. package/dist/components/badge.d.ts +8 -2
  7. package/dist/components/badge.js +1 -1
  8. package/dist/components/banner.d.ts +17 -2
  9. package/dist/components/banner.js +10 -7
  10. package/dist/components/booking-row.js +1 -1
  11. package/dist/components/button-group.js +3 -39
  12. package/dist/components/button.d.ts +2 -2
  13. package/dist/components/command.d.ts +3 -3
  14. package/dist/components/concept-convergence.js +1 -1
  15. package/dist/components/filter-category-row.js +1 -1
  16. package/dist/components/menu-item.js +3 -3
  17. package/dist/components/multi-select-combobox.js +8 -3
  18. package/dist/components/pan-zoom-viewport.d.ts +137 -0
  19. package/dist/components/pan-zoom-viewport.js +531 -0
  20. package/dist/components/place-card.js +2 -2
  21. package/dist/components/place-details-mobile.js +1 -1
  22. package/dist/components/responsive-overlay.d.ts +29 -1
  23. package/dist/components/responsive-overlay.js +45 -6
  24. package/dist/components/sheet.d.ts +6 -0
  25. package/dist/components/sheet.js +3 -133
  26. package/dist/components/signature-chip.js +2 -2
  27. package/dist/components/spinner.d.ts +2 -2
  28. package/dist/components/tabs.d.ts +16 -1
  29. package/dist/components/tabs.js +21 -3
  30. package/dist/components/tag.d.ts +2 -2
  31. package/dist/components/text.d.ts +2 -2
  32. package/dist/components/top-ranking-entry.js +3 -3
  33. package/package.json +1 -1
  34. package/src/components/badge.stories.tsx +32 -1
  35. package/src/components/badge.test.tsx +45 -0
  36. package/src/components/badge.tsx +41 -3
  37. package/src/components/banner.stories.tsx +32 -0
  38. package/src/components/banner.test.tsx +58 -0
  39. package/src/components/banner.tsx +28 -4
  40. package/src/components/multi-select-combobox.tsx +7 -1
  41. package/src/components/pan-zoom-viewport.stories.tsx +134 -0
  42. package/src/components/pan-zoom-viewport.test.tsx +381 -0
  43. package/src/components/pan-zoom-viewport.tsx +727 -0
  44. package/src/components/responsive-overlay.stories.tsx +61 -0
  45. package/src/components/responsive-overlay.test.tsx +104 -0
  46. package/src/components/responsive-overlay.tsx +87 -22
  47. package/src/components/sheet.tsx +14 -3
  48. package/src/components/tabs.stories.tsx +23 -0
  49. package/src/components/tabs.test.tsx +64 -0
  50. package/src/components/tabs.tsx +36 -3
@@ -23,6 +23,32 @@ type ResponsiveOverlayProps = {
23
23
  closeLabel?: string;
24
24
  /** Widths for the desktop dialog. Mobile is always the full-width drawer. */
25
25
  size?: React.ComponentPropsWithoutRef<typeof DialogContent>["size"];
26
+ /**
27
+ * The container on a wide screen. `dialog` (the default) is the centred box.
28
+ * `sheet` is a right rail: an inset panel held 12px off the top, right and
29
+ * bottom, 420px wide — the booking pages' table sheet, booking details,
30
+ * guest panel and create forms (floor/14-w1, calendar/07-n1, guests/07-g6).
31
+ * Narrow screens get the bottom drawer either way.
32
+ */
33
+ desktop?: "dialog" | "sheet";
34
+ /**
35
+ * Whether the wide `sheet` dims and locks the page. `true` (the default)
36
+ * is a form rail: a scrim, focus held inside, the page inert. `false` is a
37
+ * working rail: no scrim, no focus trap, and the page behind stays live —
38
+ * clicking another table on the floor does NOT close it, so the caller can
39
+ * swap what it shows. Escape and the close button still close it.
40
+ *
41
+ * Applies to `desktop="sheet"` only. The dialog is always modal, and so is
42
+ * the phone drawer: at 390 the rail covers most of the page, and a page
43
+ * that stays live under a sheet the thumb cannot see past is a trap.
44
+ */
45
+ modal?: boolean;
46
+ /**
47
+ * Where the wide container takes over from the drawer. `md` (768, the
48
+ * default) for most overlays; `lg` (1024) where the page beside the rail
49
+ * needs the room — the floor plan's rails switch there.
50
+ */
51
+ wideFrom?: "md" | "lg";
26
52
  /** Classes for the panel itself — the dialog box or the drawer sheet. */
27
53
  className?: string;
28
54
  /** Classes for the scrolling body. */
@@ -30,7 +56,9 @@ type ResponsiveOverlayProps = {
30
56
  };
31
57
  /**
32
58
  * ResponsiveOverlay — one overlay authored once: a `Drawer` under md, a
33
- * `Dialog` from md.
59
+ * `Dialog` from md — or, with `desktop="sheet"`, an inset right rail that can
60
+ * leave the page live (`modal={false}`), switching at md or at lg
61
+ * (`wideFrom`).
34
62
  *
35
63
  * The shared-table board draws the same chrome on T1, T3, T5, T6 and T8
36
64
  * (16-shared-table.html) and the two widths never disagree about it: the
@@ -1,13 +1,20 @@
1
- import { useMediaQuery } from '../chunk-OPVKL4VS.js';
1
+ import { useMediaQuery, DESKTOP_MEDIA_QUERY } from '../chunk-OPVKL4VS.js';
2
+ import { Sheet, SheetContent, SheetTitle, SheetDescription } from '../chunk-LAEVMNJN.js';
2
3
  import { Drawer, DrawerContent, DrawerTitle, DrawerDescription } from '../chunk-XLHMF6F5.js';
3
4
  import { Dialog, DialogContent, DialogTitle, DialogDescription } from '../chunk-TVEIUTBQ.js';
4
5
  import { cn } from '../chunk-FEK5XGZW.js';
5
- import '../chunk-MMUBH76A.js';
6
+ import { __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
6
7
  import * as React from 'react';
7
8
  import { X } from 'lucide-react';
8
9
  import { jsx, jsxs } from 'react/jsx-runtime';
9
10
 
10
11
  var MD_MEDIA_QUERY = "(min-width: 768px)";
12
+ var CONTAINER_TITLE = { drawer: DrawerTitle, dialog: DialogTitle, sheet: SheetTitle };
13
+ var CONTAINER_DESCRIPTION = {
14
+ drawer: DrawerDescription,
15
+ dialog: DialogDescription,
16
+ sheet: SheetDescription
17
+ };
11
18
  function useFocusReturn(open) {
12
19
  const openerRef = React.useRef(null);
13
20
  const wasOpen = React.useRef(false);
@@ -44,17 +51,23 @@ var ResponsiveOverlay = React.forwardRef(
44
51
  footer,
45
52
  closeLabel = "Close",
46
53
  size = "lg",
54
+ desktop = "dialog",
55
+ modal = true,
56
+ wideFrom = "md",
47
57
  className,
48
58
  bodyClassName
49
59
  }, ref) => {
50
- const isWide = useMediaQuery(MD_MEDIA_QUERY);
60
+ const isWide = useMediaQuery(wideFrom === "lg" ? DESKTOP_MEDIA_QUERY : MD_MEDIA_QUERY);
61
+ const container = isWide ? desktop : "drawer";
51
62
  const titleId = React.useId();
52
63
  const subtitleId = React.useId();
53
64
  useFocusReturn(open);
65
+ const Title = CONTAINER_TITLE[container];
66
+ const Description = CONTAINER_DESCRIPTION[container];
54
67
  const header = /* @__PURE__ */ jsxs("div", { "data-slot": "responsive-overlay-header", className: "flex items-start gap-3 px-5 pt-5", children: [
55
68
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
56
- isWide ? /* @__PURE__ */ jsx(DialogTitle, { id: titleId, className: "text-[19px] font-bold leading-tight", children: title }) : /* @__PURE__ */ jsx(DrawerTitle, { id: titleId, className: "text-[19px] font-bold leading-tight", children: title }),
57
- subtitle ? isWide ? /* @__PURE__ */ jsx(DialogDescription, { id: subtitleId, className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : /* @__PURE__ */ jsx(DrawerDescription, { id: subtitleId, className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : null
69
+ /* @__PURE__ */ jsx(Title, { id: titleId, className: "text-[19px] font-bold leading-tight", children: title }),
70
+ subtitle ? /* @__PURE__ */ jsx(Description, { id: subtitleId, className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : null
58
71
  ] }),
59
72
  /* @__PURE__ */ jsxs(
60
73
  "button",
@@ -86,7 +99,33 @@ var ResponsiveOverlay = React.forwardRef(
86
99
  children: footer
87
100
  }
88
101
  ) : null;
89
- if (isWide) {
102
+ if (container === "sheet") {
103
+ return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, modal, children: /* @__PURE__ */ jsxs(
104
+ SheetContent,
105
+ __spreadProps(__spreadValues({
106
+ ref,
107
+ side: "right",
108
+ attachment: "inset",
109
+ hideClose: true,
110
+ overlayClassName: "bg-scrim-overlay backdrop-blur-none",
111
+ "aria-labelledby": titleId,
112
+ "aria-describedby": subtitle ? subtitleId : void 0,
113
+ "data-slot": "responsive-overlay-sheet",
114
+ "data-modal": modal ? "true" : "false"
115
+ }, modal ? {} : { onInteractOutside: (event) => event.preventDefault() }), {
116
+ className: cn(
117
+ "flex w-[420px] max-w-[calc(100vw-1.5rem)] flex-col gap-0 bg-card p-0",
118
+ className
119
+ ),
120
+ children: [
121
+ header,
122
+ body,
123
+ foot
124
+ ]
125
+ })
126
+ ) });
127
+ }
128
+ if (container === "dialog") {
90
129
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
91
130
  DialogContent,
92
131
  {
@@ -36,6 +36,12 @@ interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof Dialog
36
36
  * buttons in the same corner.
37
37
  */
38
38
  hideClose?: boolean;
39
+ /**
40
+ * Classes for the scrim. The default `bg-black/50` is kept for every
41
+ * existing caller; pass `bg-scrim-overlay` for the themed dim, as `Drawer`
42
+ * and `Dialog` already allow. A non-modal sheet renders no scrim at all.
43
+ */
44
+ overlayClassName?: string;
39
45
  }
40
46
  declare const SheetContent: React.ForwardRefExoticComponent<SheetContentProps & React.RefAttributes<HTMLDivElement>>;
41
47
  declare const SheetHeader: {
@@ -1,133 +1,3 @@
1
- import { cn } from '../chunk-FEK5XGZW.js';
2
- import { __objRest, __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
3
- import * as React from 'react';
4
- import * as SheetPrimitive from '@radix-ui/react-dialog';
5
- import { cva } from 'class-variance-authority';
6
- import { X } from 'lucide-react';
7
- import { jsx, jsxs } from 'react/jsx-runtime';
8
-
9
- var Sheet = SheetPrimitive.Root;
10
- var SheetTrigger = SheetPrimitive.Trigger;
11
- var SheetClose = SheetPrimitive.Close;
12
- var SheetPortal = SheetPrimitive.Portal;
13
- var SheetOverlay = React.forwardRef((_a, ref) => {
14
- var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
15
- return /* @__PURE__ */ jsx(
16
- SheetPrimitive.Overlay,
17
- __spreadProps(__spreadValues({
18
- className: cn(
19
- "fixed inset-0 z-[1000] bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
- className
21
- )
22
- }, props), {
23
- ref
24
- })
25
- );
26
- });
27
- SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
28
- var sheetVariants = cva(
29
- "fixed z-[1000] gap-4 bg-background border-border shadow-[var(--shadow-overlay)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-[var(--duration-deliberate)] data-[state=closed]:duration-[var(--duration-exit-standard)] data-[state=open]:ease-[var(--ease-enter)] data-[state=closed]:ease-[var(--ease-exit)]",
30
- {
31
- variants: {
32
- side: {
33
- top: "inset-x-0 top-0 data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
34
- bottom: "inset-x-0 bottom-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
35
- left: "inset-y-0 left-0 h-full w-3/4 data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
36
- right: "inset-y-0 right-0 h-full w-3/4 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right"
37
- },
38
- attachment: {
39
- edge: "p-6",
40
- // Padding is the caller's business here: a detail surface runs a gallery
41
- // and a sticky action bar to the panel edge, and a fixed p-6 would inset
42
- // both. `edge` keeps its p-6 so nothing already shipped shifts.
43
- inset: "rounded-2xl overflow-hidden"
44
- }
45
- },
46
- compoundVariants: [
47
- // `edge` restates exactly what each side carried before `attachment`
48
- // existed — the seam border and the two inward-facing corners. Kept as
49
- // compounds rather than left on `side` so `inset` is not fighting them
50
- // through class-merge order.
51
- // `sm:max-w-sm` lives here, not on `side`: an inset panel is sized by its
52
- // caller, and a responsive cap left on `side` cannot be overridden from
53
- // className — tailwind-merge scopes `sm:max-w-*` and `max-w-*` to
54
- // different groups, so `max-w-none` loses to it at every width ≥ sm.
55
- { side: "top", attachment: "edge", class: "border-b" },
56
- { side: "bottom", attachment: "edge", class: "border-t rounded-t-2xl" },
57
- { side: "left", attachment: "edge", class: "border-r rounded-r-2xl sm:max-w-sm" },
58
- { side: "right", attachment: "edge", class: "border-l rounded-l-2xl sm:max-w-sm" },
59
- // The inset offsets live here rather than on `attachment` because each
60
- // side has to give up its own edge: a right-side panel clears top, right
61
- // and bottom but is still anchored to the right.
62
- { side: "right", attachment: "inset", class: "inset-y-3 right-3 h-auto" },
63
- { side: "left", attachment: "inset", class: "inset-y-3 left-3 h-auto" },
64
- { side: "top", attachment: "inset", class: "inset-x-3 top-3" },
65
- { side: "bottom", attachment: "inset", class: "inset-x-3 bottom-3" }
66
- ],
67
- defaultVariants: {
68
- side: "right",
69
- attachment: "edge"
70
- }
71
- }
72
- );
73
- var SheetContent = React.forwardRef((_a, ref) => {
74
- var _b = _a, { side = "right", attachment = "edge", className, children, hideClose } = _b, props = __objRest(_b, ["side", "attachment", "className", "children", "hideClose"]);
75
- return /* @__PURE__ */ jsxs(SheetPortal, { children: [
76
- /* @__PURE__ */ jsx(SheetOverlay, {}),
77
- /* @__PURE__ */ jsxs(
78
- SheetPrimitive.Content,
79
- __spreadProps(__spreadValues({
80
- ref,
81
- className: cn(sheetVariants({ side, attachment }), className)
82
- }, props), {
83
- children: [
84
- children,
85
- !hideClose && /* @__PURE__ */ jsxs(SheetPrimitive.Close, { className: "absolute right-4 top-4 rounded-lg opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary glass-hover p-2", children: [
86
- /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
87
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
88
- ] })
89
- ]
90
- })
91
- )
92
- ] });
93
- });
94
- SheetContent.displayName = SheetPrimitive.Content.displayName;
95
- var SheetHeader = (_a) => {
96
- var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
97
- return /* @__PURE__ */ jsx("div", __spreadValues({ className: cn("flex flex-col space-y-2 text-center sm:text-left", className) }, props));
98
- };
99
- SheetHeader.displayName = "SheetHeader";
100
- var SheetFooter = (_a) => {
101
- var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
102
- return /* @__PURE__ */ jsx(
103
- "div",
104
- __spreadValues({
105
- className: cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)
106
- }, props)
107
- );
108
- };
109
- SheetFooter.displayName = "SheetFooter";
110
- var SheetTitle = React.forwardRef((_a, ref) => {
111
- var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
112
- return /* @__PURE__ */ jsx(
113
- SheetPrimitive.Title,
114
- __spreadValues({
115
- ref,
116
- className: cn("text-lg font-semibold text-foreground", className)
117
- }, props)
118
- );
119
- });
120
- SheetTitle.displayName = SheetPrimitive.Title.displayName;
121
- var SheetDescription = React.forwardRef((_a, ref) => {
122
- var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
123
- return /* @__PURE__ */ jsx(
124
- SheetPrimitive.Description,
125
- __spreadValues({
126
- ref,
127
- className: cn("text-sm text-muted-foreground", className)
128
- }, props)
129
- );
130
- });
131
- SheetDescription.displayName = SheetPrimitive.Description.displayName;
132
-
133
- export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger };
1
+ export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger } from '../chunk-LAEVMNJN.js';
2
+ import '../chunk-FEK5XGZW.js';
3
+ import '../chunk-MMUBH76A.js';
@@ -1,4 +1,4 @@
1
- export { SignatureChip } from '../chunk-6W25S5CF.js';
2
- import '../chunk-NIAL4ZU2.js';
1
+ export { SignatureChip } from '../chunk-KGXTBTCJ.js';
2
+ import '../chunk-FQAIYKAS.js';
3
3
  import '../chunk-FEK5XGZW.js';
4
4
  import '../chunk-MMUBH76A.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?: "primary" | "default" | "inherit" | "destructive" | null | undefined;
7
+ tone?: "default" | "inherit" | "primary" | "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?: "primary" | "default" | "inherit" | "destructive" | null | undefined;
11
+ tone?: "default" | "inherit" | "primary" | "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;
@@ -8,7 +8,22 @@ declare const TabsList: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsL
8
8
  /** Make horizontal lists scrollable when overflowing. Default true for underline, false for pill. */
9
9
  scrollable?: boolean;
10
10
  } & React.RefAttributes<HTMLDivElement>>;
11
- declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
11
+ declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & {
12
+ /**
13
+ * A count after the label — "Today 3", "Queue 2" (today/03-d0). `null`
14
+ * means the count is unknown (still loading, or its read failed) and draws
15
+ * an em dash, "Queue —" (today/04-d3); `undefined` draws nothing.
16
+ * Tabular figures, so the strip does not shift as a count ticks.
17
+ */
18
+ count?: number | null;
19
+ /**
20
+ * What a screen reader hears for the count, in place of the bare figure —
21
+ * "3 waiting", "count unavailable". Injected, like every label here: the
22
+ * design system holds no copy. Without it the figure is read as is and the
23
+ * dash is silent.
24
+ */
25
+ countLabel?: string;
26
+ } & React.RefAttributes<HTMLButtonElement>>;
12
27
  declare const TabsContent: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
13
28
 
14
29
  export { Tabs, TabsContent, TabsList, TabsTrigger };
@@ -4,7 +4,7 @@ import { __objRest, __spreadValues, __spreadProps } from '../chunk-MMUBH76A.js';
4
4
  import * as React from 'react';
5
5
  import * as TabsPrimitive from '@radix-ui/react-tabs';
6
6
  import { motion } from 'framer-motion';
7
- import { jsx, jsxs } from 'react/jsx-runtime';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
8
 
9
9
  var TabsContext = React.createContext({
10
10
  variant: "pill",
@@ -50,7 +50,7 @@ var TabsList = React.forwardRef((_a, ref) => {
50
50
  });
51
51
  TabsList.displayName = TabsPrimitive.List.displayName;
52
52
  var TabsTrigger = React.forwardRef((_a, ref) => {
53
- var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
53
+ var _b = _a, { className, children, count, countLabel } = _b, props = __objRest(_b, ["className", "children", "count", "countLabel"]);
54
54
  const { variant, orientation, indicatorId } = React.useContext(TabsContext);
55
55
  const triggerRef = React.useRef(null);
56
56
  const [isActive, setIsActive] = React.useState(false);
@@ -111,7 +111,25 @@ var TabsTrigger = React.forwardRef((_a, ref) => {
111
111
  )
112
112
  }
113
113
  ),
114
- /* @__PURE__ */ jsx("span", { className: "relative z-10 flex items-center gap-2", children })
114
+ /* @__PURE__ */ jsxs("span", { className: "relative z-10 flex items-center gap-2", children: [
115
+ children,
116
+ count !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
117
+ " ",
118
+ /* @__PURE__ */ jsx(
119
+ "span",
120
+ {
121
+ "data-slot": "tabs-trigger-count",
122
+ "aria-hidden": countLabel !== void 0 || count === null ? true : void 0,
123
+ className: "tabular-nums",
124
+ children: count === null ? "\u2014" : count
125
+ }
126
+ ),
127
+ countLabel !== void 0 ? /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
128
+ " ",
129
+ countLabel
130
+ ] }) : null
131
+ ] }) : null
132
+ ] })
115
133
  ]
116
134
  })
117
135
  );
@@ -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?: "primary" | "default" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
6
+ variant?: "default" | "primary" | "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?: "primary" | "default" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
10
+ variant?: "default" | "primary" | "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?: "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;
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;
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,8 +1,8 @@
1
- import { MenuItem } from '../chunk-XNBDACL7.js';
2
- import '../chunk-6W25S5CF.js';
1
+ import { MenuItem } from '../chunk-ZUBBMLBD.js';
2
+ import '../chunk-KGXTBTCJ.js';
3
3
  import { Separator } from '../chunk-NWZGSLPU.js';
4
4
  import { Skeleton } from '../chunk-XBBEX4SF.js';
5
- import '../chunk-NIAL4ZU2.js';
5
+ import '../chunk-FQAIYKAS.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.35",
3
+ "version": "0.8.36",
4
4
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { ChevronDown, Users } from "lucide-react";
2
+ import { Armchair, Check, ChevronDown, CircleHelp, Clock, Users } from "lucide-react";
3
3
  import { Avatar, AvatarFallback, AvatarGroup } from "./avatar";
4
4
  import { Badge } from "./badge";
5
5
  import { Button } from "./button";
@@ -22,6 +22,10 @@ const meta = {
22
22
  "warning",
23
23
  "brown",
24
24
  "onPrimary",
25
+ "softSuccess",
26
+ "softWarning",
27
+ "softDestructive",
28
+ "softNeutral",
25
29
  ],
26
30
  },
27
31
  },
@@ -126,6 +130,33 @@ export const Assignee: Story = {
126
130
  ),
127
131
  };
128
132
 
133
+ /**
134
+ * Soft tones with an icon — a booking's live status in a table row
135
+ * (calendar/11-dp): a wash of the status colour, its ink on top, a 14px glyph
136
+ * so the status never rests on colour alone.
137
+ */
138
+ export const SoftTones: Story = {
139
+ render: () => (
140
+ <div className="flex flex-wrap items-center gap-2">
141
+ <Badge as="span" variant="softSuccess" icon={<Armchair />}>
142
+ Seated
143
+ </Badge>
144
+ <Badge as="span" variant="softDestructive" icon={<Clock />}>
145
+ 25 min late
146
+ </Badge>
147
+ <Badge as="span" variant="softWarning" icon={<CircleHelp />}>
148
+ Needs an answer
149
+ </Badge>
150
+ <Badge as="span" variant="softNeutral" icon={<Check />}>
151
+ Confirmed
152
+ </Badge>
153
+ <Badge as="span" variant="softNeutral">
154
+ Now
155
+ </Badge>
156
+ </div>
157
+ ),
158
+ };
159
+
129
160
  export const AllVariants: Story = {
130
161
  render: () => (
131
162
  <div className="flex flex-wrap items-center gap-3">
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Armchair } from "lucide-react";
4
+ import { Badge } from "./badge";
5
+
6
+ describe("Badge", () => {
7
+ it("renders the icon before the label and hides it from assistive tech", () => {
8
+ render(
9
+ <Badge variant="softSuccess" icon={<Armchair data-testid="glyph" />}>
10
+ Seated
11
+ </Badge>
12
+ );
13
+ const badge = screen.getByText("Seated");
14
+ const icon = badge.querySelector('[data-slot="badge-icon"]');
15
+ expect(icon).toHaveAttribute("aria-hidden", "true");
16
+ expect(icon).toContainElement(screen.getByTestId("glyph"));
17
+ expect(badge.firstElementChild).toBe(icon);
18
+ expect(badge).toHaveClass("gap-1");
19
+ });
20
+
21
+ it("renders no icon slot and no gap when there is no icon", () => {
22
+ render(<Badge variant="softNeutral">Confirmed</Badge>);
23
+ const badge = screen.getByText("Confirmed");
24
+ expect(badge.querySelector('[data-slot="badge-icon"]')).toBeNull();
25
+ expect(badge).not.toHaveClass("gap-1");
26
+ });
27
+
28
+ it.each([
29
+ ["softSuccess", "bg-success/15", "text-success-ink"],
30
+ ["softWarning", "bg-warning/20", "text-warning-ink"],
31
+ ["softDestructive", "bg-destructive/12", "text-destructive-ink"],
32
+ ["softNeutral", "bg-surface-2", "text-ink-secondary"],
33
+ ] as const)("%s paints a wash with the tone's ink token", (variant, wash, ink) => {
34
+ render(<Badge variant={variant}>Status</Badge>);
35
+ const badge = screen.getByText("Status");
36
+ expect(badge).toHaveClass(wash, ink);
37
+ // The solid fill tokens fall short of AA as text on their own wash.
38
+ expect(badge.className).not.toMatch(/\btext-(success|warning|destructive)\b(?!-)/);
39
+ });
40
+
41
+ it("keeps an existing variant unchanged", () => {
42
+ render(<Badge variant="success">Solid</Badge>);
43
+ expect(screen.getByText("Solid")).toHaveClass("bg-success", "text-success-foreground");
44
+ });
45
+ });
@@ -58,6 +58,22 @@ const badgeVariants = cva(
58
58
  */
59
59
  assignee:
60
60
  "rounded-full border-0 bg-surface-2 py-1 pl-1 pr-2 gap-1.5 text-[12px] leading-[1.2] font-semibold text-foreground",
61
+ /**
62
+ * Soft tones — a wash of the status colour with its INK on top, for a
63
+ * status that sits in a list and should not shout: the booking
64
+ * design's "Seated" (success), "25 min late" (destructive), "Needs an
65
+ * answer" (warning) and "Confirmed" (neutral) pills in a table row
66
+ * (calendar/11-dp, floor/14-w1, queue/02-q0).
67
+ *
68
+ * The ink tokens, not the solid ones: `--success` / `--warning` /
69
+ * `--destructive` are tuned as fills and fall short of AA as text on
70
+ * their own wash; `--*-ink` exists for exactly that. Usually paired
71
+ * with an `icon`, so the status does not rest on colour alone.
72
+ */
73
+ softSuccess: "rounded-md border-0 bg-success/15 px-2 py-1 text-success-ink",
74
+ softWarning: "rounded-md border-0 bg-warning/20 px-2 py-1 text-warning-ink",
75
+ softDestructive: "rounded-md border-0 bg-destructive/12 px-2 py-1 text-destructive-ink",
76
+ softNeutral: "rounded-md border-0 bg-surface-2 px-2 py-1 text-ink-secondary",
61
77
  },
62
78
  /**
63
79
  * `status` is the taller pill the booking design draws beside a
@@ -94,15 +110,37 @@ export interface BadgeProps
94
110
  * clickable row.
95
111
  */
96
112
  as?: "div" | "span";
113
+ /**
114
+ * A lucide glyph drawn at 14px before the label — the armchair on
115
+ * "Seated", the clock on "25 min late". Decorative: the label carries the
116
+ * meaning, so the slot is `aria-hidden`.
117
+ */
118
+ icon?: React.ReactNode;
97
119
  }
98
120
 
99
- function Badge({ className, variant, size, live, as: Tag = "div", ...props }: BadgeProps) {
121
+ function Badge({
122
+ className,
123
+ variant,
124
+ size,
125
+ live,
126
+ as: Tag = "div",
127
+ icon,
128
+ children,
129
+ ...props
130
+ }: BadgeProps) {
100
131
  return (
101
132
  <Tag
102
133
  data-slot="badge"
103
- className={cn(badgeVariants({ variant, size, live }), className)}
134
+ className={cn(badgeVariants({ variant, size, live }), icon ? "gap-1" : undefined, className)}
104
135
  {...props}
105
- />
136
+ >
137
+ {icon ? (
138
+ <span data-slot="badge-icon" aria-hidden className="inline-flex shrink-0 [&>svg]:size-3.5">
139
+ {icon}
140
+ </span>
141
+ ) : null}
142
+ {children}
143
+ </Tag>
106
144
  );
107
145
  }
108
146
 
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { TriangleAlert } from "lucide-react";
3
3
  import { Banner } from "./banner";
4
+ import { Button } from "./button";
4
5
 
5
6
  const meta: Meta<typeof Banner> = {
6
7
  title: "Booking/Banner",
@@ -30,6 +31,37 @@ export const Warning: Story = {
30
31
  },
31
32
  };
32
33
 
34
+ /** today/04-d3: a read that failed, on a page that is otherwise live. */
35
+ export const Destructive: Story = {
36
+ args: {
37
+ tone: "destructive",
38
+ children: (
39
+ <>
40
+ <b className="font-bold">The queue didn&apos;t load.</b> This is on our side; everything
41
+ else is live.
42
+ </>
43
+ ),
44
+ },
45
+ };
46
+
47
+ /** reports/03-p9: the failure carries its own way out. */
48
+ export const DestructiveWithAction: Story = {
49
+ args: {
50
+ tone: "destructive",
51
+ children: (
52
+ <>
53
+ <b className="font-bold">Walk-ins didn&apos;t load.</b> Covers and walk-in figures read —
54
+ until they do.
55
+ </>
56
+ ),
57
+ action: (
58
+ <Button variant="link" className="h-auto p-0 font-bold text-destructive-ink">
59
+ Retry
60
+ </Button>
61
+ ),
62
+ },
63
+ };
64
+
33
65
  /** B0c: a venue that takes no bookings. */
34
66
  export const Info: Story = {
35
67
  args: { tone: "info", children: "This venue does not take bookings. Walk-ins only." },