@bearmenu/ui 0.8.52 → 0.8.54

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 (44) hide show
  1. package/dist/{chunk-4WW3HPIW.js → chunk-6Z6MBILC.js} +8 -4
  2. package/dist/chunk-GIJGVFGC.js +177 -0
  3. package/dist/chunk-RKNPPYRU.js +86 -0
  4. package/dist/{chunk-2ADCZXZJ.js → chunk-RXXD3UYP.js} +3 -1
  5. package/dist/components/alert.d.ts +2 -2
  6. package/dist/components/badge.d.ts +1 -1
  7. package/dist/components/button.d.ts +2 -2
  8. package/dist/components/combobox.js +1 -1
  9. package/dist/components/event-detail-panel.d.ts +17 -6
  10. package/dist/components/event-detail-panel.js +58 -8
  11. package/dist/components/facet-group.d.ts +9 -2
  12. package/dist/components/facet-group.js +108 -10
  13. package/dist/components/filter-option-chip.d.ts +15 -0
  14. package/dist/components/filter-option-chip.js +3 -0
  15. package/dist/components/filter-option-group.d.ts +37 -0
  16. package/dist/components/filter-option-group.js +10 -0
  17. package/dist/components/filter-panel-footer.d.ts +15 -0
  18. package/dist/components/filter-panel-footer.js +63 -0
  19. package/dist/components/menu-item.d.ts +24 -3
  20. package/dist/components/menu-item.js +1 -1
  21. package/dist/components/multi-select-combobox.js +1 -1
  22. package/dist/components/place-card.js +1 -1
  23. package/dist/components/popover.js +1 -1
  24. package/dist/components/searchable-select.js +1 -1
  25. package/dist/components/spinner.d.ts +2 -2
  26. package/dist/components/tag.d.ts +2 -2
  27. package/dist/components/text.d.ts +1 -1
  28. package/dist/components/toggle.d.ts +1 -1
  29. package/dist/components/top-ranking-entry.js +1 -1
  30. package/package.json +11 -8
  31. package/src/components/event-detail-panel.stories.tsx +16 -0
  32. package/src/components/event-detail-panel.test.tsx +36 -0
  33. package/src/components/event-detail-panel.tsx +86 -14
  34. package/src/components/facet-group.tsx +153 -54
  35. package/src/components/filter-option-chip.test.tsx +50 -0
  36. package/src/components/filter-option-chip.tsx +81 -0
  37. package/src/components/filter-option-group.stories.tsx +250 -0
  38. package/src/components/filter-option-group.test.tsx +100 -0
  39. package/src/components/filter-option-group.tsx +231 -0
  40. package/src/components/filter-panel-footer.tsx +63 -0
  41. package/src/components/menu-item.stories.tsx +82 -0
  42. package/src/components/menu-item.test.tsx +55 -1
  43. package/src/components/menu-item.tsx +31 -4
  44. package/src/components/popover.tsx +3 -1
@@ -0,0 +1,82 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { MenuItem, type MenuItemLabels } from "./menu-item";
3
+ import type { MenuItemPreview } from "./menu-types";
4
+ import { mockGradients } from "./menu-mocks";
5
+
6
+ const tagliatelle: MenuItemPreview = {
7
+ id: "i1",
8
+ slug: "truffle-tagliatelle",
9
+ name: "Truffle Tagliatelle",
10
+ kind: "dish",
11
+ category_slug: "pasta",
12
+ description: "Fresh pasta, black truffle, parmesan.",
13
+ mentions: 17,
14
+ sizes_and_prices: [{ size: 320, price: 62 }],
15
+ };
16
+
17
+ const mentionLabels: MenuItemLabels = {
18
+ currency: "lei",
19
+ mentions: (count) => `Mentioned in ${count} reviews`,
20
+ signature: "SIGNATURE",
21
+ mentioned: "MENTIONED",
22
+ };
23
+
24
+ // The label and the kind move together: "Praised in" with the thumbs-up,
25
+ // "Mentioned in" with the quote glyph.
26
+ const praiseLabels: MenuItemLabels = {
27
+ ...mentionLabels,
28
+ mentions: (count) => `Praised in ${count} reviews`,
29
+ };
30
+
31
+ const meta: Meta<typeof MenuItem> = {
32
+ title: "Menu/MenuItem",
33
+ component: MenuItem,
34
+ tags: ["autodocs"],
35
+ decorators: [
36
+ (Story) => (
37
+ <div className="w-[390px] bg-background">
38
+ <Story />
39
+ </div>
40
+ ),
41
+ ],
42
+ args: {
43
+ item: tagliatelle,
44
+ labels: mentionLabels,
45
+ formatPrice: String,
46
+ markGradient: mockGradients.pasta,
47
+ onOpen: () => {},
48
+ onMentions: () => {},
49
+ },
50
+ argTypes: {
51
+ countKind: { control: "inline-radio", options: ["mentions", "praise"] },
52
+ },
53
+ parameters: {
54
+ docs: {
55
+ description: {
56
+ component:
57
+ "THE menu row. The count at the right edge is drawn with one of two glyphs, set by `countKind`. `\"mentions\"` (the default) counts every review that named the item, complaints included, and takes the quote glyph. `\"praise\"` counts only the reviews that praised it and takes a thumbs-up. A thumbs-up never sits on a count that includes complaints, so pass `\"praise\"` only when the number is praise.",
58
+ },
59
+ },
60
+ },
61
+ };
62
+
63
+ export default meta;
64
+ type Story = StoryObj<typeof MenuItem>;
65
+
66
+ /** The default: every mention, complaints included. Quote glyph. */
67
+ export const Mentions: Story = {};
68
+
69
+ /** A praise-only count. Thumbs-up, in the same slot as the quote glyph. */
70
+ export const Praise: Story = {
71
+ args: { countKind: "praise", labels: praiseLabels },
72
+ };
73
+
74
+ /** The two kinds stacked, so the glyphs can be compared in place. */
75
+ export const CountKinds: Story = {
76
+ render: (args) => (
77
+ <div className="divide-y divide-border">
78
+ <MenuItem {...args} countKind="mentions" labels={mentionLabels} />
79
+ <MenuItem {...args} countKind="praise" labels={praiseLabels} />
80
+ </div>
81
+ ),
82
+ };
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { render, screen } from "@testing-library/react";
3
- import { MenuItem, type MenuItemLabels } from "./menu-item";
3
+ import { MentionSignal, MenuItem, type MenuItemLabels } from "./menu-item";
4
4
  import type { MenuItemPreview } from "./menu-types";
