@bearmenu/ui 0.8.35 → 0.8.37

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 (76) 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-TVEIUTBQ.js → chunk-IQIJCMG2.js} +1 -1
  4. package/dist/{chunk-6W25S5CF.js → chunk-KGXTBTCJ.js} +1 -1
  5. package/dist/chunk-LAEVMNJN.js +135 -0
  6. package/dist/{chunk-VCRXL7H5.js → chunk-PGQXVJXW.js} +2 -2
  7. package/dist/{chunk-F3XK34DV.js → chunk-TB2K5JN3.js} +2 -2
  8. package/dist/{chunk-XNBDACL7.js → chunk-ZUBBMLBD.js} +1 -1
  9. package/dist/components/badge.d.ts +8 -2
  10. package/dist/components/badge.js +1 -1
  11. package/dist/components/banner.d.ts +17 -2
  12. package/dist/components/banner.js +10 -7
  13. package/dist/components/booking-row.js +1 -1
  14. package/dist/components/button-group.js +3 -39
  15. package/dist/components/calendar.js +1 -1
  16. package/dist/components/combobox.js +3 -3
  17. package/dist/components/command.js +2 -2
  18. package/dist/components/concept-convergence.js +1 -1
  19. package/dist/components/dialog.js +1 -1
  20. package/dist/components/dropdown-menu.js +6 -6
  21. package/dist/components/filter-category-row.js +1 -1
  22. package/dist/components/input-group.js +1 -1
  23. package/dist/components/menu-item.js +3 -3
  24. package/dist/components/multi-select-combobox.js +10 -5
  25. package/dist/components/multi-select-menu.js +1 -1
  26. package/dist/components/pan-zoom-viewport.d.ts +137 -0
  27. package/dist/components/pan-zoom-viewport.js +531 -0
  28. package/dist/components/place-card.js +2 -2
  29. package/dist/components/place-details-mobile.js +1 -1
  30. package/dist/components/responsive-overlay.d.ts +29 -1
  31. package/dist/components/responsive-overlay.js +46 -7
  32. package/dist/components/searchable-select.js +3 -3
  33. package/dist/components/select.js +2 -2
  34. package/dist/components/selection-bar.js +1 -1
  35. package/dist/components/sheet.d.ts +6 -0
  36. package/dist/components/sheet.js +3 -133
  37. package/dist/components/signature-chip.js +2 -2
  38. package/dist/components/tabs.d.ts +16 -1
  39. package/dist/components/tabs.js +21 -3
  40. package/dist/components/toggle-group.js +1 -1
  41. package/dist/components/toggle.d.ts +6 -0
  42. package/dist/components/toggle.js +1 -1
  43. package/dist/components/top-ranking-entry.js +3 -3
  44. package/package.json +1 -1
  45. package/src/components/badge.stories.tsx +32 -1
  46. package/src/components/badge.test.tsx +45 -0
  47. package/src/components/badge.tsx +41 -3
  48. package/src/components/banner.stories.tsx +32 -0
  49. package/src/components/banner.test.tsx +58 -0
  50. package/src/components/banner.tsx +28 -4
  51. package/src/components/calendar.tsx +1 -1
  52. package/src/components/combobox.tsx +1 -1
  53. package/src/components/command.test.tsx +10 -0
  54. package/src/components/command.tsx +1 -1
  55. package/src/components/dialog.tsx +1 -1
  56. package/src/components/dropdown-menu.test.tsx +31 -0
  57. package/src/components/dropdown-menu.tsx +6 -6
  58. package/src/components/input-group.tsx +1 -1
  59. package/src/components/multi-select-combobox.tsx +7 -1
  60. package/src/components/multi-select-menu.tsx +1 -1
  61. package/src/components/pan-zoom-viewport.stories.tsx +134 -0
  62. package/src/components/pan-zoom-viewport.test.tsx +381 -0
  63. package/src/components/pan-zoom-viewport.tsx +727 -0
  64. package/src/components/responsive-overlay.stories.tsx +61 -0
  65. package/src/components/responsive-overlay.test.tsx +104 -0
  66. package/src/components/responsive-overlay.tsx +87 -22
  67. package/src/components/searchable-select.tsx +1 -1
  68. package/src/components/select.tsx +2 -2
  69. package/src/components/selection-bar.tsx +1 -1
  70. package/src/components/sheet.tsx +14 -3
  71. package/src/components/tabs.stories.tsx +23 -0
  72. package/src/components/tabs.test.tsx +64 -0
  73. package/src/components/tabs.tsx +36 -3
  74. package/src/components/toggle.test.tsx +19 -0
  75. package/src/components/toggle.tsx +8 -2
  76. package/src/globals.css +28 -19
@@ -1,6 +1,6 @@
1
1
  import { Popover, PopoverTrigger, PopoverContent } from '../chunk-2ADCZXZJ.js';
2
- import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-VCRXL7H5.js';
3
- import '../chunk-TVEIUTBQ.js';
2
+ import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-PGQXVJXW.js';
3
+ import '../chunk-IQIJCMG2.js';
4
4
  import { Button } from '../chunk-5SGKGFMN.js';
5
5
  import '../chunk-RIH2IS7X.js';
6
6
  import '../chunk-5YNIQ2BL.js';
@@ -99,7 +99,7 @@ function SearchableSelect({
99
99
  e.stopPropagation();
100
100
  onValueChange(void 0);
101
101
  },
102
- className: "absolute right-9 top-1/2 -translate-y-1/2 inline-flex size-6 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors",
102
+ className: "absolute right-9 top-1/2 -translate-y-1/2 inline-flex size-6 items-center justify-center rounded-full text-muted-foreground hover:bg-accent-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors",
103
103
  children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
104
104
  }
