@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.
- package/dist/{chunk-4WW3HPIW.js → chunk-6Z6MBILC.js} +8 -4
- package/dist/chunk-GIJGVFGC.js +177 -0
- package/dist/chunk-RKNPPYRU.js +86 -0
- package/dist/{chunk-2ADCZXZJ.js → chunk-RXXD3UYP.js} +3 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/combobox.js +1 -1
- package/dist/components/event-detail-panel.d.ts +17 -6
- package/dist/components/event-detail-panel.js +58 -8
- package/dist/components/facet-group.d.ts +9 -2
- package/dist/components/facet-group.js +108 -10
- package/dist/components/filter-option-chip.d.ts +15 -0
- package/dist/components/filter-option-chip.js +3 -0
- package/dist/components/filter-option-group.d.ts +37 -0
- package/dist/components/filter-option-group.js +10 -0
- package/dist/components/filter-panel-footer.d.ts +15 -0
- package/dist/components/filter-panel-footer.js +63 -0
- package/dist/components/menu-item.d.ts +24 -3
- package/dist/components/menu-item.js +1 -1
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/place-card.js +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +1 -1
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/top-ranking-entry.js +1 -1
- package/package.json +11 -8
- package/src/components/event-detail-panel.stories.tsx +16 -0
- package/src/components/event-detail-panel.test.tsx +36 -0
- package/src/components/event-detail-panel.tsx +86 -14
- package/src/components/facet-group.tsx +153 -54
- package/src/components/filter-option-chip.test.tsx +50 -0
- package/src/components/filter-option-chip.tsx +81 -0
- package/src/components/filter-option-group.stories.tsx +250 -0
- package/src/components/filter-option-group.test.tsx +100 -0
- package/src/components/filter-option-group.tsx +231 -0
- package/src/components/filter-panel-footer.tsx +63 -0
- package/src/components/menu-item.stories.tsx +82 -0
- package/src/components/menu-item.test.tsx +55 -1
- package/src/components/menu-item.tsx +31 -4
- 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
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|