5
5
 
6
6
  const wine: MenuItemPreview = {
@@ -55,3 +55,57 @@ describe("MenuItem external rating", () => {
55
55
  expect(screen.getByRole("img", { name: "Rated 3.9 on Vivino" })).toBeTruthy();
56
56
  });
57
57
  });
58
+
59
+ describe("MenuItem count glyph", () => {
60
+ const praiseLabels: MenuItemLabels = {
61
+ ...labels,
62
+ mentions: (count) => `Praised in ${count} reviews`,
63
+ };
64
+
65
+ it("draws the quote glyph when no kind is passed, so existing callers look the same", () => {
66
+ render(<MenuItem item={wine} labels={labels} formatPrice={String} />);
67
+ const count = screen.getByLabelText("Mentioned in 4 reviews");
68
+ const glyph = count.querySelector("svg");
69
+ expect(glyph).toHaveClass("lucide-message-square-quote", "h-3.5", "w-3.5", "opacity-60");
70
+ expect(count.querySelector(".lucide-thumbs-up")).toBeNull();
71
+ });
72
+
73
+ it("draws the quote glyph for raw mentions, which can include complaints", () => {
74
+ render(<MenuItem item={wine} labels={labels} formatPrice={String} countKind="mentions" />);
75
+ const count = screen.getByLabelText("Mentioned in 4 reviews");
76
+ expect(count.querySelector("svg")).toHaveClass("lucide-message-square-quote");
77
+ expect(count.querySelector(".lucide-thumbs-up")).toBeNull();
78
+ });
79
+
80
+ it("draws a thumbs-up in the quote glyph's slot when the count is praise", () => {
81
+ render(
82
+ <MenuItem
83
+ item={wine}
84
+ labels={praiseLabels}
85
+ formatPrice={String}
86
+ countKind="praise"
87
+ onMentions={() => {}}
88
+ />
89
+ );
90
+ const count = screen.getByRole("button", { name: "Praised in 4 reviews" });
91
+ const glyph = count.querySelector("svg");
92
+ expect(glyph).toHaveClass("lucide-thumbs-up", "h-3.5", "w-3.5", "opacity-60");
93
+ expect(glyph).toHaveAttribute("aria-hidden", "true");
94
+ expect(count.querySelector(".lucide-message-square-quote")).toBeNull();
95
+ expect(count.textContent).toBe("4");
96
+ });
97
+ });
98
+
99
+ describe("MentionSignal count glyph", () => {
100
+ it("draws the quote glyph by default and the thumbs-up only for praise", () => {
101
+ const { rerender } = render(<MentionSignal label="Mentioned in 9 reviews" count={9} />);
102
+ expect(screen.getByLabelText("Mentioned in 9 reviews").querySelector("svg")).toHaveClass(
103
+ "lucide-message-square-quote"
104
+ );
105
+
106
+ rerender(<MentionSignal label="Praised in 9 reviews" count={9} countKind="praise" />);
107
+ expect(screen.getByLabelText("Praised in 9 reviews").querySelector("svg")).toHaveClass(
108
+ "lucide-thumbs-up"
109
+ );
110
+ });
111
+ });
@@ -1,5 +1,5 @@
1
1
  import { memo } from "react";
