@bearmenu/ui 0.8.53 → 0.8.55
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-CBBECMBD.js} +19 -5
- package/dist/{chunk-2ADCZXZJ.js → chunk-RXXD3UYP.js} +3 -1
- 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/menu-item.d.ts +32 -3
- package/dist/components/menu-item.js +1 -1
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/tabs.js +6 -2
- package/dist/components/top-ranking-entry.js +1 -1
- package/package.json +221 -24
- 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/menu-item.stories.tsx +82 -0
- package/src/components/menu-item.test.tsx +77 -1
- package/src/components/menu-item.tsx +48 -5
- package/src/components/popover.tsx +3 -1
- package/src/components/tabs.test.tsx +14 -0
- package/src/components/tabs.tsx +8 -2
|
@@ -2,7 +2,7 @@ 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';
|
|
5
|
-
import { Star, MessageSquareQuote } from 'lucide-react';
|
|
5
|
+
import { Star, ThumbsUp, MessageSquareQuote } from 'lucide-react';
|
|
6
6
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
8
|
function DefaultLink(_a) {
|
|
@@ -13,10 +13,12 @@ function MentionSignal({
|
|
|
13
13
|
label,
|
|
14
14
|
count,
|
|
15
15
|
onMentions,
|
|
16
|
-
formatCount = String
|
|
16
|
+
formatCount = String,
|
|
17
|
+
countKind = "mentions"
|
|
17
18
|
}) {
|
|
19
|
+
const Glyph = countKind === "praise" ? ThumbsUp : MessageSquareQuote;
|
|
18
20
|
const content = /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5", children: [
|
|
19
|
-
/* @__PURE__ */ jsx(
|
|
21
|
+
/* @__PURE__ */ jsx(Glyph, { className: "h-3.5 w-3.5 opacity-60", "aria-hidden": true }),
|
|
20
22
|
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatCount(count) })
|
|
21
23
|
] });
|
|
22
24
|
const shared = cn(
|
|
@@ -75,6 +77,8 @@ var MenuItem = memo(function MenuItem2({
|
|
|
75
77
|
signatureTier = "strong",
|
|
76
78
|
onSignatureEvidence,
|
|
77
79
|
onMentions,
|
|
80
|
+
countKind = "mentions",
|
|
81
|
+
emphasis = "price",
|
|
78
82
|
externalRating,
|
|
79
83
|
formatRating = (score) => score.toFixed(1),
|
|
80
84
|
markGradient,
|
|
@@ -203,7 +207,16 @@ var MenuItem = memo(function MenuItem2({
|
|
|
203
207
|
(minPrice != null || hasQuantity || mentionCount > 0 || ratingText != null) && /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex items-baseline justify-between gap-x-2 gap-y-1", children: [
|
|
204
208
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-baseline gap-x-2", children: [
|
|
205
209
|
minPrice != null && /* @__PURE__ */ jsxs("span", { className: "flex items-baseline gap-1", children: [
|
|
206
|
-
/* @__PURE__ */ jsx(
|
|
210
|
+
/* @__PURE__ */ jsx(
|
|
211
|
+
"span",
|
|
212
|
+
{
|
|
213
|
+
className: cn(
|
|
214
|
+
"tabular-nums",
|
|
215
|
+
emphasis === "praise" ? "text-[13px] font-medium text-muted-foreground" : "text-[15px] font-bold text-foreground"
|
|
216
|
+
),
|
|
217
|
+
children: isPriceRange ? `${formatPrice(minPrice)}\u2013${formatPrice(maxPrice)}` : formatPrice(minPrice)
|
|
218
|
+
}
|
|
219
|
+
),
|
|
207
220
|
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: labels.currency })
|
|
208
221
|
] }),
|
|
209
222
|
hasQuantity && /* @__PURE__ */ jsxs("span", { className: "text-[12px] tabular-nums text-muted-foreground", children: [
|
|
@@ -225,7 +238,8 @@ var MenuItem = memo(function MenuItem2({
|
|
|
225
238
|
label: labels.mentions(mentionCount),
|
|
226
239
|
count: mentionCount,
|
|
227
240
|
onMentions,
|
|
228
|
-
formatCount
|
|
241
|
+
formatCount,
|
|
242
|
+
countKind
|
|
229
243
|
}
|
|
230
244
|
)
|
|
231
245
|
] })
|
|
@@ -19,7 +19,9 @@ var PopoverContent = React.forwardRef((_a, ref) => {
|
|
|
19
19
|
sideOffset,
|
|
20
20
|
collisionPadding,
|
|
21
21
|
className: cn(
|
|
22
|
-
|
|
22
|
+
// Drawers and sheets use layer 1000. A body-portalled popover must
|
|
23
|
+
// clear that layer or its visible actions cannot receive a pointer.
|
|
24
|
+
"z-[1010] w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none",
|
|
23
25
|
"max-h-[var(--radix-popover-content-available-height)] overflow-y-auto overscroll-contain",
|
|
24
26
|
"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",
|
|
25
27
|
className
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-
|
|
1
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-RXXD3UYP.js';
|
|
2
2
|
import { Command, CommandInput, CommandList, CommandLoading, CommandEmpty, CommandGroup, CommandItem } from '../chunk-PGQXVJXW.js';
|
|
3
3
|
import '../chunk-IQIJCMG2.js';
|
|
4
4
|
import { Button } from '../chunk-GHKTVHQG.js';
|
|
@@ -111,8 +111,19 @@ type EventDetailPanelProps = {
|
|
|
111
111
|
/** The hype tier, rendered as given (the app's own badge) in the chip row. */
|
|
112
112
|
hype?: React.ReactNode;
|
|
113
113
|
venue?: EventDetailVenue;
|
|
114
|
-
/** Activity tags
|
|
115
|
-
tags?: string
|
|
114
|
+
/** Activity tags. Pass a destination to make a tag a browse action. */
|
|
115
|
+
tags?: (string | {
|
|
116
|
+
label: string;
|
|
117
|
+
href: string;
|
|
118
|
+
onClick?: () => void;
|
|
119
|
+
})[];
|
|
120
|
+
/** Entry information opened by the price/free-entry control. */
|
|
121
|
+
entryDetails?: {
|
|
122
|
+
label: string;
|
|
123
|
+
description: string;
|
|
124
|
+
action?: React.ReactNode;
|
|
125
|
+
onOpenChange?: (open: boolean) => void;
|
|
126
|
+
};
|
|
116
127
|
/** Performer names, in the order the API gave them. */
|
|
117
128
|
performers?: string[];
|
|
118
129
|
/** How many performers to show before the "show all" chip. Default 5. */
|
|
@@ -137,7 +148,7 @@ type EventDetailPanelProps = {
|
|
|
137
148
|
/** Rendered instead of the poster when it's absent. Defaults to `<EventPosterFallback />`. */
|
|
138
149
|
fallback?: React.ReactNode;
|
|
139
150
|
};
|
|
140
|
-
declare function EventDetailPanel({ event, labels, formatters, dateLine, dateTime, status, soonLabel, hype, venue, tags, performers, performersPreview, showAllPerformersLabel, headerActions, footer, externalLink, heroHeight, size, linkComponent: LinkComponent, className, fallback, }: EventDetailPanelProps): react_jsx_runtime.JSX.Element;
|
|
151
|
+
declare function EventDetailPanel({ event, labels, formatters, dateLine, dateTime, status, soonLabel, hype, venue, tags, entryDetails, performers, performersPreview, showAllPerformersLabel, headerActions, footer, externalLink, heroHeight, size, linkComponent: LinkComponent, className, fallback, }: EventDetailPanelProps): react_jsx_runtime.JSX.Element;
|
|
141
152
|
type EventDetailFooterProps = {
|
|
142
153
|
/**
|
|
143
154
|
* The one primary action — a `Button` (reserve, tickets, directions when
|
|
@@ -205,17 +216,17 @@ type EventDetailPanelSkeletonProps = {
|
|
|
205
216
|
};
|
|
206
217
|
/** Mirrors `<EventDetailPanel>`'s geometry: capped hero, two title lines, when-line, chips, venue card, line-up, about, ghost row and primary. */
|
|
207
218
|
declare function EventDetailPanelSkeleton({ size, heroHeight, headerActions, label, className, }: EventDetailPanelSkeletonProps): react_jsx_runtime.JSX.Element;
|
|
208
|
-
type EventDetailChipRowProps = Pick<EventDetailPanelProps, "event" | "formatters" | "status" | "soonLabel" | "hype" | "tags"> & {
|
|
219
|
+
type EventDetailChipRowProps = Pick<EventDetailPanelProps, "event" | "formatters" | "status" | "soonLabel" | "hype" | "tags" | "entryDetails" | "linkComponent"> & {
|
|
209
220
|
/** Only the chip strings — a page rendering the row alone need not name the section headings. */
|
|
210
221
|
labels: Pick<EventDetailPanelLabels, "free" | "liveNow" | "ended">;
|
|
211
222
|
className?: string;
|
|
212
223
|
};
|
|
213
224
|
/**
|
|
214
|
-
* One wrapping row
|
|
225
|
+
* One wrapping row: metadata, entry information, then activity links. Exported
|
|
215
226
|
* so the page renders the same row above its title — it is the panel's row,
|
|
216
227
|
* not a second one.
|
|
217
228
|
*/
|
|
218
|
-
declare function EventDetailChipRow({ event, labels, formatters, status, soonLabel, hype, tags, className, }: EventDetailChipRowProps): react_jsx_runtime.JSX.Element;
|
|
229
|
+
declare function EventDetailChipRow({ event, labels, formatters, status, soonLabel, hype, tags, entryDetails, linkComponent: LinkComponent, className, }: EventDetailChipRowProps): react_jsx_runtime.JSX.Element;
|
|
219
230
|
type EventDetailVenueCardProps = {
|
|
220
231
|
venue: EventDetailVenue;
|
|
221
232
|
labels: Pick<EventDetailPanelLabels, "venueNoPage" | "grade">;
|
|
@@ -4,13 +4,14 @@ import { resolveEventEntryFee } from '../chunk-DNSXMHYX.js';
|
|
|
4
4
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
5
5
|
import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
|
|
6
6
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
7
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-RXXD3UYP.js';
|
|
7
8
|
import { Button } from '../chunk-GHKTVHQG.js';
|
|
8
9
|
import '../chunk-RIH2IS7X.js';
|
|
9
10
|
import '../chunk-5YNIQ2BL.js';
|
|
10
11
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
11
12
|
import { __spreadProps, __spreadValues, __objRest } from '../chunk-MMUBH76A.js';
|
|
12
13
|
import * as React from 'react';
|
|
13
|
-
import { Calendar, User, CalendarX, Unplug, RefreshCw,
|
|
14
|
+
import { Calendar, User, CalendarX, Unplug, RefreshCw, Gift, Ticket, Timer, ChevronRight, MapPin, Star } from 'lucide-react';
|
|
14
15
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
15
16
|
|
|
16
17
|
function DefaultLink(_a) {
|
|
@@ -34,6 +35,7 @@ function EventDetailPanel({
|
|
|
34
35
|
hype,
|
|
35
36
|
venue,
|
|
36
37
|
tags,
|
|
38
|
+
entryDetails,
|
|
37
39
|
performers,
|
|
38
40
|
performersPreview = 5,
|
|
39
41
|
showAllPerformersLabel,
|
|
@@ -122,7 +124,9 @@ function EventDetailPanel({
|
|
|
122
124
|
status,
|
|
123
125
|
soonLabel,
|
|
124
126
|
hype,
|
|
125
|
-
tags
|
|
127
|
+
tags,
|
|
128
|
+
entryDetails,
|
|
129
|
+
linkComponent: LinkComponent
|
|
126
130
|
}
|
|
127
131
|
),
|
|
128
132
|
venue && /* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -396,10 +400,14 @@ function EventDetailChipRow({
|
|
|
396
400
|
soonLabel,
|
|
397
401
|
hype,
|
|
398
402
|
tags,
|
|
403
|
+
entryDetails,
|
|
404
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
399
405
|
className
|
|
400
406
|
}) {
|
|
401
407
|
const fee = resolveEventEntryFee(event);
|
|
402
|
-
|
|
408
|
+
const priceLabel = fee.kind === "free" ? labels.free : fee.kind === "paid" ? formatters.price(fee.amountRon) : null;
|
|
409
|
+
const priceIcon = fee.kind === "free" ? /* @__PURE__ */ jsx(Gift, { className: "h-3.5 w-3.5", "aria-hidden": true }) : /* @__PURE__ */ jsx(Ticket, { className: "h-3.5 w-3.5", "aria-hidden": true });
|
|
410
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-wrap items-center gap-[7px]", className), children: [
|
|
403
411
|
status === "live" && /* @__PURE__ */ jsxs(
|
|
404
412
|
"span",
|
|
405
413
|
{
|
|
@@ -436,7 +444,35 @@ function EventDetailChipRow({
|
|
|
436
444
|
children: labels.ended
|
|
437
445
|
}
|
|
438
446
|
),
|
|
439
|
-
|
|
447
|
+
priceLabel && entryDetails ? /* @__PURE__ */ jsxs(Popover, { onOpenChange: entryDetails.onOpenChange, children: [
|
|
448
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
449
|
+
Button,
|
|
450
|
+
{
|
|
451
|
+
variant: "outline",
|
|
452
|
+
size: "sm",
|
|
453
|
+
className: "min-h-11 rounded-full",
|
|
454
|
+
"aria-label": entryDetails.label,
|
|
455
|
+
children: [
|
|
456
|
+
priceIcon,
|
|
457
|
+
priceLabel,
|
|
458
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": true })
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
) }),
|
|
462
|
+
/* @__PURE__ */ jsxs(
|
|
463
|
+
PopoverContent,
|
|
464
|
+
{
|
|
465
|
+
align: "start",
|
|
466
|
+
"aria-label": entryDetails.label,
|
|
467
|
+
className: "flex max-w-[calc(100vw-2rem)] flex-col gap-3",
|
|
468
|
+
children: [
|
|
469
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: priceLabel }),
|
|
470
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: entryDetails.description }),
|
|
471
|
+
entryDetails.action
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
] }) : fee.kind === "free" ? /* @__PURE__ */ jsxs(
|
|
440
476
|
"span",
|
|
441
477
|
{
|
|
442
478
|
className: cn(
|
|
@@ -448,13 +484,27 @@ function EventDetailChipRow({
|
|
|
448
484
|
labels.free
|
|
449
485
|
]
|
|
450
486
|
}
|
|
451
|
-
),
|
|
452
|
-
fee.kind === "paid" && /* @__PURE__ */ jsxs("span", { className: cn(CHIP, "text-foreground"), children: [
|
|
487
|
+
) : fee.kind === "paid" ? /* @__PURE__ */ jsxs("span", { className: cn(CHIP, "text-foreground"), children: [
|
|
453
488
|
/* @__PURE__ */ jsx(Ticket, { className: "h-3 w-3", "aria-hidden": true }),
|
|
454
489
|
formatters.price(fee.amountRon)
|
|
455
|
-
] }),
|
|
490
|
+
] }) : null,
|
|
456
491
|
hype,
|
|
457
|
-
tags == null ? void 0 : tags.map(
|
|
492
|
+
tags == null ? void 0 : tags.map(
|
|
493
|
+
(tag) => typeof tag === "string" ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: tag }, tag) : /* @__PURE__ */ jsx(
|
|
494
|
+
Button,
|
|
495
|
+
{
|
|
496
|
+
asChild: true,
|
|
497
|
+
variant: "outline",
|
|
498
|
+
size: "sm",
|
|
499
|
+
className: "min-h-11 rounded-full",
|
|
500
|
+
children: /* @__PURE__ */ jsxs(LinkComponent, { href: tag.href, onClick: tag.onClick, children: [
|
|
501
|
+
tag.label,
|
|
502
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": true })
|
|
503
|
+
] })
|
|
504
|
+
},
|
|
505
|
+
tag.href
|
|
506
|
+
)
|
|
507
|
+
)
|
|
458
508
|
] });
|
|
459
509
|
}
|
|
460
510
|
function EventDetailVenueCard({
|
|
@@ -64,6 +64,18 @@ type MenuItemLabels = {
|
|
|
64
64
|
*/
|
|
65
65
|
externalRating?: (score: string) => string;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* What the count beside the row counts.
|
|
69
|
+
*
|
|
70
|
+
* - `"mentions"`: every review that named the item, complaints included. Drawn
|
|
71
|
+
* with the quote glyph.
|
|
72
|
+
* - `"praise"`: only the reviews that praised it. Drawn with a thumbs-up.
|
|
73
|
+
*
|
|
74
|
+
* The caller decides, because only the caller knows which number it passed in
|
|
75
|
+
* `item.mentions`. A thumbs-up must never sit on a count that includes
|
|
76
|
+
* complaints, so `"mentions"` is the default.
|
|
77
|
+
*/
|
|
78
|
+
type MenuItemCountKind = "praise" | "mentions";
|
|
67
79
|
type MenuItemProps = {
|
|
68
80
|
item: MenuItemPreview;
|
|
69
81
|
labels: MenuItemLabels;
|
|
@@ -85,6 +97,20 @@ type MenuItemProps = {
|
|
|
85
97
|
onSignatureEvidence?: () => void;
|
|
86
98
|
/** Opens the reviews behind the mention count. */
|
|
87
99
|
onMentions?: () => void;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the count is praise or every mention. Sets the glyph before the
|
|
102
|
+
* numeral: a thumbs-up for `"praise"`, the quote glyph for `"mentions"`.
|
|
103
|
+
* Defaults to `"mentions"`. Keep `labels.mentions` in step with it.
|
|
104
|
+
*/
|
|
105
|
+
countKind?: MenuItemCountKind;
|
|
106
|
+
/**
|
|
107
|
+
* What the meta row leads with. `"price"` (default): the price is bold, the
|
|
108
|
+
* count's peer at the other end. `"praise"`: on surfaces where reviews'
|
|
109
|
+
* praise is the point (the map's lists), the price steps down to secondary
|
|
110
|
+
* text, a fact to check rather than the row's headline, and the praise count
|
|
111
|
+
* keeps the bold accent.
|
|
112
|
+
*/
|
|
113
|
+
emphasis?: "price" | "praise";
|
|
88
114
|
/**
|
|
89
115
|
* An outside aggregate score for the item (a wine's Vivino rating, a beer's
|
|
90
116
|
* Untappd one), shown as a star and the score beside the mention count.
|
|
@@ -128,17 +154,20 @@ type MenuItemProps = {
|
|
|
128
154
|
className?: string;
|
|
129
155
|
};
|
|
130
156
|
/**
|
|
131
|
-
* The mention count: a
|
|
157
|
+
* The mention count: a glyph and the numeral, pinned right. The glyph is the
|
|
158
|
+
* quote glyph for every mention, or a thumbs-up when the count is praise only
|
|
159
|
+
* (see `MenuItemCountKind`).
|
|
132
160
|
*
|
|
133
161
|
* Deliberately NOT a badge — a chip would add a filled shape to a row that has
|
|
134
162
|
* none, and would read as a status label the venue was given rather than a
|
|
135
163
|
* count drawn from the same reviews.
|
|
136
164
|
*/
|
|
137
|
-
declare function MentionSignal({ label, count, onMentions, formatCount, }: {
|
|
165
|
+
declare function MentionSignal({ label, count, onMentions, formatCount, countKind, }: {
|
|
138
166
|
label: string;
|
|
139
167
|
count: number;
|
|
140
168
|
onMentions?: () => void;
|
|
141
169
|
formatCount?: (value: number) => string;
|
|
170
|
+
countKind?: MenuItemCountKind;
|
|
142
171
|
}): react_jsx_runtime.JSX.Element;
|
|
143
172
|
/**
|
|
144
173
|
* An external aggregate score: a star and the numeral, in the neutral muted
|
|
@@ -152,4 +181,4 @@ declare function ExternalRatingSignal({ label, value }: {
|
|
|
152
181
|
}): react_jsx_runtime.JSX.Element;
|
|
153
182
|
declare const MenuItem: React.NamedExoticComponent<MenuItemProps>;
|
|
154
183
|
|
|
155
|
-
export { ExternalRatingSignal, MentionSignal, MenuItem, type MenuItemLabels, type MenuItemLinkProps, type MenuItemProps };
|
|
184
|
+
export { ExternalRatingSignal, MentionSignal, MenuItem, type MenuItemCountKind, type MenuItemLabels, type MenuItemLinkProps, type MenuItemProps };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ExternalRatingSignal, MentionSignal, MenuItem } from '../chunk-
|
|
1
|
+
export { ExternalRatingSignal, MentionSignal, MenuItem } from '../chunk-CBBECMBD.js';
|
|
2
2
|
import '../chunk-6PQINSAA.js';
|
|
3
3
|
import '../chunk-RYPPN2GZ.js';
|
|
4
4
|
import '../chunk-FEK5XGZW.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Checkbox } from '../chunk-L3CDXVEP.js';
|
|
2
|
-
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-
|
|
2
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-RXXD3UYP.js';
|
|
3
3
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-PGQXVJXW.js';
|
|
4
4
|
import '../chunk-IQIJCMG2.js';
|
|
5
5
|
import { Button } from '../chunk-GHKTVHQG.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { Popover, PopoverAnchor, PopoverArrow, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from '../chunk-
|
|
1
|
+
export { Popover, PopoverAnchor, PopoverArrow, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from '../chunk-RXXD3UYP.js';
|
|
2
2
|
import '../chunk-FEK5XGZW.js';
|
|
3
3
|
import '../chunk-MMUBH76A.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-
|
|
1
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-RXXD3UYP.js';
|
|
2
2
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-PGQXVJXW.js';
|
|
3
3
|
import '../chunk-IQIJCMG2.js';
|
|
4
4
|
import { Button } from '../chunk-GHKTVHQG.js';
|
package/dist/components/tabs.js
CHANGED
|
@@ -93,7 +93,9 @@ var TabsTrigger = React.forwardRef((_a, ref) => {
|
|
|
93
93
|
transition: archetype.indicatorMorph,
|
|
94
94
|
className: cn(
|
|
95
95
|
"absolute inset-0 -z-0 motion-reduce:hidden",
|
|
96
|
-
|
|
96
|
+
// Dark: `bg-background` is darker than the `bg-muted` track, so the
|
|
97
|
+
// selection read as a hole; it lifts to the raised surface instead.
|
|
98
|
+
variant === "pill" && "rounded-xl bg-background shadow-sm dark:bg-surface-2 dark:ring-1 dark:ring-border",
|
|
97
99
|
variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
|
|
98
100
|
variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
|
|
99
101
|
)
|
|
@@ -105,7 +107,9 @@ var TabsTrigger = React.forwardRef((_a, ref) => {
|
|
|
105
107
|
"aria-hidden": true,
|
|
106
108
|
className: cn(
|
|
107
109
|
"absolute inset-0 -z-0 hidden motion-reduce:block",
|
|
108
|
-
|
|
110
|
+
// Dark: `bg-background` is darker than the `bg-muted` track, so the
|
|
111
|
+
// selection read as a hole; it lifts to the raised surface instead.
|
|
112
|
+
variant === "pill" && "rounded-xl bg-background shadow-sm dark:bg-surface-2 dark:ring-1 dark:ring-border",
|
|
109
113
|
variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
|
|
110
114
|
variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
|
|
111
115
|
)
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bearmenu/ui",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.8.55",
|
|
4
|
+
"packageManager": "pnpm@10.28.1",
|
|
5
|
+
"description": "BearMenu design system \u2014 shared UI tokens, primitives, and components",
|
|
5
6
|
"license": "UNLICENSED",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -16,20 +17,226 @@
|
|
|
16
17
|
"src"
|
|
17
18
|
],
|
|
18
19
|
"exports": {
|
|
20
|
+
"./components/accordion": "./src/components/accordion.tsx",
|
|
21
|
+
"./components/action-bar": "./src/components/action-bar.tsx",
|
|
22
|
+
"./components/activity-row": "./src/components/activity-row.tsx",
|
|
23
|
+
"./components/alert": "./src/components/alert.tsx",
|
|
24
|
+
"./components/alert-dialog": "./src/components/alert-dialog.tsx",
|
|
25
|
+
"./components/aspect-ratio": "./src/components/aspect-ratio.tsx",
|
|
26
|
+
"./components/avatar": "./src/components/avatar.tsx",
|
|
27
|
+
"./components/badge": "./src/components/badge.tsx",
|
|
28
|
+
"./components/banner": "./src/components/banner.tsx",
|
|
29
|
+
"./components/booking-row": "./src/components/booking-row.tsx",
|
|
30
|
+
"./components/booking-summary-card": "./src/components/booking-summary-card.tsx",
|
|
31
|
+
"./components/breadcrumb": "./src/components/breadcrumb.tsx",
|
|
32
|
+
"./components/button": "./src/components/button.tsx",
|
|
33
|
+
"./components/button-group": "./src/components/button-group.tsx",
|
|
34
|
+
"./components/calendar": "./src/components/calendar.tsx",
|
|
35
|
+
"./components/card": "./src/components/card.tsx",
|
|
36
|
+
"./components/carousel": "./src/components/carousel.tsx",
|
|
37
|
+
"./components/category-deck": "./src/components/category-deck.tsx",
|
|
38
|
+
"./components/checkbox": "./src/components/checkbox.tsx",
|
|
39
|
+
"./components/circular-progress": "./src/components/circular-progress.tsx",
|
|
40
|
+
"./components/coach-marks": "./src/components/coach-marks.tsx",
|
|
41
|
+
"./components/collapsible": "./src/components/collapsible.tsx",
|
|
42
|
+
"./components/combobox": "./src/components/combobox.tsx",
|
|
43
|
+
"./components/command": "./src/components/command.tsx",
|
|
44
|
+
"./components/concept-card": "./src/components/concept-card.tsx",
|
|
45
|
+
"./components/concept-compare": "./src/components/concept-compare.tsx",
|
|
46
|
+
"./components/concept-compare-rail": "./src/components/concept-compare-rail.tsx",
|
|
47
|
+
"./components/concept-convergence": "./src/components/concept-convergence.tsx",
|
|
48
|
+
"./components/convergence": "./src/components/convergence.tsx",
|
|
49
|
+
"./components/coverflow": "./src/components/coverflow.tsx",
|
|
50
|
+
"./components/currency-input": "./src/components/currency-input.tsx",
|
|
51
|
+
"./components/date-input": "./src/components/date-input.tsx",
|
|
52
|
+
"./components/day-rail-chip": "./src/components/day-rail-chip.tsx",
|
|
53
|
+
"./components/description-list": "./src/components/description-list.tsx",
|
|
54
|
+
"./components/dialog": "./src/components/dialog.tsx",
|
|
55
|
+
"./components/doorway-tiles": "./src/components/doorway-tiles.tsx",
|
|
56
|
+
"./components/drawer": "./src/components/drawer.tsx",
|
|
57
|
+
"./components/drift-rail": "./src/components/drift-rail.tsx",
|
|
58
|
+
"./components/dropdown-menu": "./src/components/dropdown-menu.tsx",
|
|
59
|
+
"./components/empty-state": "./src/components/empty-state.tsx",
|
|
60
|
+
"./components/event-card": "./src/components/event-card.tsx",
|
|
61
|
+
"./components/event-compact-row": "./src/components/event-compact-row.tsx",
|
|
62
|
+
"./components/event-day-header": "./src/components/event-day-header.tsx",
|
|
63
|
+
"./components/event-day-separator": "./src/components/event-day-separator.tsx",
|
|
64
|
+
"./components/event-detail-panel": "./src/components/event-detail-panel.tsx",
|
|
65
|
+
"./components/event-hub-hero": "./src/components/event-hub-hero.tsx",
|
|
66
|
+
"./components/event-index-row": "./src/components/event-index-row.tsx",
|
|
67
|
+
"./components/event-list-row": "./src/components/event-list-row.tsx",
|
|
68
|
+
"./components/event-list-row-wide": "./src/components/event-list-row-wide.tsx",
|
|
69
|
+
"./components/event-poster-fallback": "./src/components/event-poster-fallback.tsx",
|
|
70
|
+
"./components/event-tonight-strip": "./src/components/event-tonight-strip.tsx",
|
|
71
|
+
"./components/event-types": "./src/components/event-types.ts",
|
|
72
|
+
"./components/events-empty-state": "./src/components/events-empty-state.tsx",
|
|
73
|
+
"./components/explainer-steps": "./src/components/explainer-steps.tsx",
|
|
74
|
+
"./components/facet-group": "./src/components/facet-group.tsx",
|
|
75
|
+
"./components/featured-rail-item": "./src/components/featured-rail-item.tsx",
|
|
76
|
+
"./components/field": "./src/components/field.tsx",
|
|
77
|
+
"./components/filter-category-row": "./src/components/filter-category-row.tsx",
|
|
78
|
+
"./components/form": "./src/components/form.tsx",
|
|
79
|
+
"./components/glass-ground": "./src/components/glass-ground.tsx",
|
|
80
|
+
"./components/grade-atom": "./src/components/grade-atom.tsx",
|
|
81
|
+
"./components/haptic-switch": "./src/components/haptic-switch.tsx",
|
|
82
|
+
"./components/hold-countdown": "./src/components/hold-countdown.tsx",
|
|
83
|
+
"./components/hub-surface": "./src/components/hub-surface.tsx",
|
|
84
|
+
"./components/hue-chip": "./src/components/hue-chip.tsx",
|
|
85
|
+
"./components/icon": "./src/components/icon.tsx",
|
|
86
|
+
"./components/input": "./src/components/input.tsx",
|
|
87
|
+
"./components/input-group": "./src/components/input-group.tsx",
|
|
88
|
+
"./components/input-otp": "./src/components/input-otp.tsx",
|
|
89
|
+
"./components/item": "./src/components/item.tsx",
|
|
90
|
+
"./components/kbd": "./src/components/kbd.tsx",
|
|
91
|
+
"./components/label": "./src/components/label.tsx",
|
|
92
|
+
"./components/kitchen-dossier": "./src/components/kitchen-dossier.tsx",
|
|
93
|
+
"./components/lazy-markdown": "./src/components/lazy-markdown.tsx",
|
|
94
|
+
"./components/link-button": "./src/components/link-button.tsx",
|
|
95
|
+
"./components/markdown-renderer": "./src/components/markdown-renderer.tsx",
|
|
96
|
+
"./components/menu-dead-end": "./src/components/menu-dead-end.tsx",
|
|
97
|
+
"./components/menu-family-block": "./src/components/menu-family-block.tsx",
|
|
98
|
+
"./components/menu-family-card": "./src/components/menu-family-card.tsx",
|
|
99
|
+
"./components/menu-field": "./src/components/menu-field.tsx",
|
|
100
|
+
"./components/menu-filter-chips": "./src/components/menu-filter-chips.tsx",
|
|
101
|
+
"./components/menu-group-rows": "./src/components/menu-group-rows.tsx",
|
|
102
|
+
"./components/menu-hub-hero": "./src/components/menu-hub-hero.tsx",
|
|
103
|
+
"./components/menu-index-header": "./src/components/menu-index-header.tsx",
|
|
104
|
+
"./components/menu-index-list": "./src/components/menu-index-list.tsx",
|
|
105
|
+
"./components/menu-intent-hero": "./src/components/menu-intent-hero.tsx",
|
|
106
|
+
"./components/menu-item": "./src/components/menu-item.tsx",
|
|
107
|
+
"./components/menu-pick-row": "./src/components/menu-pick-row.tsx",
|
|
108
|
+
"./components/menu-kitchen-list": "./src/components/menu-kitchen-list.tsx",
|
|
109
|
+
"./components/menu-mocks": "./src/components/menu-mocks.ts",
|
|
110
|
+
"./components/menu-types": "./src/components/menu-types.ts",
|
|
111
|
+
"./components/motion-icon": "./src/components/motion-icon.tsx",
|
|
112
|
+
"./components/motion-icon-actions": "./src/components/motion-icon-actions.tsx",
|
|
113
|
+
"./components/motion-icon-controls": "./src/components/motion-icon-controls.tsx",
|
|
114
|
+
"./components/motion-icon-set": "./src/components/motion-icon-set.tsx",
|
|
115
|
+
"./components/multi-select-combobox": "./src/components/multi-select-combobox.tsx",
|
|
116
|
+
"./components/multi-select-menu": "./src/components/multi-select-menu.tsx",
|
|
117
|
+
"./components/native-select": "./src/components/native-select.tsx",
|
|
118
|
+
"./components/open-now-list": "./src/components/open-now-list.tsx",
|
|
119
|
+
"./components/pagination": "./src/components/pagination.tsx",
|
|
120
|
+
"./components/pan-zoom-viewport": "./src/components/pan-zoom-viewport.tsx",
|
|
121
|
+
"./components/party-counter": "./src/components/party-counter.tsx",
|
|
122
|
+
"./components/phone-frame": "./src/components/phone-frame.tsx",
|
|
123
|
+
"./components/photo-ground": "./src/components/photo-ground.tsx",
|
|
124
|
+
"./components/place-about-tab": "./src/components/place-about-tab.tsx",
|
|
125
|
+
"./components/place-card": "./src/components/place-card.tsx",
|
|
126
|
+
"./components/place-details-mobile": "./src/components/place-details-mobile.tsx",
|
|
127
|
+
"./components/place-schedule-week": "./src/components/place-schedule-week.tsx",
|
|
128
|
+
"./components/place-types": "./src/components/place-types.ts",
|
|
129
|
+
"./components/popover": "./src/components/popover.tsx",
|
|
130
|
+
"./components/poster-stack": "./src/components/poster-stack.tsx",
|
|
131
|
+
"./components/poster-tape": "./src/components/poster-tape.tsx",
|
|
132
|
+
"./components/poster-wall": "./src/components/poster-wall.tsx",
|
|
133
|
+
"./components/progress": "./src/components/progress.tsx",
|
|
134
|
+
"./components/qa-block": "./src/components/qa-block.tsx",
|
|
135
|
+
"./components/qr-panel": "./src/components/qr-panel.tsx",
|
|
136
|
+
"./components/quiet-stretch": "./src/components/quiet-stretch.tsx",
|
|
137
|
+
"./components/radio-group": "./src/components/radio-group.tsx",
|
|
138
|
+
"./components/reconcile-bar": "./src/components/reconcile-bar.tsx",
|
|
139
|
+
"./components/responsive-overlay": "./src/components/responsive-overlay.tsx",
|
|
140
|
+
"./components/rating": "./src/components/rating.tsx",
|
|
141
|
+
"./components/scroll-area": "./src/components/scroll-area.tsx",
|
|
142
|
+
"./components/scroll-fade": "./src/components/scroll-fade.tsx",
|
|
143
|
+
"./components/search-bar": "./src/components/search-bar.tsx",
|
|
144
|
+
"./components/searchable-select": "./src/components/searchable-select.tsx",
|
|
145
|
+
"./components/section-header": "./src/components/section-header.tsx",
|
|
146
|
+
"./components/segmented-nav": "./src/components/segmented-nav.tsx",
|
|
147
|
+
"./components/select": "./src/components/select.tsx",
|
|
148
|
+
"./components/selection-bar": "./src/components/selection-bar.tsx",
|
|
149
|
+
"./components/separator": "./src/components/separator.tsx",
|
|
150
|
+
"./components/sheet": "./src/components/sheet.tsx",
|
|
151
|
+
"./components/signature-chip": "./src/components/signature-chip.tsx",
|
|
152
|
+
"./components/skeleton": "./src/components/skeleton.tsx",
|
|
153
|
+
"./components/skeleton-card": "./src/components/skeleton-card.tsx",
|
|
154
|
+
"./components/slider": "./src/components/slider.tsx",
|
|
155
|
+
"./components/sliding-panels": "./src/components/sliding-panels.tsx",
|
|
156
|
+
"./components/sonner": "./src/components/sonner.tsx",
|
|
157
|
+
"./components/sort-option-list": "./src/components/sort-option-list.tsx",
|
|
158
|
+
"./components/spinner": "./src/components/spinner.tsx",
|
|
159
|
+
"./components/split-share-row": "./src/components/split-share-row.tsx",
|
|
160
|
+
"./components/stat-banner": "./src/components/stat-banner.tsx",
|
|
161
|
+
"./components/stepper": "./src/components/stepper.tsx",
|
|
162
|
+
"./components/sticky-container": "./src/components/sticky-container.tsx",
|
|
163
|
+
"./components/story": "./src/components/story.tsx",
|
|
164
|
+
"./components/switch": "./src/components/switch.tsx",
|
|
165
|
+
"./components/table": "./src/components/table.tsx",
|
|
166
|
+
"./components/table-ledger-card": "./src/components/table-ledger-card.tsx",
|
|
167
|
+
"./components/table-trigger-band": "./src/components/table-trigger-band.tsx",
|
|
168
|
+
"./components/tabs": "./src/components/tabs.tsx",
|
|
169
|
+
"./components/tag": "./src/components/tag.tsx",
|
|
170
|
+
"./components/tag-group": "./src/components/tag-group.tsx",
|
|
171
|
+
"./components/text": "./src/components/text.tsx",
|
|
172
|
+
"./components/textarea": "./src/components/textarea.tsx",
|
|
173
|
+
"./components/time-slot": "./src/components/time-slot.tsx",
|
|
174
|
+
"./components/timeline-day": "./src/components/timeline-day.tsx",
|
|
175
|
+
"./components/timeline-hour": "./src/components/timeline-hour.tsx",
|
|
176
|
+
"./components/timeline-hour-puck": "./src/components/timeline-hour-puck.tsx",
|
|
177
|
+
"./components/toggle": "./src/components/toggle.tsx",
|
|
178
|
+
"./components/toggle-group": "./src/components/toggle-group.tsx",
|
|
179
|
+
"./components/tooltip": "./src/components/tooltip.tsx",
|
|
180
|
+
"./components/top-comparison": "./src/components/top-comparison.tsx",
|
|
181
|
+
"./components/top-cross-link": "./src/components/top-cross-link.tsx",
|
|
182
|
+
"./components/top-ranking-entry": "./src/components/top-ranking-entry.tsx",
|
|
183
|
+
"./components/top-ranking-hero": "./src/components/top-ranking-hero.tsx",
|
|
184
|
+
"./components/top-ranking-legend": "./src/components/top-ranking-legend.tsx",
|
|
185
|
+
"./components/top-types": "./src/components/top-types.ts",
|
|
186
|
+
"./components/use-mobile": "./src/components/use-mobile.tsx",
|
|
187
|
+
"./components/venue-events-block": "./src/components/venue-events-block.tsx",
|
|
188
|
+
"./components/venue-grade-dimensions": "./src/components/venue-grade-dimensions.tsx",
|
|
189
|
+
"./components/venue-quotes": "./src/components/venue-quotes.tsx",
|
|
190
|
+
"./components/venue-wall": "./src/components/venue-wall.tsx",
|
|
191
|
+
"./components/weekday-chip": "./src/components/weekday-chip.tsx",
|
|
192
|
+
"./components/weekday-theme-banner": "./src/components/weekday-theme-banner.tsx",
|
|
193
|
+
"./components/weekend-panel": "./src/components/weekend-panel.tsx",
|
|
19
194
|
"./globals.css": "./src/globals.css",
|
|
20
|
-
"./
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"./hooks
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"./lib
|
|
29
|
-
|
|
30
|
-
|
|
195
|
+
"./hooks/use-drag-swipe": "./src/hooks/use-drag-swipe.ts",
|
|
196
|
+
"./hooks/use-haptic": "./src/hooks/use-haptic.ts",
|
|
197
|
+
"./hooks/use-media-query": "./src/hooks/use-media-query.ts",
|
|
198
|
+
"./hooks/use-mobile": "./src/hooks/use-mobile.ts",
|
|
199
|
+
"./hooks/use-mounted-theme": "./src/hooks/use-mounted-theme.ts",
|
|
200
|
+
"./hooks/use-sticky": "./src/hooks/use-sticky.ts",
|
|
201
|
+
"./lib/card-hover": "./src/lib/card-hover.ts",
|
|
202
|
+
"./lib/grade": "./src/lib/grade.ts",
|
|
203
|
+
"./lib/icon-depth": "./src/lib/icon-depth.ts",
|
|
204
|
+
"./lib/icon-registry": "./src/lib/icon-registry.ts",
|
|
205
|
+
"./lib/motion": "./src/lib/motion.ts",
|
|
206
|
+
"./lib/overlay-pill": "./src/lib/overlay-pill.ts",
|
|
207
|
+
"./lib/utils": "./src/lib/utils.ts",
|
|
208
|
+
"./components/filter-option-chip": "./src/components/filter-option-chip.tsx",
|
|
209
|
+
"./components/filter-option-group": "./src/components/filter-option-group.tsx",
|
|
210
|
+
"./components/filter-panel-footer": "./src/components/filter-panel-footer.tsx"
|
|
211
|
+
},
|
|
212
|
+
"publishConfig": {
|
|
213
|
+
"exports": {
|
|
214
|
+
"./globals.css": "./src/globals.css",
|
|
215
|
+
"./components/*": {
|
|
216
|
+
"types": "./dist/components/*.d.ts",
|
|
217
|
+
"import": "./dist/components/*.js"
|
|
218
|
+
},
|
|
219
|
+
"./hooks/*": {
|
|
220
|
+
"types": "./dist/hooks/*.d.ts",
|
|
221
|
+
"import": "./dist/hooks/*.js"
|
|
222
|
+
},
|
|
223
|
+
"./lib/*": {
|
|
224
|
+
"types": "./dist/lib/*.d.ts",
|
|
225
|
+
"import": "./dist/lib/*.js"
|
|
226
|
+
}
|
|
31
227
|
}
|
|
32
228
|
},
|
|
229
|
+
"scripts": {
|
|
230
|
+
"storybook": "storybook dev -p 6006",
|
|
231
|
+
"build-storybook": "storybook build",
|
|
232
|
+
"build": "tsup",
|
|
233
|
+
"dev": "tsup --watch",
|
|
234
|
+
"typecheck": "tsc --noEmit",
|
|
235
|
+
"test": "vitest run",
|
|
236
|
+
"test:watch": "vitest",
|
|
237
|
+
"clean": "rm -rf dist",
|
|
238
|
+
"prepublishOnly": "pnpm run build"
|
|
239
|
+
},
|
|
33
240
|
"peerDependencies": {
|
|
34
241
|
"@hookform/resolvers": "^3.0.0 || ^5.0.0",
|
|
35
242
|
"@radix-ui/react-accordion": "*",
|
|
@@ -138,15 +345,5 @@
|
|
|
138
345
|
"vaul": "^1.1.2",
|
|
139
346
|
"vitest": "^4.1.5",
|
|
140
347
|
"zod": "^3.25.67"
|
|
141
|
-
},
|
|
142
|
-
"scripts": {
|
|
143
|
-
"storybook": "storybook dev -p 6006",
|
|
144
|
-
"build-storybook": "storybook build",
|
|
145
|
-
"build": "tsup",
|
|
146
|
-
"dev": "tsup --watch",
|
|
147
|
-
"typecheck": "tsc --noEmit",
|
|
148
|
-
"test": "vitest run",
|
|
149
|
-
"test:watch": "vitest",
|
|
150
|
-
"clean": "rm -rf dist"
|
|
151
348
|
}
|
|
152
|
-
}
|
|
349
|
+
}
|
|
@@ -283,6 +283,22 @@ export const Loading: Story = {
|
|
|
283
283
|
decorators: SideSheet.decorators,
|
|
284
284
|
};
|
|
285
285
|
|
|
286
|
+
export const EntryAndBrowseActions: Story = {
|
|
287
|
+
args: {
|
|
288
|
+
tags: [{ label: "Party", href: "/events?tags=party" }],
|
|
289
|
+
entryDetails: {
|
|
290
|
+
label: "Entry and tickets",
|
|
291
|
+
description: "Check availability with the organizer.",
|
|
292
|
+
action: (
|
|
293
|
+
<Button asChild fullWidth>
|
|
294
|
+
<a href="https://tickets.example.com">Get tickets</a>
|
|
295
|
+
</Button>
|
|
296
|
+
),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
decorators: SideSheet.decorators,
|
|
300
|
+
};
|
|
301
|
+
|
|
286
302
|
export const Error: Story = {
|
|
287
303
|
render: () => (
|
|
288
304
|
<EventDetailPanelError
|
|
@@ -37,6 +37,42 @@ const event: EventPreview = {
|
|
|
37
37
|
const base = { event, labels, formatters, dateLine: "Tuesday · 17:30" };
|
|
38
38
|
|
|
39
39
|
describe("EventDetailPanel", () => {
|
|
40
|
+
it("opens entry information and exposes a separate booking link", () => {
|
|
41
|
+
render(
|
|
42
|
+
<EventDetailPanel
|
|
43
|
+
{...base}
|
|
44
|
+
entryDetails={{
|
|
45
|
+
label: "Entry and tickets",
|
|
46
|
+
description: "Book with the organizer.",
|
|
47
|
+
action: <a href="https://tickets.example.com">Get tickets</a>,
|
|
48
|
+
}}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
expect(
|
|
52
|
+
screen.queryByRole("link", { name: "Get tickets" }),
|
|
53
|
+
).not.toBeInTheDocument();
|
|
54
|
+
fireEvent.click(screen.getByRole("button", { name: "Entry and tickets" }));
|
|
55
|
+
expect(screen.getByText("Book with the organizer.")).toBeVisible();
|
|
56
|
+
expect(screen.getByRole("link", { name: "Get tickets" })).toHaveAttribute(
|
|
57
|
+
"href",
|
|
58
|
+
"https://tickets.example.com",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("gives browse tags destinations while keeping descriptive tags out of the tab order", () => {
|
|
63
|
+
render(
|
|
64
|
+
<EventDetailPanel
|
|
65
|
+
{...base}
|
|
66
|
+
tags={["Descriptive", { label: "Party", href: "/events?tags=party" }]}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByRole("link", { name: "Party" })).toHaveAttribute(
|
|
70
|
+
"href",
|
|
71
|
+
"/events?tags=party",
|
|
72
|
+
);
|
|
73
|
+
expect(screen.getByText("Descriptive").tagName).toBe("SPAN");
|
|
74
|
+
});
|
|
75
|
+
|
|
40
76
|
it("shows the status chip for live, soon and ended, and none when upcoming", () => {
|
|
41
77
|
const { rerender } = render(<EventDetailPanel {...base} />);
|
|
42
78
|
expect(screen.queryByText("Now")).not.toBeInTheDocument();
|
|
@@ -19,6 +19,7 @@ import { cn } from "../lib/utils";
|
|
|
19
19
|
import { GradeAtom, type GradeAtomLabels } from "./grade-atom";
|
|
20
20
|
import type { PlaceGrade } from "./place-types";
|
|
21
21
|
import { Button } from "./button";
|
|
22
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
22
23
|
import { EventPosterFallback } from "./event-poster-fallback";
|
|
23
24
|
import { Separator } from "./separator";
|
|
24
25
|
import { Skeleton } from "./skeleton";
|
|
@@ -144,8 +145,15 @@ export type EventDetailPanelProps = {
|
|
|
144
145
|
/** The hype tier, rendered as given (the app's own badge) in the chip row. */
|
|
145
146
|
hype?: React.ReactNode;
|
|
146
147
|
venue?: EventDetailVenue;
|
|
147
|
-
/** Activity tags
|
|
148
|
-
tags?: string[];
|
|
148
|
+
/** Activity tags. Pass a destination to make a tag a browse action. */
|
|
149
|
+
tags?: (string | { label: string; href: string; onClick?: () => void })[];
|
|
150
|
+
/** Entry information opened by the price/free-entry control. */
|
|
151
|
+
entryDetails?: {
|
|
152
|
+
label: string;
|
|
153
|
+
description: string;
|
|
154
|
+
action?: React.ReactNode;
|
|
155
|
+
onOpenChange?: (open: boolean) => void;
|
|
156
|
+
};
|
|
149
157
|
/** Performer names, in the order the API gave them. */
|
|
150
158
|
performers?: string[];
|
|
151
159
|
/** How many performers to show before the "show all" chip. Default 5. */
|
|
@@ -197,6 +205,7 @@ export function EventDetailPanel({
|
|
|
197
205
|
hype,
|
|
198
206
|
venue,
|
|
199
207
|
tags,
|
|
208
|
+
entryDetails,
|
|
200
209
|
performers,
|
|
201
210
|
performersPreview = 5,
|
|
202
211
|
showAllPerformersLabel,
|
|
@@ -289,6 +298,8 @@ export function EventDetailPanel({
|
|
|
289
298
|
soonLabel={soonLabel}
|
|
290
299
|
hype={hype}
|
|
291
300
|
tags={tags}
|
|
301
|
+
entryDetails={entryDetails}
|
|
302
|
+
linkComponent={LinkComponent}
|
|
292
303
|
/>
|
|
293
304
|
|
|
294
305
|
{venue && (
|
|
@@ -675,7 +686,14 @@ export function EventDetailPanelSkeleton({
|
|
|
675
686
|
|
|
676
687
|
export type EventDetailChipRowProps = Pick<
|
|
677
688
|
EventDetailPanelProps,
|
|
678
|
-
|
|
689
|
+
| "event"
|
|
690
|
+
| "formatters"
|
|
691
|
+
| "status"
|
|
692
|
+
| "soonLabel"
|
|
693
|
+
| "hype"
|
|
694
|
+
| "tags"
|
|
695
|
+
| "entryDetails"
|
|
696
|
+
| "linkComponent"
|
|
679
697
|
> & {
|
|
680
698
|
/** Only the chip strings — a page rendering the row alone need not name the section headings. */
|
|
681
699
|
labels: Pick<EventDetailPanelLabels, "free" | "liveNow" | "ended">;
|
|
@@ -683,7 +701,7 @@ export type EventDetailChipRowProps = Pick<
|
|
|
683
701
|
};
|
|
684
702
|
|
|
685
703
|
/**
|
|
686
|
-
* One wrapping row
|
|
704
|
+
* One wrapping row: metadata, entry information, then activity links. Exported
|
|
687
705
|
* so the page renders the same row above its title — it is the panel's row,
|
|
688
706
|
* not a second one.
|
|
689
707
|
*/
|
|
@@ -695,11 +713,25 @@ export function EventDetailChipRow({
|
|
|
695
713
|
soonLabel,
|
|
696
714
|
hype,
|
|
697
715
|
tags,
|
|
716
|
+
entryDetails,
|
|
717
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
698
718
|
className,
|
|
699
719
|
}: EventDetailChipRowProps) {
|
|
700
720
|
const fee = resolveEventEntryFee(event);
|
|
721
|
+
const priceLabel =
|
|
722
|
+
fee.kind === "free"
|
|
723
|
+
? labels.free
|
|
724
|
+
: fee.kind === "paid"
|
|
725
|
+
? formatters.price(fee.amountRon)
|
|
726
|
+
: null;
|
|
727
|
+
const priceIcon =
|
|
728
|
+
fee.kind === "free" ? (
|
|
729
|
+
<Gift className="h-3.5 w-3.5" aria-hidden />
|
|
730
|
+
) : (
|
|
731
|
+
<Ticket className="h-3.5 w-3.5" aria-hidden />
|
|
732
|
+
);
|
|
701
733
|
return (
|
|
702
|
-
<div className={cn("flex flex-wrap gap-[7px]", className)}>
|
|
734
|
+
<div className={cn("flex flex-wrap items-center gap-[7px]", className)}>
|
|
703
735
|
{status === "live" && (
|
|
704
736
|
<span
|
|
705
737
|
className={cn(
|
|
@@ -732,7 +764,33 @@ export function EventDetailChipRow({
|
|
|
732
764
|
{labels.ended}
|
|
733
765
|
</span>
|
|
734
766
|
)}
|
|
735
|
-
{
|
|
767
|
+
{priceLabel && entryDetails ? (
|
|
768
|
+
<Popover onOpenChange={entryDetails.onOpenChange}>
|
|
769
|
+
<PopoverTrigger asChild>
|
|
770
|
+
<Button
|
|
771
|
+
variant="outline"
|
|
772
|
+
size="sm"
|
|
773
|
+
className="min-h-11 rounded-full"
|
|
774
|
+
aria-label={entryDetails.label}
|
|
775
|
+
>
|
|
776
|
+
{priceIcon}
|
|
777
|
+
{priceLabel}
|
|
778
|
+
<ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
|
779
|
+
</Button>
|
|
780
|
+
</PopoverTrigger>
|
|
781
|
+
<PopoverContent
|
|
782
|
+
align="start"
|
|
783
|
+
aria-label={entryDetails.label}
|
|
784
|
+
className="flex max-w-[calc(100vw-2rem)] flex-col gap-3"
|
|
785
|
+
>
|
|
786
|
+
<p className="text-sm font-semibold">{priceLabel}</p>
|
|
787
|
+
<p className="text-sm text-muted-foreground">
|
|
788
|
+
{entryDetails.description}
|
|
789
|
+
</p>
|
|
790
|
+
{entryDetails.action}
|
|
791
|
+
</PopoverContent>
|
|
792
|
+
</Popover>
|
|
793
|
+
) : fee.kind === "free" ? (
|
|
736
794
|
<span
|
|
737
795
|
className={cn(
|
|
738
796
|
CHIP,
|
|
@@ -742,19 +800,33 @@ export function EventDetailChipRow({
|
|
|
742
800
|
<Gift className="h-3 w-3" aria-hidden />
|
|
743
801
|
{labels.free}
|
|
744
802
|
</span>
|
|
745
|
-
)
|
|
746
|
-
{fee.kind === "paid" && (
|
|
803
|
+
) : fee.kind === "paid" ? (
|
|
747
804
|
<span className={cn(CHIP, "text-foreground")}>
|
|
748
805
|
<Ticket className="h-3 w-3" aria-hidden />
|
|
749
806
|
{formatters.price(fee.amountRon)}
|
|
750
807
|
</span>
|
|
751
|
-
)}
|
|
808
|
+
) : null}
|
|
752
809
|
{hype}
|
|
753
|
-
{tags?.map((tag) =>
|
|
754
|
-
|
|
755
|
-
{tag}
|
|
756
|
-
|
|
757
|
-
|
|
810
|
+
{tags?.map((tag) =>
|
|
811
|
+
typeof tag === "string" ? (
|
|
812
|
+
<span key={tag} className="text-xs text-muted-foreground">
|
|
813
|
+
{tag}
|
|
814
|
+
</span>
|
|
815
|
+
) : (
|
|
816
|
+
<Button
|
|
817
|
+
key={tag.href}
|
|
818
|
+
asChild
|
|
819
|
+
variant="outline"
|
|
820
|
+
size="sm"
|
|
821
|
+
className="min-h-11 rounded-full"
|
|
822
|
+
>
|
|
823
|
+
<LinkComponent href={tag.href} onClick={tag.onClick}>
|
|
824
|
+
{tag.label}
|
|
825
|
+
<ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
|
826
|
+
</LinkComponent>
|
|
827
|
+
</Button>
|
|
828
|
+
),
|
|
829
|
+
)}
|
|
758
830
|
</div>
|
|
759
831
|
);
|
|
760
832
|
}
|
|
@@ -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,79 @@ 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
|
+
});
|
|
112
|
+
|
|
113
|
+
// Where reviews' praise is the point (the map's lists), the price is a fact a
|
|
114
|
+
// person checks, not the row's headline: it steps down to secondary text and
|
|
115
|
+
// the praise count keeps the bold accent (HIG review, 2026-11-08).
|
|
116
|
+
describe("MenuItem emphasis", () => {
|
|
117
|
+
it("keeps the price bold by default", () => {
|
|
118
|
+
render(<MenuItem item={wine} labels={labels} formatPrice={String} onOpen={() => {}} />);
|
|
119
|
+
const price = screen.getByText("169");
|
|
120
|
+
expect(price.className).toContain("font-bold");
|
|
121
|
+
expect(price.className).toContain("text-foreground");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("sets the price as secondary text when praise leads", () => {
|
|
125
|
+
render(
|
|
126
|
+
<MenuItem item={wine} labels={labels} formatPrice={String} emphasis="praise" onOpen={() => {}} />
|
|
127
|
+
);
|
|
128
|
+
const price = screen.getByText("169");
|
|
129
|
+
expect(price.className).not.toContain("font-bold");
|
|
130
|
+
expect(price.className).toContain("text-muted-foreground");
|
|
131
|
+
expect(price.className).toContain("text-[13px]");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -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,20 @@ 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;
|
|
114
|
+
/**
|
|
115
|
+
* What the meta row leads with. `"price"` (default): the price is bold, the
|
|
116
|
+
* count's peer at the other end. `"praise"`: on surfaces where reviews'
|
|
117
|
+
* praise is the point (the map's lists), the price steps down to secondary
|
|
118
|
+
* text, a fact to check rather than the row's headline, and the praise count
|
|
119
|
+
* keeps the bold accent.
|
|
120
|
+
*/
|
|
121
|
+
emphasis?: "price" | "praise";
|
|
95
122
|
/**
|
|
96
123
|
* An outside aggregate score for the item (a wine's Vivino rating, a beer's
|
|
97
124
|
* Untappd one), shown as a star and the score beside the mention count.
|
|
@@ -136,7 +163,9 @@ export type MenuItemProps = {
|
|
|
136
163
|
};
|
|
137
164
|
|
|
138
165
|
/**
|
|
139
|
-
* The mention count: a
|
|
166
|
+
* The mention count: a glyph and the numeral, pinned right. The glyph is the
|
|
167
|
+
* quote glyph for every mention, or a thumbs-up when the count is praise only
|
|
168
|
+
* (see `MenuItemCountKind`).
|
|
140
169
|
*
|
|
141
170
|
* Deliberately NOT a badge — a chip would add a filled shape to a row that has
|
|
142
171
|
* none, and would read as a status label the venue was given rather than a
|
|
@@ -147,18 +176,22 @@ export function MentionSignal({
|
|
|
147
176
|
count,
|
|
148
177
|
onMentions,
|
|
149
178
|
formatCount = String,
|
|
179
|
+
countKind = "mentions",
|
|
150
180
|
}: {
|
|
151
181
|
label: string;
|
|
152
182
|
count: number;
|
|
153
183
|
onMentions?: () => void;
|
|
154
184
|
formatCount?: (value: number) => string;
|
|
185
|
+
countKind?: MenuItemCountKind;
|
|
155
186
|
}) {
|
|
156
187
|
/* The glyph and the numeral have to read as ONE object: `gap-0.5` instead of
|
|
157
188
|
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.
|
|
189
|
+
a prefix mark qualifying the value rather than a peer competing with it.
|
|
190
|
+
Both glyphs take the same slot, so switching the kind moves nothing. */
|
|
191
|
+
const Glyph = countKind === "praise" ? ThumbsUp : MessageSquareQuote;
|
|
159
192
|
const content = (
|
|
160
193
|
<span className="inline-flex items-center gap-0.5">
|
|
161
|
-
<
|
|
194
|
+
<Glyph className="h-3.5 w-3.5 opacity-60" aria-hidden />
|
|
162
195
|
<span className="tabular-nums">{formatCount(count)}</span>
|
|
163
196
|
</span>
|
|
164
197
|
);
|
|
@@ -233,6 +266,8 @@ export const MenuItem = memo(function MenuItem({
|
|
|
233
266
|
signatureTier = "strong",
|
|
234
267
|
onSignatureEvidence,
|
|
235
268
|
onMentions,
|
|
269
|
+
countKind = "mentions",
|
|
270
|
+
emphasis = "price",
|
|
236
271
|
externalRating,
|
|
237
272
|
formatRating = (score) => score.toFixed(1),
|
|
238
273
|
markGradient,
|
|
@@ -422,7 +457,14 @@ export const MenuItem = memo(function MenuItem({
|
|
|
422
457
|
<div className="flex min-w-0 items-baseline gap-x-2">
|
|
423
458
|
{minPrice != null && (
|
|
424
459
|
<span className="flex items-baseline gap-1">
|
|
425
|
-
<span
|
|
460
|
+
<span
|
|
461
|
+
className={cn(
|
|
462
|
+
"tabular-nums",
|
|
463
|
+
emphasis === "praise"
|
|
464
|
+
? "text-[13px] font-medium text-muted-foreground"
|
|
465
|
+
: "text-[15px] font-bold text-foreground"
|
|
466
|
+
)}
|
|
467
|
+
>
|
|
426
468
|
{isPriceRange
|
|
427
469
|
? `${formatPrice(minPrice)}–${formatPrice(maxPrice!)}`
|
|
428
470
|
: formatPrice(minPrice)}
|
|
@@ -455,6 +497,7 @@ export const MenuItem = memo(function MenuItem({
|
|
|
455
497
|
count={mentionCount}
|
|
456
498
|
onMentions={onMentions}
|
|
457
499
|
formatCount={formatCount}
|
|
500
|
+
countKind={countKind}
|
|
458
501
|
/>
|
|
459
502
|
)}
|
|
460
503
|
</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
|
|
@@ -169,3 +169,17 @@ describe("Tabs", () => {
|
|
|
169
169
|
expect(screen.getByText("Panel A")).toBeInTheDocument();
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
// In dark mode `bg-background` is darker than the pill track (`bg-muted`), so
|
|
174
|
+
// the selected segment read as a hole; it lifts to the raised surface there.
|
|
175
|
+
describe("pill indicator in dark mode", () => {
|
|
176
|
+
it("raises the selected segment above the track", () => {
|
|
177
|
+
const { container } = render(<Triplet />);
|
|
178
|
+
const indicators = container.querySelectorAll('[role="tab"][data-state="active"] > span[aria-hidden]');
|
|
179
|
+
expect(indicators.length).toBeGreaterThan(0);
|
|
180
|
+
indicators.forEach((indicator) => {
|
|
181
|
+
expect(indicator.className).toContain("dark:bg-surface-2");
|
|
182
|
+
expect(indicator.className).toContain("dark:ring-1");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
package/src/components/tabs.tsx
CHANGED
|
@@ -153,7 +153,10 @@ const TabsTrigger = React.forwardRef<
|
|
|
153
153
|
transition={archetype.indicatorMorph}
|
|
154
154
|
className={cn(
|
|
155
155
|
"absolute inset-0 -z-0 motion-reduce:hidden",
|
|
156
|
-
|
|
156
|
+
// Dark: `bg-background` is darker than the `bg-muted` track, so the
|
|
157
|
+
// selection read as a hole; it lifts to the raised surface instead.
|
|
158
|
+
variant === "pill" &&
|
|
159
|
+
"rounded-xl bg-background shadow-sm dark:bg-surface-2 dark:ring-1 dark:ring-border",
|
|
157
160
|
variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
|
|
158
161
|
variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
|
|
159
162
|
)}
|
|
@@ -165,7 +168,10 @@ const TabsTrigger = React.forwardRef<
|
|
|
165
168
|
aria-hidden
|
|
166
169
|
className={cn(
|
|
167
170
|
"absolute inset-0 -z-0 hidden motion-reduce:block",
|
|
168
|
-
|
|
171
|
+
// Dark: `bg-background` is darker than the `bg-muted` track, so the
|
|
172
|
+
// selection read as a hole; it lifts to the raised surface instead.
|
|
173
|
+
variant === "pill" &&
|
|
174
|
+
"rounded-xl bg-background shadow-sm dark:bg-surface-2 dark:ring-1 dark:ring-border",
|
|
169
175
|
variant === "underline" && orientation === "horizontal" && "border-b-2 border-primary",
|
|
170
176
|
variant === "underline" && orientation === "vertical" && "border-r-2 border-primary"
|
|
171
177
|
)}
|