105
105
  )
@@ -118,12 +118,12 @@ var SelectItem = React.forwardRef((_a, ref) => {
118
118
  ref,
119
119
  "data-slot": "select-item",
120
120
  className: cn(
121
- "relative flex w-full cursor-default select-none items-center rounded-md py-2 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[state=checked]:font-semibold",
121
+ "relative flex w-full cursor-default select-none items-center rounded-md py-2 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent-subtle data-[highlighted]:text-foreground data-[state=checked]:font-semibold",
122
122
  className
123
123
  )
124
124
  }, props), {
125
125
  children: [
126
- /* @__PURE__ */ jsx("span", { className: "absolute left-2 flex size-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "size-4" }) }) }),
126
+ /* @__PURE__ */ jsx("span", { className: "absolute left-2 flex size-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "size-4 text-accent" }) }) }),
127
127
  /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
128
128
  ]
129
129
  })
@@ -43,7 +43,7 @@ var SelectionBar = React.forwardRef(
43
43
  type: "button",
44
44
  "aria-label": "Clear selection",
45
45
  onClick: onClear,
46
- className: "inline-flex size-6 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
46
+ className: "inline-flex size-6 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
47
47
  children: /* @__PURE__ */ jsx(X, { className: "size-4" })
48
48
  }
49
49
  ),
@@ -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';
@@ -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
  );
@@ -1,4 +1,4 @@
1
- import { toggleVariants } from '../chunk-F3XK34DV.js';
1
+ import { toggleVariants } from '../chunk-TB2K5JN3.js';
2
2
  import { cn } from '../chunk-FEK5XGZW.js';
3
3
  import { __objRest, __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
4
4
  import * as React from 'react';
@@ -4,6 +4,12 @@ import * as React from 'react';
4
4
  import * as TogglePrimitive from '@radix-ui/react-toggle';
5
5
  import { VariantProps } from 'class-variance-authority';
6
6
 
7
+ /**
8
+ * Hover is a step of the surface (`--accent-subtle`), as Button's quiet
9
+ * variants do. The shadcn default, `hover:bg-accent/50 hover:text-accent-foreground`,
10
+ * put white type on a half-strength terracotta: 2.2:1. The on-state's
11
+ * `data-[state=on]:` utilities sort after `hover:` and still win while on.
12
+ */
7
13
  declare const toggleVariants: (props?: ({
8
14
  variant?: "default" | "tab" | "outline" | "chip" | "segmented" | null | undefined;
9
15
  size?: "default" | "sm" | "lg" | null | undefined;
@@ -1,3 +1,3 @@
1
- export { Toggle, toggleVariants } from '../chunk-F3XK34DV.js';
1
+ export { Toggle, toggleVariants } from '../chunk-TB2K5JN3.js';
2
2
  import '../chunk-FEK5XGZW.js';
3
3
  import '../chunk-MMUBH76A.js';
@@ -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.37",
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." },
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Banner } from "./banner";
5
+
6
+ describe("Banner", () => {
7
+ it("paints the destructive tone with the ink token and a hairline", () => {
8
+ render(<Banner tone="destructive">The queue didn&apos;t load.</Banner>);
9
+ const banner = document.querySelector('[data-slot="banner"]');
10
+ expect(banner).toHaveClass("bg-destructive/10", "text-destructive-ink", "border");
11
+ });
12
+
13
+ it("draws the destructive tone's own glyph unless told otherwise", () => {
14
+ const { rerender } = render(<Banner tone="destructive">Failed</Banner>);
15
+ const banner = document.querySelector('[data-slot="banner"]');
16
+ expect(banner?.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
17
+ rerender(
18
+ <Banner tone="destructive" icon={null}>
19
+ Failed
20
+ </Banner>
21
+ );
22
+ expect(banner?.querySelector("svg")).toBeNull();
23
+ });
24
+
25
+ it("places the action after the text and leaves it working", async () => {
26
+ const onRetry = vi.fn();
27
+ render(
28
+ <Banner
29
+ tone="destructive"
30
+ action={
31
+ <button type="button" onClick={onRetry}>
32
+ Retry
33
+ </button>
34
+ }
35
+ >
36
+ Walk-ins didn&apos;t load.
37
+ </Banner>
38
+ );
39
+ const banner = document.querySelector('[data-slot="banner"]');
40
+ const slot = document.querySelector('[data-slot="banner-action"]');
41
+ expect(banner?.lastElementChild).toBe(slot);
42
+ await userEvent.click(screen.getByRole("button", { name: "Retry" }));
43
+ expect(onRetry).toHaveBeenCalledOnce();
44
+ });
45
+
46
+ it("renders no action slot without an action", () => {
47
+ render(<Banner>Walk-ins only.</Banner>);
48
+ expect(document.querySelector('[data-slot="banner-action"]')).toBeNull();
49
+ });
50
+
51
+ it("keeps the info and warning tones borderless", () => {
52
+ const { rerender } = render(<Banner tone="info">Note</Banner>);
53
+ const banner = document.querySelector('[data-slot="banner"]');
54
+ expect(banner).not.toHaveClass("border");
55
+ rerender(<Banner tone="warning">Note</Banner>);
56
+ expect(banner).not.toHaveClass("border");
57
+ });
58
+ });