2
- import { MessageSquareQuote, Star } from "lucide-react";
2
+ import { MessageSquareQuote, Star, ThumbsUp } from "lucide-react";
3
3
  import { cn } from "../lib/utils";
4
4
  import { SignatureChip } from "./signature-chip";
5
5
  import type { MenuItemPreview, MenuLinkProps, MenuLinkComponent } from "./menu-types";
@@ -71,6 +71,19 @@ export type MenuItemLabels = {
71
71
  externalRating?: (score: string) => string;
72
72
  };
73
73
 
74
+ /**
75
+ * What the count beside the row counts.
76
+ *
77
+ * - `"mentions"`: every review that named the item, complaints included. Drawn
78
+ * with the quote glyph.
79
+ * - `"praise"`: only the reviews that praised it. Drawn with a thumbs-up.
80
+ *
81
+ * The caller decides, because only the caller knows which number it passed in
82
+ * `item.mentions`. A thumbs-up must never sit on a count that includes
83
+ * complaints, so `"mentions"` is the default.
84
+ */
85
+ export type MenuItemCountKind = "praise" | "mentions";
86
+
74
87
  export type MenuItemProps = {
75
88
  item: MenuItemPreview;
76
89
  labels: MenuItemLabels;
@@ -92,6 +105,12 @@ export type MenuItemProps = {
92
105
  onSignatureEvidence?: () => void;
93
106
  /** Opens the reviews behind the mention count. */
94
107
  onMentions?: () => void;
108
+ /**
109
+ * Whether the count is praise or every mention. Sets the glyph before the
110
+ * numeral: a thumbs-up for `"praise"`, the quote glyph for `"mentions"`.
111
+ * Defaults to `"mentions"`. Keep `labels.mentions` in step with it.
112
+ */
113
+ countKind?: MenuItemCountKind;
95
114
  /**
96
115
  * An outside aggregate score for the item (a wine's Vivino rating, a beer's
97
116
  * Untappd one), shown as a star and the score beside the mention count.
@@ -136,7 +155,9 @@ export type MenuItemProps = {
136
155
  };
137
156
 
138
157
  /**
139
- * The mention count: a filled quote glyph and the numeral, pinned right.
158
+ * The mention count: a glyph and the numeral, pinned right. The glyph is the
159
+ * quote glyph for every mention, or a thumbs-up when the count is praise only
160
+ * (see `MenuItemCountKind`).
140
161
  *
141
162
  * Deliberately NOT a badge — a chip would add a filled shape to a row that has
142
163
  * none, and would read as a status label the venue was given rather than a
@@ -147,18 +168,22 @@ export function MentionSignal({
147
168
  count,
148
169
  onMentions,
149
170
  formatCount = String,
171
+ countKind = "mentions",
150
172
  }: {
151
173
  label: string;
152
174
  count: number;
153
175
  onMentions?: () => void;
154
176
  formatCount?: (value: number) => string;
177
+ countKind?: MenuItemCountKind;
155
178
  }) {
156
179
  /* The glyph and the numeral have to read as ONE object: `gap-0.5` instead of
157
180
  4, and the glyph lighter and a step smaller than the numeral so it reads as
158
- a prefix mark qualifying the value rather than a peer competing with it. */
181
+ a prefix mark qualifying the value rather than a peer competing with it.
182
+ Both glyphs take the same slot, so switching the kind moves nothing. */
183
+ const Glyph = countKind === "praise" ? ThumbsUp : MessageSquareQuote;
159
184
  const content = (
160
185
  <span className="inline-flex items-center gap-0.5">
161
- <MessageSquareQuote className="h-3.5 w-3.5 opacity-60" aria-hidden />
186
+ <Glyph className="h-3.5 w-3.5 opacity-60" aria-hidden />
162
187
  <span className="tabular-nums">{formatCount(count)}</span>
163
188
  </span>
164
189
  );
@@ -233,6 +258,7 @@ export const MenuItem = memo(function MenuItem({
233
258
  signatureTier = "strong",
234
259
  onSignatureEvidence,
235
260
  onMentions,
261
+ countKind = "mentions",
236
262
  externalRating,
237
263
  formatRating = (score) => score.toFixed(1),
238
264
  markGradient,
@@ -455,6 +481,7 @@ export const MenuItem = memo(function MenuItem({
455
481
  count={mentionCount}
456
482
  onMentions={onMentions}
457
483
  formatCount={formatCount}
484
+ countKind={countKind}
458
485
  />
459
486
  )}
460
487
  </div>
@@ -27,7 +27,9 @@ const PopoverContent = React.forwardRef<
27
27
  // than escape.
28
28
  collisionPadding={collisionPadding}
29
29
  className={cn(
30
- "z-50 w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none",
30
+ // Drawers and sheets use layer 1000. A body-portalled popover must
31
+ // clear that layer or its visible actions cannot receive a pointer.
32
+ "z-[1010] w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none",
31
33
  "max-h-[var(--radix-popover-content-available-height)] overflow-y-auto overscroll-contain",
32
34
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
33
35
  className