@bearmenu/ui 0.8.54 → 0.8.56
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-CZ2HC7WY.js → chunk-EGN5DOLQ.js} +2 -2
- package/dist/{chunk-YE4ZQUTI.js → chunk-QOXXIYDG.js} +14 -2
- package/dist/{chunk-I5V446BX.js → chunk-TDTLEDX6.js} +1 -1
- package/dist/{chunk-6Z6MBILC.js → chunk-WBTSOARU.js} +66 -3
- package/dist/components/concept-compare.d.ts +1 -1
- package/dist/components/doorway-tiles.d.ts +1 -1
- package/dist/components/event-detail-panel.d.ts +1 -1
- package/dist/components/event-detail-panel.js +2 -2
- package/dist/components/grade-atom.d.ts +1 -1
- package/dist/components/grade-atom.js +2 -2
- package/dist/components/kitchen-dossier.d.ts +1 -1
- package/dist/components/menu-dead-end.d.ts +1 -1
- package/dist/components/menu-family-block.d.ts +1 -1
- package/dist/components/menu-family-block.js +3 -3
- package/dist/components/menu-family-card.d.ts +1 -1
- package/dist/components/menu-filter-chips.d.ts +1 -1
- package/dist/components/menu-group-rows.d.ts +1 -1
- package/dist/components/menu-index-header.d.ts +1 -1
- package/dist/components/menu-index-list.d.ts +1 -1
- package/dist/components/menu-intent-hero.d.ts +1 -1
- package/dist/components/menu-item.d.ts +42 -1
- package/dist/components/menu-item.js +3 -1
- package/dist/components/menu-kitchen-list.d.ts +1 -1
- package/dist/components/menu-kitchen-list.js +3 -3
- package/dist/components/menu-mocks.d.ts +1 -1
- package/dist/components/menu-types.d.ts +1 -1
- package/dist/components/open-now-list.d.ts +1 -1
- package/dist/components/place-about-tab.d.ts +1 -1
- package/dist/components/place-card.d.ts +1 -1
- package/dist/components/place-card.js +2 -2
- package/dist/components/place-details-mobile.d.ts +1 -1
- package/dist/components/place-schedule-week.d.ts +1 -1
- package/dist/components/place-types.d.ts +1 -1
- package/dist/components/qa-block.d.ts +1 -1
- package/dist/components/tabs.js +6 -2
- package/dist/components/top-comparison.d.ts +1 -1
- package/dist/components/top-cross-link.d.ts +1 -1
- package/dist/components/top-ranking-entry.d.ts +1 -1
- package/dist/components/top-ranking-entry.js +3 -1
- package/dist/components/top-ranking-hero.d.ts +1 -1
- package/dist/components/top-types.d.ts +1 -1
- package/dist/components/venue-grade-dimensions.d.ts +1 -1
- package/dist/components/venue-quotes.d.ts +1 -1
- package/dist/components/venue-wall.d.ts +1 -1
- package/dist/lib/grade.d.ts +1 -1
- package/dist/lib/grade.js +1 -1
- package/dist/{place-types-Dzy6FH67.d.ts → place-types-CjB4JlH0.d.ts} +2 -2
- package/package.json +1 -1
- package/src/components/grade-atom.test.tsx +15 -0
- package/src/components/grade-atom.tsx +15 -3
- package/src/components/menu-item.stories.tsx +108 -0
- package/src/components/menu-item.test.tsx +122 -0
- package/src/components/menu-item.tsx +106 -7
- package/src/components/tabs.test.tsx +14 -0
- package/src/components/tabs.tsx +8 -2
- package/src/lib/grade.ts +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { GradeAtom } from './chunk-
|
|
2
|
-
import { showsGradeAtom } from './chunk-
|
|
1
|
+
import { GradeAtom } from './chunk-QOXXIYDG.js';
|
|
2
|
+
import { showsGradeAtom } from './chunk-TDTLEDX6.js';
|
|
3
3
|
import { Separator } from './chunk-NWZGSLPU.js';
|
|
4
4
|
import { Skeleton } from './chunk-XBBEX4SF.js';
|
|
5
5
|
import { cn } from './chunk-FEK5XGZW.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shouldShowGradeNumber, resolveGradeBand, formatGrade } from './chunk-
|
|
1
|
+
import { shouldShowGradeNumber, resolveGradeBand, formatGrade } from './chunk-TDTLEDX6.js';
|
|
2
2
|
import { cn } from './chunk-FEK5XGZW.js';
|
|
3
3
|
import { Info, ShieldAlert } from 'lucide-react';
|
|
4
4
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
@@ -22,6 +22,18 @@ var BAND_CLASSES_PHOTO = {
|
|
|
22
22
|
weak: { ring: "ring-rose-400/70", num: "text-rose-200" }
|
|
23
23
|
};
|
|
24
24
|
var SIZE = {
|
|
25
|
+
// Inline in a line of text (a menu row's name or venue line): 18px fits a
|
|
26
|
+
// 15px name and a 12px venue line without making either taller, which the
|
|
27
|
+
// 24px xs did (Adrian, 2026-11-09). Number only, like xs.
|
|
28
|
+
"2xs": {
|
|
29
|
+
box: "h-[18px] px-1 rounded",
|
|
30
|
+
minW: "min-w-7",
|
|
31
|
+
num: "text-2xs",
|
|
32
|
+
desc: "text-2xs",
|
|
33
|
+
gap: "gap-0.5",
|
|
34
|
+
shield: "h-2.5 w-2.5",
|
|
35
|
+
showDesc: false
|
|
36
|
+
},
|
|
25
37
|
xs: {
|
|
26
38
|
box: "h-6 px-1.5 rounded-md",
|
|
27
39
|
minW: "min-w-9",
|
|
@@ -73,7 +85,7 @@ function GradeAtom({
|
|
|
73
85
|
const sz = SIZE[size];
|
|
74
86
|
const infoGlyph = showInfo ? /* @__PURE__ */ jsx(Info, { className: cn("shrink-0 opacity-70", sz.shield), "aria-hidden": true }) : null;
|
|
75
87
|
if (!shouldShowGradeNumber(grade)) {
|
|
76
|
-
if (size === "xs") return null;
|
|
88
|
+
if (size === "xs" || size === "2xs") return null;
|
|
77
89
|
return /* @__PURE__ */ jsxs(
|
|
78
90
|
"span",
|
|
79
91
|
{
|
|
@@ -16,7 +16,7 @@ function shouldShowGradeNumber(grade) {
|
|
|
16
16
|
}
|
|
17
17
|
function showsGradeAtom(grade, size) {
|
|
18
18
|
if ((grade == null ? void 0 : grade.overall) == null) return false;
|
|
19
|
-
return size !== "xs" || shouldShowGradeNumber(grade);
|
|
19
|
+
return size !== "xs" && size !== "2xs" || shouldShowGradeNumber(grade);
|
|
20
20
|
}
|
|
21
21
|
function resolveDimensionTone(score) {
|
|
22
22
|
if (score >= 75) return "strong";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { SignatureChip } from './chunk-6PQINSAA.js';
|
|
2
|
+
import { GradeAtom } from './chunk-QOXXIYDG.js';
|
|
3
|
+
import { showsGradeAtom } from './chunk-TDTLEDX6.js';
|
|
2
4
|
import { cn } from './chunk-FEK5XGZW.js';
|
|
3
5
|
import { __objRest, __spreadValues } from './chunk-MMUBH76A.js';
|
|
4
6
|
import { memo } from 'react';
|
|
5
7
|
import { Star, ThumbsUp, MessageSquareQuote } from 'lucide-react';
|
|
6
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
8
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
9
|
|
|
8
10
|
function DefaultLink(_a) {
|
|
9
11
|
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
@@ -78,10 +80,15 @@ var MenuItem = memo(function MenuItem2({
|
|
|
78
80
|
onSignatureEvidence,
|
|
79
81
|
onMentions,
|
|
80
82
|
countKind = "mentions",
|
|
83
|
+
emphasis = "price",
|
|
81
84
|
externalRating,
|
|
82
85
|
formatRating = (score) => score.toFixed(1),
|
|
83
86
|
markGradient,
|
|
84
87
|
venueName,
|
|
88
|
+
venueStatus,
|
|
89
|
+
venueDetail,
|
|
90
|
+
venueGrade,
|
|
91
|
+
nameGrade,
|
|
85
92
|
subtitle,
|
|
86
93
|
href,
|
|
87
94
|
linkLabel,
|
|
@@ -183,6 +190,16 @@ var MenuItem = memo(function MenuItem2({
|
|
|
183
190
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
184
191
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
|
|
185
192
|
/* @__PURE__ */ jsx("h3", { className: "text-[15px] font-semibold leading-snug text-foreground", children: item.name }),
|
|
193
|
+
nameGrade && showsGradeAtom(nameGrade.grade, "2xs") && /* @__PURE__ */ jsx(
|
|
194
|
+
GradeAtom,
|
|
195
|
+
{
|
|
196
|
+
grade: nameGrade.grade,
|
|
197
|
+
labels: nameGrade.labels,
|
|
198
|
+
size: "2xs",
|
|
199
|
+
hideDescriptor: true,
|
|
200
|
+
className: "shrink-0 self-center"
|
|
201
|
+
}
|
|
202
|
+
),
|
|
186
203
|
showSignatureBadge && /* @__PURE__ */ jsx(
|
|
187
204
|
SignatureChip,
|
|
188
205
|
{
|
|
@@ -192,7 +209,44 @@ var MenuItem = memo(function MenuItem2({
|
|
|
192
209
|
}
|
|
193
210
|
)
|
|
194
211
|
] }),
|
|
195
|
-
venueName && /* @__PURE__ */
|
|
212
|
+
venueName && /* @__PURE__ */ jsxs("p", { className: "mt-0.5 flex min-w-0 items-center gap-1.5 text-[12px]", children: [
|
|
213
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate font-semibold text-foreground", children: venueName }),
|
|
214
|
+
venueGrade && showsGradeAtom(venueGrade.grade, "2xs") && /* @__PURE__ */ jsx(
|
|
215
|
+
GradeAtom,
|
|
216
|
+
{
|
|
217
|
+
grade: venueGrade.grade,
|
|
218
|
+
labels: venueGrade.labels,
|
|
219
|
+
size: "2xs",
|
|
220
|
+
hideDescriptor: true,
|
|
221
|
+
className: "shrink-0"
|
|
222
|
+
}
|
|
223
|
+
),
|
|
224
|
+
venueDetail && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
225
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-muted-foreground", children: "\xB7" }),
|
|
226
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 font-normal text-muted-foreground", children: venueDetail })
|
|
227
|
+
] }),
|
|
228
|
+
venueStatus && /* @__PURE__ */ jsxs(
|
|
229
|
+
"span",
|
|
230
|
+
{
|
|
231
|
+
"data-slot": "venue-status",
|
|
232
|
+
"data-tone": venueStatus.tone,
|
|
233
|
+
className: "inline-flex shrink-0 items-center gap-1 font-normal text-muted-foreground",
|
|
234
|
+
children: [
|
|
235
|
+
/* @__PURE__ */ jsx(
|
|
236
|
+
"span",
|
|
237
|
+
{
|
|
238
|
+
"aria-hidden": true,
|
|
239
|
+
className: cn(
|
|
240
|
+
"size-1.5 shrink-0 rounded-full",
|
|
241
|
+
venueStatus.tone === "open" ? "bg-success" : "bg-muted-foreground"
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
),
|
|
245
|
+
venueStatus.label
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
] }),
|
|
196
250
|
subtitle ? /* @__PURE__ */ jsx("p", { className: "mt-0.5 line-clamp-2 text-xs italic leading-snug text-muted-foreground", children: subtitle }) : item.description && /* @__PURE__ */ jsx(
|
|
197
251
|
"p",
|
|
198
252
|
{
|
|
@@ -206,7 +260,16 @@ var MenuItem = memo(function MenuItem2({
|
|
|
206
260
|
(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: [
|
|
207
261
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-baseline gap-x-2", children: [
|
|
208
262
|
minPrice != null && /* @__PURE__ */ jsxs("span", { className: "flex items-baseline gap-1", children: [
|
|
209
|
-
/* @__PURE__ */ jsx(
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
"span",
|
|
265
|
+
{
|
|
266
|
+
className: cn(
|
|
267
|
+
"tabular-nums",
|
|
268
|
+
emphasis === "praise" ? "text-[13px] font-medium text-muted-foreground" : "text-[15px] font-bold text-foreground"
|
|
269
|
+
),
|
|
270
|
+
children: isPriceRange ? `${formatPrice(minPrice)}\u2013${formatPrice(maxPrice)}` : formatPrice(minPrice)
|
|
271
|
+
}
|
|
272
|
+
),
|
|
210
273
|
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: labels.currency })
|
|
211
274
|
] }),
|
|
212
275
|
hasQuantity && /* @__PURE__ */ jsxs("span", { className: "text-[12px] tabular-nums text-muted-foreground", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MenuConceptPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
3
|
import 'react';
|
|
4
|
-
import '../place-types-
|
|
4
|
+
import '../place-types-CjB4JlH0.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { G as GradeAtomLabels, a as PlaceGrade } from '../place-types-
|
|
3
|
+
import { G as GradeAtomLabels, a as PlaceGrade } from '../place-types-CjB4JlH0.js';
|
|
4
4
|
import { EventPreview, EventFormatters } from './event-types.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { GradeAtom } from '../chunk-
|
|
2
|
-
import { showsGradeAtom } from '../chunk-
|
|
1
|
+
import { GradeAtom } from '../chunk-QOXXIYDG.js';
|
|
2
|
+
import { showsGradeAtom } from '../chunk-TDTLEDX6.js';
|
|
3
3
|
import { resolveEventEntryFee } from '../chunk-DNSXMHYX.js';
|
|
4
4
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
5
5
|
import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import 'react/jsx-runtime';
|
|
2
2
|
import '../lib/grade.js';
|
|
3
|
-
export { i as GradeAtom, G as GradeAtomLabels, j as GradeAtomProps, k as GradeAtomSize } from '../place-types-
|
|
3
|
+
export { i as GradeAtom, G as GradeAtomLabels, j as GradeAtomProps, k as GradeAtomSize } from '../place-types-CjB4JlH0.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { MenuVenueSignature, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
4
|
-
import { P as PlacePreview, C as CitedText, b as PlaceGradeDimensions } from '../place-types-
|
|
4
|
+
import { P as PlacePreview, C as CitedText, b as PlaceGradeDimensions } from '../place-types-CjB4JlH0.js';
|
|
5
5
|
import { VenueGradeDimensionKey } from './venue-grade-dimensions.js';
|
|
6
6
|
import '../lib/grade.js';
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MenuKitchenListLabels } from './menu-kitchen-list.js';
|
|
3
3
|
import { MenuFamilyBlockPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
4
|
-
import '../place-types-
|
|
4
|
+
import '../place-types-CjB4JlH0.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
import 'react';
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { MenuKitchenList } from '../chunk-
|
|
1
|
+
import { MenuKitchenList } from '../chunk-EGN5DOLQ.js';
|
|
2
2
|
import { HueChip } from '../chunk-6VOWT4YR.js';
|
|
3
|
-
import '../chunk-
|
|
4
|
-
import '../chunk-
|
|
3
|
+
import '../chunk-QOXXIYDG.js';
|
|
4
|
+
import '../chunk-TDTLEDX6.js';
|
|
5
5
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
6
6
|
import { PhotoGround, PHOTO_GROUND } from '../chunk-SWD6CJ35.js';
|
|
7
7
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MenuFamilyPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
3
|
import 'react';
|
|
4
|
-
import '../place-types-
|
|
4
|
+
import '../place-types-CjB4JlH0.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MenuIndexEntry, MenuLinkComponent } from './menu-types.js';
|
|
3
3
|
import 'react';
|
|
4
|
-
import '../place-types-
|
|
4
|
+
import '../place-types-CjB4JlH0.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
7
7
|
type MenuIndexHeaderProps = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MenuIndexEntry, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
3
|
import 'react';
|
|
4
|
-
import '../place-types-
|
|
4
|
+
import '../place-types-CjB4JlH0.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
import { a as PlaceGrade, G as GradeAtomLabels } from '../place-types-CjB4JlH0.js';
|
|
3
4
|
import { MenuItemPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
4
|
-
import '../place-types-Dzy6FH67.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -103,6 +103,14 @@ type MenuItemProps = {
|
|
|
103
103
|
* Defaults to `"mentions"`. Keep `labels.mentions` in step with it.
|
|
104
104
|
*/
|
|
105
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";
|
|
106
114
|
/**
|
|
107
115
|
* An outside aggregate score for the item (a wine's Vivino rating, a beer's
|
|
108
116
|
* Untappd one), shown as a star and the score beside the mention count.
|
|
@@ -126,6 +134,39 @@ type MenuItemProps = {
|
|
|
126
134
|
* default.
|
|
127
135
|
*/
|
|
128
136
|
venueName?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Whether the venue is open, on the venue line after its name: a dot
|
|
139
|
+
* (`open` in the success green, `closed` muted) and the caller's words, e.g.
|
|
140
|
+
* "Closes at 23:00" / "Opens at 09:00". Only with `venueName`.
|
|
141
|
+
*/
|
|
142
|
+
venueStatus?: {
|
|
143
|
+
tone: "open" | "closed";
|
|
144
|
+
label: string;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* A short detail about the venue, muted after its name and grade: its
|
|
148
|
+
* neighbourhood, say. Only with `venueName`.
|
|
149
|
+
*/
|
|
150
|
+
venueDetail?: string;
|
|
151
|
+
/**
|
|
152
|
+
* The venue's grade, drawn as `<GradeAtom size="2xs">` (inline, 18px) beside its name. Only
|
|
153
|
+
* a grade with a number shows: the xs atom draws nothing for the soft
|
|
154
|
+
* "worth exploring" state (see `showsGradeAtom`).
|
|
155
|
+
*/
|
|
156
|
+
venueGrade?: {
|
|
157
|
+
grade: PlaceGrade;
|
|
158
|
+
labels: GradeAtomLabels;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* A grade beside the row's NAME, for rows where the name is the place (the
|
|
162
|
+
* map's Drinks list leads with the bar, and its venue line says what the
|
|
163
|
+
* bar is praised for). Use it instead of `venueGrade` there, so the grade
|
|
164
|
+
* never sits beside "Praised for cocktails". Same rule: only with a number.
|
|
165
|
+
*/
|
|
166
|
+
nameGrade?: {
|
|
167
|
+
grade: PlaceGrade;
|
|
168
|
+
labels: GradeAtomLabels;
|
|
169
|
+
};
|
|
129
170
|
/**
|
|
130
171
|
* Explicit subtitle, in italic, in place of the description. Used where the
|
|
131
172
|
* reviewer's reason IS the row's content rather than the payoff for tapping.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export { ExternalRatingSignal, MentionSignal, MenuItem } from '../chunk-
|
|
1
|
+
export { ExternalRatingSignal, MentionSignal, MenuItem } from '../chunk-WBTSOARU.js';
|
|
2
2
|
import '../chunk-6PQINSAA.js';
|
|
3
|
+
import '../chunk-QOXXIYDG.js';
|
|
4
|
+
import '../chunk-TDTLEDX6.js';
|
|
3
5
|
import '../chunk-RYPPN2GZ.js';
|
|
4
6
|
import '../chunk-FEK5XGZW.js';
|
|
5
7
|
import '../chunk-MMUBH76A.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { G as GradeAtomLabels } from '../place-types-
|
|
2
|
+
import { G as GradeAtomLabels } from '../place-types-CjB4JlH0.js';
|
|
3
3
|
import { MenuKitchenPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
4
4
|
import '../lib/grade.js';
|
|
5
5
|
import 'react';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { MenuKitchenList, MenuKitchenListSkeleton } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import '../chunk-
|
|
1
|
+
export { MenuKitchenList, MenuKitchenListSkeleton } from '../chunk-EGN5DOLQ.js';
|
|
2
|
+
import '../chunk-QOXXIYDG.js';
|
|
3
|
+
import '../chunk-TDTLEDX6.js';
|
|
4
4
|
import '../chunk-NWZGSLPU.js';
|
|
5
5
|
import '../chunk-XBBEX4SF.js';
|
|
6
6
|
import '../chunk-FEK5XGZW.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MenuFamilyBlockPreview, MenuConceptPreview, MenuFamilyPreview, MenuFilterChip, MenuIndexEntry, MenuIntentFigure, MenuItemPreview, MenuKitchenPreview, MenuLoosenOption, QaItem, MenuVenuePreview } from './menu-types.js';
|
|
2
2
|
import { DoorwayTile } from './doorway-tiles.js';
|
|
3
|
-
import { P as PlacePreview } from '../place-types-
|
|
3
|
+
import { P as PlacePreview } from '../place-types-CjB4JlH0.js';
|
|
4
4
|
import 'react';
|
|
5
5
|
import 'react/jsx-runtime';
|
|
6
6
|
import '../lib/grade.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentType, CSSProperties, ReactNode } from 'react';
|
|
2
|
-
import { M as MenuSizeAndPrice, P as PlacePreview } from '../place-types-
|
|
2
|
+
import { M as MenuSizeAndPrice, P as PlacePreview } from '../place-types-CjB4JlH0.js';
|
|
3
3
|
import 'react/jsx-runtime';
|
|
4
4
|
import '../lib/grade.js';
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MenuVenuePreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
3
|
import 'react';
|
|
4
|
-
import '../place-types-
|
|
4
|
+
import '../place-types-CjB4JlH0.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { P as PlacePreview, c as PlaceAboutTabLabels, d as PlaceLinkKind } from '../place-types-
|
|
2
|
+
import { P as PlacePreview, c as PlaceAboutTabLabels, d as PlaceLinkKind } from '../place-types-CjB4JlH0.js';
|
|
3
3
|
import '../lib/grade.js';
|
|
4
4
|
|
|
5
5
|
interface PlaceAboutTabProps {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as React$1 from 'react';
|
|
3
3
|
import { ReactNode, MouseEvent } from 'react';
|
|
4
|
-
import { P as PlacePreview, e as PlaceCardLabels } from '../place-types-
|
|
4
|
+
import { P as PlacePreview, e as PlaceCardLabels } from '../place-types-CjB4JlH0.js';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { HOVER_IMAGE_MOTION, HOVER_IMAGE_WRAPPER, HOVER_MOTION, HOVER_LIFT_GRID_GROUP } from '../chunk-XCB3F6SF.js';
|
|
2
2
|
import { NEUTRAL_ICON_FILTER } from '../chunk-LQWGZIYA.js';
|
|
3
3
|
import { OVERLAY_PILL } from '../chunk-EZG2NELX.js';
|
|
4
|
-
import { GradeAtom } from '../chunk-
|
|
5
|
-
import { showsGradeAtom } from '../chunk-
|
|
4
|
+
import { GradeAtom } from '../chunk-QOXXIYDG.js';
|
|
5
|
+
import { showsGradeAtom } from '../chunk-TDTLEDX6.js';
|
|
6
6
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
7
7
|
import { springSnappy } from '../chunk-OIV7B44M.js';
|
|
8
8
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { P as PlacePreview, f as PlaceDetailsMobileLabels } from '../place-types-
|
|
2
|
+
import { P as PlacePreview, f as PlaceDetailsMobileLabels } from '../place-types-CjB4JlH0.js';
|
|
3
3
|
import '../lib/grade.js';
|
|
4
4
|
|
|
5
5
|
interface PlaceDetailsMobileProps {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { g as PlaceSchedule, h as PlaceScheduleWeekLabels } from '../place-types-
|
|
2
|
+
import { g as PlaceSchedule, h as PlaceScheduleWeekLabels } from '../place-types-CjB4JlH0.js';
|
|
3
3
|
import '../lib/grade.js';
|
|
4
4
|
|
|
5
5
|
interface PlaceScheduleWeekProps {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { C as CitedText, l as CitedValue, M as MenuSizeAndPrice, c as PlaceAboutTabLabels, e as PlaceCardLabels, f as PlaceDetailsMobileLabels, a as PlaceGrade, b as PlaceGradeDimensions, m as PlaceHourBlock, d as PlaceLinkKind, n as PlaceMustTry, P as PlacePreview, o as PlacePriceRange, p as PlaceReviewsDigest, g as PlaceSchedule, h as PlaceScheduleWeekLabels, W as WeekdayKey } from '../place-types-
|
|
1
|
+
export { C as CitedText, l as CitedValue, M as MenuSizeAndPrice, c as PlaceAboutTabLabels, e as PlaceCardLabels, f as PlaceDetailsMobileLabels, a as PlaceGrade, b as PlaceGradeDimensions, m as PlaceHourBlock, d as PlaceLinkKind, n as PlaceMustTry, P as PlacePreview, o as PlacePriceRange, p as PlaceReviewsDigest, g as PlaceSchedule, h as PlaceScheduleWeekLabels, W as WeekdayKey } from '../place-types-CjB4JlH0.js';
|
|
2
2
|
import 'react/jsx-runtime';
|
|
3
3
|
import '../lib/grade.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
|
)
|
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { MenuLinkComponent } from './menu-types.js';
|
|
3
3
|
import { TopComparisonRow } from './top-types.js';
|
|
4
4
|
import 'react';
|
|
5
|
-
import '../place-types-
|
|
5
|
+
import '../place-types-CjB4JlH0.js';
|
|
6
6
|
import '../lib/grade.js';
|
|
7
7
|
|
|
8
8
|
type TopComparisonLabels = {
|
|
@@ -3,7 +3,7 @@ import { MenuItemLabels } from './menu-item.js';
|
|
|
3
3
|
import { MenuItemPreview, MenuLinkComponent } from './menu-types.js';
|
|
4
4
|
import { TopRankEntry } from './top-types.js';
|
|
5
5
|
import 'react';
|
|
6
|
-
import '../place-types-
|
|
6
|
+
import '../place-types-CjB4JlH0.js';
|
|
7
7
|
import '../lib/grade.js';
|
|
8
8
|
|
|
9
9
|
type TopRankingEntryLabels = {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { MenuItem } from '../chunk-
|
|
1
|
+
import { MenuItem } from '../chunk-WBTSOARU.js';
|
|
2
2
|
import '../chunk-6PQINSAA.js';
|
|
3
|
+
import '../chunk-QOXXIYDG.js';
|
|
4
|
+
import '../chunk-TDTLEDX6.js';
|
|
3
5
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
4
6
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
5
7
|
import '../chunk-RYPPN2GZ.js';
|
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { MenuLinkComponent } from './menu-types.js';
|
|
3
3
|
import { TopExit } from './top-types.js';
|
|
4
4
|
import 'react';
|
|
5
|
-
import '../place-types-
|
|
5
|
+
import '../place-types-CjB4JlH0.js';
|
|
6
6
|
import '../lib/grade.js';
|
|
7
7
|
|
|
8
8
|
type TopRankingHeroProps = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
|
-
import { P as PlacePreview } from '../place-types-
|
|
3
|
+
import { P as PlacePreview } from '../place-types-CjB4JlH0.js';
|
|
4
4
|
import 'react';
|
|
5
5
|
import '../lib/grade.js';
|
|
6
6
|
|
package/dist/lib/grade.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ declare function shouldShowGradeNumber(grade: {
|
|
|
44
44
|
declare function showsGradeAtom<T extends {
|
|
45
45
|
overall?: number | null;
|
|
46
46
|
show_number?: boolean;
|
|
47
|
-
}>(grade: T | null | undefined, size: "xs" | "sm" | "md" | "lg"): grade is T;
|
|
47
|
+
}>(grade: T | null | undefined, size: "2xs" | "xs" | "sm" | "md" | "lg"): grade is T;
|
|
48
48
|
/** Tone for a single 0–100 dimension score, driving the page dimension-bar
|
|
49
49
|
* colour: strong (≥75) = accent, adequate (50–74) = muted, low (<50) = amber
|
|
50
50
|
* (a soft "fair warning", never error-red). */
|
package/dist/lib/grade.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { formatGrade, resolveDimensionTone, resolveGradeBand, shouldShowGradeNumber, showsGradeAtom } from '../chunk-
|
|
1
|
+
export { formatGrade, resolveDimensionTone, resolveGradeBand, shouldShowGradeNumber, showsGradeAtom } from '../chunk-TDTLEDX6.js';
|
|
2
2
|
import '../chunk-MMUBH76A.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { GradeBand } from './lib/grade.js';
|
|
3
3
|
|
|
4
|
-
type GradeAtomSize = "xs" | "sm" | "md" | "lg";
|
|
4
|
+
type GradeAtomSize = "2xs" | "xs" | "sm" | "md" | "lg";
|
|
5
5
|
/** Labels passed to `<GradeAtom>`. All translation happens in the caller. */
|
|
6
6
|
type GradeAtomLabels = {
|
|
7
7
|
/** The `show_number=false` state, e.g. "Worth exploring". */
|
|
@@ -23,7 +23,7 @@ type GradeAtomLabels = {
|
|
|
23
23
|
type GradeAtomProps = {
|
|
24
24
|
grade: PlaceGrade;
|
|
25
25
|
labels: GradeAtomLabels;
|
|
26
|
-
/** xs = card chip · sm = chat/compact · md = drawer · lg = page hero. */
|
|
26
|
+
/** 2xs = inline in a line of text · xs = card chip · sm = chat/compact · md = drawer · lg = page hero. */
|
|
27
27
|
size?: GradeAtomSize;
|
|
28
28
|
/** Force-hide the descriptor even at sizes that would show it (tight rows). */
|
|
29
29
|
hideDescriptor?: boolean;
|
package/package.json
CHANGED
|
@@ -23,6 +23,21 @@ describe("GradeAtom", () => {
|
|
|
23
23
|
expect(screen.getByLabelText("Nota 8,7 din 10")).toHaveTextContent("8,7");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
// A pill that sits inside a line of text (a menu row's name or venue line)
|
|
27
|
+
// must not make the line taller: at `xs` (24px) it pushed rows apart
|
|
28
|
+
// (Adrian, 9 Nov). `2xs` is 18px with an 11px number, and like `xs` it has
|
|
29
|
+
// no room for the soft "worth exploring" sentence.
|
|
30
|
+
it("has an inline size that fits a line of text, number only", () => {
|
|
31
|
+
render(<GradeAtom grade={{ overall: 8.7 }} labels={labels} size="2xs" />);
|
|
32
|
+
const pill = screen.getByLabelText("Grade 8.7 out of 10");
|
|
33
|
+
expect(pill.className).toContain("h-[18px]");
|
|
34
|
+
expect(pill.innerHTML).toContain("text-2xs");
|
|
35
|
+
const { container } = render(
|
|
36
|
+
<GradeAtom grade={{ overall: 8.7, show_number: false }} labels={labels} size="2xs" />
|
|
37
|
+
);
|
|
38
|
+
expect(container.textContent).toBe("");
|
|
39
|
+
});
|
|
40
|
+
|
|
26
41
|
it("hands the formatter the rounded, clamped grade", () => {
|
|
27
42
|
const seen: number[] = [];
|
|
28
43
|
const spy: GradeAtomLabels = { ...labels, value: (n) => (seen.push(n), String(n)) };
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "../lib/grade";
|
|
30
30
|
import type { PlaceGrade } from "./place-types";
|
|
31
31
|
|
|
32
|
-
export type GradeAtomSize = "xs" | "sm" | "md" | "lg";
|
|
32
|
+
export type GradeAtomSize = "2xs" | "xs" | "sm" | "md" | "lg";
|
|
33
33
|
|
|
34
34
|
/** Labels passed to `<GradeAtom>`. All translation happens in the caller. */
|
|
35
35
|
export type GradeAtomLabels = {
|
|
@@ -53,7 +53,7 @@ export type GradeAtomLabels = {
|
|
|
53
53
|
export type GradeAtomProps = {
|
|
54
54
|
grade: PlaceGrade;
|
|
55
55
|
labels: GradeAtomLabels;
|
|
56
|
-
/** xs = card chip · sm = chat/compact · md = drawer · lg = page hero. */
|
|
56
|
+
/** 2xs = inline in a line of text · xs = card chip · sm = chat/compact · md = drawer · lg = page hero. */
|
|
57
57
|
size?: GradeAtomSize;
|
|
58
58
|
/** Force-hide the descriptor even at sizes that would show it (tight rows). */
|
|
59
59
|
hideDescriptor?: boolean;
|
|
@@ -136,6 +136,18 @@ const SIZE: Record<
|
|
|
136
136
|
showDesc: boolean;
|
|
137
137
|
}
|
|
138
138
|
> = {
|
|
139
|
+
// Inline in a line of text (a menu row's name or venue line): 18px fits a
|
|
140
|
+
// 15px name and a 12px venue line without making either taller, which the
|
|
141
|
+
// 24px xs did (Adrian, 2026-11-09). Number only, like xs.
|
|
142
|
+
"2xs": {
|
|
143
|
+
box: "h-[18px] px-1 rounded",
|
|
144
|
+
minW: "min-w-7",
|
|
145
|
+
num: "text-2xs",
|
|
146
|
+
desc: "text-2xs",
|
|
147
|
+
gap: "gap-0.5",
|
|
148
|
+
shield: "h-2.5 w-2.5",
|
|
149
|
+
showDesc: false,
|
|
150
|
+
},
|
|
139
151
|
xs: {
|
|
140
152
|
box: "h-6 px-1.5 rounded-md",
|
|
141
153
|
minW: "min-w-9",
|
|
@@ -208,7 +220,7 @@ export function GradeAtom({
|
|
|
208
220
|
// caller falls back to its Google star instead (gate on `showsGradeAtom`, and
|
|
209
221
|
// see its note for why callers can't test `overall != null` any more).
|
|
210
222
|
if (!shouldShowGradeNumber(grade)) {
|
|
211
|
-
if (size === "xs") return null;
|
|
223
|
+
if (size === "xs" || size === "2xs") return null;
|
|
212
224
|
return (
|
|
213
225
|
<span
|
|
214
226
|
className={cn(
|
|
@@ -80,3 +80,111 @@ export const CountKinds: Story = {
|
|
|
80
80
|
</div>
|
|
81
81
|
),
|
|
82
82
|
};
|
|
83
|
+
|
|
84
|
+
const brisket: MenuItemPreview = {
|
|
85
|
+
id: "i2",
|
|
86
|
+
slug: "brisket",
|
|
87
|
+
name: "Brisket",
|
|
88
|
+
kind: "dish",
|
|
89
|
+
category_slug: "meat",
|
|
90
|
+
mentions: 60,
|
|
91
|
+
sizes_and_prices: [{ size: 0, price: 375 }],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const gradeLabels = {
|
|
95
|
+
soft: "Worth exploring",
|
|
96
|
+
bands: {
|
|
97
|
+
exceptional: "Exceptional",
|
|
98
|
+
great: "Great",
|
|
99
|
+
good: "Good",
|
|
100
|
+
decent: "Decent",
|
|
101
|
+
weak: "Weak",
|
|
102
|
+
},
|
|
103
|
+
aria: (value: string) => `Grade ${value} out of 10`,
|
|
104
|
+
capped: "Capped for low confidence",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The venue line on a cross-venue list (the map's Dishes and Drinks): the place
|
|
109
|
+
* in full ink with its grade, then its neighbourhood and whether it is open,
|
|
110
|
+
* muted. With `emphasis="praise"` the price steps back and the praise leads.
|
|
111
|
+
*/
|
|
112
|
+
export const VenueLine: Story = {
|
|
113
|
+
render: (args) => (
|
|
114
|
+
<div className="divide-y divide-border">
|
|
115
|
+
<MenuItem
|
|
116
|
+
{...args}
|
|
117
|
+
item={brisket}
|
|
118
|
+
countKind="praise"
|
|
119
|
+
labels={praiseLabels}
|
|
120
|
+
emphasis="praise"
|
|
121
|
+
subtitle="“Carnea a fost foarte fragedă și avea un gust intens de vită.”"
|
|
122
|
+
venueName="Texas Grill"
|
|
123
|
+
venueGrade={{
|
|
124
|
+
grade: { overall: 8.7, show_number: true, capped: false, confidence: "high" },
|
|
125
|
+
labels: gradeLabels,
|
|
126
|
+
}}
|
|
127
|
+
venueDetail="Centru"
|
|
128
|
+
venueStatus={{ tone: "open", label: "Închide la 23:00" }}
|
|
129
|
+
/>
|
|
130
|
+
<MenuItem
|
|
131
|
+
{...args}
|
|
132
|
+
item={{ ...brisket, id: "i3", name: "Ramen Tori Paitan", mentions: 58 }}
|
|
133
|
+
countKind="praise"
|
|
134
|
+
labels={praiseLabels}
|
|
135
|
+
emphasis="praise"
|
|
136
|
+
venueName="YUSO"
|
|
137
|
+
venueGrade={{
|
|
138
|
+
grade: { overall: 9.2, show_number: true, capped: false, confidence: "high" },
|
|
139
|
+
labels: gradeLabels,
|
|
140
|
+
}}
|
|
141
|
+
venueDetail="Mărăști"
|
|
142
|
+
venueStatus={{ tone: "closed", label: "Deschide la 12:00" }}
|
|
143
|
+
/>
|
|
144
|
+
<MenuItem
|
|
145
|
+
{...args}
|
|
146
|
+
item={{ ...brisket, id: "i4", name: "Cannoli Siciliani", mentions: 12 }}
|
|
147
|
+
countKind="praise"
|
|
148
|
+
labels={praiseLabels}
|
|
149
|
+
emphasis="praise"
|
|
150
|
+
venueName="Cannoleria Cluj-Napoca Restaurant & Pasticceria"
|
|
151
|
+
venueGrade={{
|
|
152
|
+
grade: { overall: 7.4, show_number: true, capped: false, confidence: "medium" },
|
|
153
|
+
labels: gradeLabels,
|
|
154
|
+
}}
|
|
155
|
+
venueDetail="Zorilor"
|
|
156
|
+
venueStatus={{ tone: "open", label: "Închide la 22:00" }}
|
|
157
|
+
/>
|
|
158
|
+
{/* A row whose name is the place (the map's Drinks list): the grade goes
|
|
159
|
+
with the name, and the venue line says what the place is praised for. */}
|
|
160
|
+
<MenuItem
|
|
161
|
+
{...args}
|
|
162
|
+
item={{ id: "f1", slug: "the-soviet", name: "The Soviet", kind: "drink", mentions: 271 }}
|
|
163
|
+
countKind="praise"
|
|
164
|
+
labels={praiseLabels}
|
|
165
|
+
emphasis="praise"
|
|
166
|
+
subtitle="“A very nice place to drink very good cocktails with some unusual names.”"
|
|
167
|
+
nameGrade={{
|
|
168
|
+
grade: { overall: 9.2, show_number: true, capped: false, confidence: "high" },
|
|
169
|
+
labels: gradeLabels,
|
|
170
|
+
}}
|
|
171
|
+
venueName="Praised for cocktails"
|
|
172
|
+
venueDetail="Centru"
|
|
173
|
+
venueStatus={{ tone: "open", label: "Închide la 02:00" }}
|
|
174
|
+
/>
|
|
175
|
+
<MenuItem
|
|
176
|
+
{...args}
|
|
177
|
+
item={{ ...brisket, id: "i5", name: "Papanași Cremoși", mentions: 9 }}
|
|
178
|
+
countKind="praise"
|
|
179
|
+
labels={praiseLabels}
|
|
180
|
+
emphasis="praise"
|
|
181
|
+
venueName="Madrugada"
|
|
182
|
+
venueGrade={{
|
|
183
|
+
grade: { overall: 8.1, show_number: false, capped: false, confidence: "low" },
|
|
184
|
+
labels: gradeLabels,
|
|
185
|
+
}}
|
|
186
|
+
venueDetail="Centru"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
),
|
|
190
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import type { GradeAtomLabels } from "./grade-atom";
|
|
3
4
|
import { MentionSignal, MenuItem, type MenuItemLabels } from "./menu-item";
|
|
4
5
|
import type { MenuItemPreview } from "./menu-types";
|
|
5
6
|
|
|
@@ -109,3 +110,124 @@ describe("MentionSignal count glyph", () => {
|
|
|
109
110
|
);
|
|
110
111
|
});
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
// Where reviews' praise is the point (the map's lists), the price is a fact a
|
|
115
|
+
// person checks, not the row's headline: it steps down to secondary text and
|
|
116
|
+
// the praise count keeps the bold accent (HIG review, 2026-11-08).
|
|
117
|
+
describe("MenuItem emphasis", () => {
|
|
118
|
+
it("keeps the price bold by default", () => {
|
|
119
|
+
render(<MenuItem item={wine} labels={labels} formatPrice={String} onOpen={() => {}} />);
|
|
120
|
+
const price = screen.getByText("169");
|
|
121
|
+
expect(price.className).toContain("font-bold");
|
|
122
|
+
expect(price.className).toContain("text-foreground");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("sets the price as secondary text when praise leads", () => {
|
|
126
|
+
render(
|
|
127
|
+
<MenuItem item={wine} labels={labels} formatPrice={String} emphasis="praise" onOpen={() => {}} />
|
|
128
|
+
);
|
|
129
|
+
const price = screen.getByText("169");
|
|
130
|
+
expect(price.className).not.toContain("font-bold");
|
|
131
|
+
expect(price.className).toContain("text-muted-foreground");
|
|
132
|
+
expect(price.className).toContain("text-[13px]");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// On cross-venue surfaces the venue line can say whether the place is open:
|
|
137
|
+
// a dot (success green open, muted closed) and the words. The words carry the
|
|
138
|
+
// meaning; the dot is the glance. A long venue gives way, the status never.
|
|
139
|
+
describe("MenuItem venue status", () => {
|
|
140
|
+
const row = (venueStatus?: { tone: "open" | "closed"; label: string }) =>
|
|
141
|
+
render(
|
|
142
|
+
<MenuItem
|
|
143
|
+
item={wine}
|
|
144
|
+
labels={labels}
|
|
145
|
+
formatPrice={String}
|
|
146
|
+
venueName="Texas Grill · Centru"
|
|
147
|
+
venueStatus={venueStatus}
|
|
148
|
+
onOpen={() => {}}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
it("says an open place closes, with a green dot", () => {
|
|
153
|
+
const { container } = row({ tone: "open", label: "Closes at 23:00" });
|
|
154
|
+
const status = container.querySelector('[data-slot="venue-status"]')!;
|
|
155
|
+
expect(status.textContent).toBe("Closes at 23:00");
|
|
156
|
+
expect(status.getAttribute("data-tone")).toBe("open");
|
|
157
|
+
expect(status.querySelector("[aria-hidden]")!.className).toContain("bg-success");
|
|
158
|
+
expect(status.className).toContain("shrink-0");
|
|
159
|
+
expect(screen.getByText("Texas Grill · Centru").className).toContain("min-w-0");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("says a closed place opens, with a muted dot", () => {
|
|
163
|
+
const { container } = row({ tone: "closed", label: "Opens at 09:00" });
|
|
164
|
+
const dot = container.querySelector('[data-slot="venue-status"] [aria-hidden]')!;
|
|
165
|
+
expect(dot.className).toContain("bg-muted-foreground");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("draws no status when none is given", () => {
|
|
169
|
+
const { container } = row();
|
|
170
|
+
expect(container.querySelector('[data-slot="venue-status"]')).toBeNull();
|
|
171
|
+
expect(screen.getByText("Texas Grill · Centru")).toBeTruthy();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// The venue line has its own order (Adrian, 2026-11-09): the place in full
|
|
176
|
+
// ink, its grade beside it, then its neighbourhood and hours, muted. Before,
|
|
177
|
+
// all of it was one grey string and the place weighed what its details did.
|
|
178
|
+
describe("MenuItem venue line", () => {
|
|
179
|
+
const grade = { overall: 8.7, show_number: true, capped: false, confidence: "high" } as const;
|
|
180
|
+
const gradeLabels: GradeAtomLabels = {
|
|
181
|
+
soft: "Worth exploring",
|
|
182
|
+
bands: { exceptional: "Exceptional", great: "Great", good: "Good", decent: "Decent", weak: "Weak" },
|
|
183
|
+
aria: (value) => `Grade ${value} out of 10`,
|
|
184
|
+
capped: "Capped",
|
|
185
|
+
};
|
|
186
|
+
const venue = (props: Partial<Parameters<typeof MenuItem>[0]>) =>
|
|
187
|
+
render(
|
|
188
|
+
<MenuItem
|
|
189
|
+
item={wine}
|
|
190
|
+
labels={labels}
|
|
191
|
+
formatPrice={String}
|
|
192
|
+
venueName="Texas Grill"
|
|
193
|
+
onOpen={() => {}}
|
|
194
|
+
{...props}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
it("sets the place in full ink and its neighbourhood muted after it", () => {
|
|
199
|
+
venue({ venueDetail: "Centru" });
|
|
200
|
+
const place = screen.getByText("Texas Grill");
|
|
201
|
+
expect(place.className).toContain("text-foreground");
|
|
202
|
+
expect(place.className).toContain("font-semibold");
|
|
203
|
+
const detail = screen.getByText("Centru");
|
|
204
|
+
expect(detail.className).toContain("text-muted-foreground");
|
|
205
|
+
expect(detail.className).toContain("font-normal");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("puts the place's grade beside its name", () => {
|
|
209
|
+
venue({ venueGrade: { grade, labels: gradeLabels } });
|
|
210
|
+
// The inline size: the pill fits the line instead of making it taller.
|
|
211
|
+
expect(screen.getByLabelText("Grade 8.7 out of 10").className).toContain("h-[18px]");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// The map's Drinks rows lead with the bar; their venue line says what it
|
|
215
|
+
// is praised for. The grade goes with the bar's name, never beside
|
|
216
|
+
// "Praised for cocktails" (Adrian, 9 Nov).
|
|
217
|
+
it("can put the grade beside the row's name when the name is the place", () => {
|
|
218
|
+
const { container } = venue({
|
|
219
|
+
item: { ...wine, name: "The Soviet" },
|
|
220
|
+
venueName: "Praised for cocktails",
|
|
221
|
+
nameGrade: { grade, labels: gradeLabels },
|
|
222
|
+
});
|
|
223
|
+
const pill = screen.getByLabelText("Grade 8.7 out of 10");
|
|
224
|
+
expect(container.querySelector("h3")!.parentElement!.contains(pill)).toBe(true);
|
|
225
|
+
expect(screen.getByText("Praised for cocktails").parentElement!.contains(pill)).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("draws no grade without a number to show", () => {
|
|
229
|
+
venue({ venueGrade: { grade: { ...grade, show_number: false }, labels: gradeLabels } });
|
|
230
|
+
expect(screen.queryByLabelText(/^Grade/)).toBeNull();
|
|
231
|
+
expect(screen.queryByText("Worth exploring")).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
2
|
import { MessageSquareQuote, Star, ThumbsUp } from "lucide-react";
|
|
3
|
+
import { showsGradeAtom } from "../lib/grade";
|
|
3
4
|
import { cn } from "../lib/utils";
|
|
5
|
+
import { GradeAtom, type GradeAtomLabels } from "./grade-atom";
|
|
6
|
+
import type { PlaceGrade } from "./place-types";
|
|
4
7
|
import { SignatureChip } from "./signature-chip";
|
|
5
8
|
import type { MenuItemPreview, MenuLinkProps, MenuLinkComponent } from "./menu-types";
|
|
6
9
|
|
|
@@ -111,6 +114,14 @@ export type MenuItemProps = {
|
|
|
111
114
|
* Defaults to `"mentions"`. Keep `labels.mentions` in step with it.
|
|
112
115
|
*/
|
|
113
116
|
countKind?: MenuItemCountKind;
|
|
117
|
+
/**
|
|
118
|
+
* What the meta row leads with. `"price"` (default): the price is bold, the
|
|
119
|
+
* count's peer at the other end. `"praise"`: on surfaces where reviews'
|
|
120
|
+
* praise is the point (the map's lists), the price steps down to secondary
|
|
121
|
+
* text, a fact to check rather than the row's headline, and the praise count
|
|
122
|
+
* keeps the bold accent.
|
|
123
|
+
*/
|
|
124
|
+
emphasis?: "price" | "praise";
|
|
114
125
|
/**
|
|
115
126
|
* An outside aggregate score for the item (a wine's Vivino rating, a beer's
|
|
116
127
|
* Untappd one), shown as a star and the score beside the mention count.
|
|
@@ -134,6 +145,30 @@ export type MenuItemProps = {
|
|
|
134
145
|
* default.
|
|
135
146
|
*/
|
|
136
147
|
venueName?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Whether the venue is open, on the venue line after its name: a dot
|
|
150
|
+
* (`open` in the success green, `closed` muted) and the caller's words, e.g.
|
|
151
|
+
* "Closes at 23:00" / "Opens at 09:00". Only with `venueName`.
|
|
152
|
+
*/
|
|
153
|
+
venueStatus?: { tone: "open" | "closed"; label: string };
|
|
154
|
+
/**
|
|
155
|
+
* A short detail about the venue, muted after its name and grade: its
|
|
156
|
+
* neighbourhood, say. Only with `venueName`.
|
|
157
|
+
*/
|
|
158
|
+
venueDetail?: string;
|
|
159
|
+
/**
|
|
160
|
+
* The venue's grade, drawn as `<GradeAtom size="2xs">` (inline, 18px) beside its name. Only
|
|
161
|
+
* a grade with a number shows: the xs atom draws nothing for the soft
|
|
162
|
+
* "worth exploring" state (see `showsGradeAtom`).
|
|
163
|
+
*/
|
|
164
|
+
venueGrade?: { grade: PlaceGrade; labels: GradeAtomLabels };
|
|
165
|
+
/**
|
|
166
|
+
* A grade beside the row's NAME, for rows where the name is the place (the
|
|
167
|
+
* map's Drinks list leads with the bar, and its venue line says what the
|
|
168
|
+
* bar is praised for). Use it instead of `venueGrade` there, so the grade
|
|
169
|
+
* never sits beside "Praised for cocktails". Same rule: only with a number.
|
|
170
|
+
*/
|
|
171
|
+
nameGrade?: { grade: PlaceGrade; labels: GradeAtomLabels };
|
|
137
172
|
/**
|
|
138
173
|
* Explicit subtitle, in italic, in place of the description. Used where the
|
|
139
174
|
* reviewer's reason IS the row's content rather than the payoff for tapping.
|
|
@@ -259,10 +294,15 @@ export const MenuItem = memo(function MenuItem({
|
|
|
259
294
|
onSignatureEvidence,
|
|
260
295
|
onMentions,
|
|
261
296
|
countKind = "mentions",
|
|
297
|
+
emphasis = "price",
|
|
262
298
|
externalRating,
|
|
263
299
|
formatRating = (score) => score.toFixed(1),
|
|
264
300
|
markGradient,
|
|
265
301
|
venueName,
|
|
302
|
+
venueStatus,
|
|
303
|
+
venueDetail,
|
|
304
|
+
venueGrade,
|
|
305
|
+
nameGrade,
|
|
266
306
|
subtitle,
|
|
267
307
|
href,
|
|
268
308
|
linkLabel,
|
|
@@ -390,6 +430,15 @@ export const MenuItem = memo(function MenuItem({
|
|
|
390
430
|
<div className="min-w-0 flex-1">
|
|
391
431
|
<div className="flex items-start gap-1.5">
|
|
392
432
|
<h3 className="text-[15px] font-semibold leading-snug text-foreground">{item.name}</h3>
|
|
433
|
+
{nameGrade && showsGradeAtom(nameGrade.grade, "2xs") && (
|
|
434
|
+
<GradeAtom
|
|
435
|
+
grade={nameGrade.grade}
|
|
436
|
+
labels={nameGrade.labels}
|
|
437
|
+
size="2xs"
|
|
438
|
+
hideDescriptor
|
|
439
|
+
className="shrink-0 self-center"
|
|
440
|
+
/>
|
|
441
|
+
)}
|
|
393
442
|
{showSignatureBadge && (
|
|
394
443
|
<SignatureChip
|
|
395
444
|
label={signatureTier === "weak" ? labels.mentioned : labels.signature}
|
|
@@ -399,13 +448,56 @@ export const MenuItem = memo(function MenuItem({
|
|
|
399
448
|
)}
|
|
400
449
|
</div>
|
|
401
450
|
|
|
402
|
-
{/* Venue attribution — cross-venue surfaces only.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
451
|
+
{/* Venue attribution — cross-venue surfaces only. The place in full
|
|
452
|
+
ink, semibold at 12px, is a different KIND of fact from the muted
|
|
453
|
+
description below, and outranks its own details: the grade beside
|
|
454
|
+
it, then the neighbourhood and the hours, muted and regular. Before
|
|
455
|
+
0.8.56 the line was one grey string, and the place weighed what its
|
|
456
|
+
details did. No pin glyph: repeated down a list it is visual lint,
|
|
457
|
+
and a pin reads "address" when the fact is "kitchen". */}
|
|
407
458
|
{venueName && (
|
|
408
|
-
<p className="mt-0.5
|
|
459
|
+
<p className="mt-0.5 flex min-w-0 items-center gap-1.5 text-[12px]">
|
|
460
|
+
<span className="min-w-0 truncate font-semibold text-foreground">{venueName}</span>
|
|
461
|
+
{venueGrade && showsGradeAtom(venueGrade.grade, "2xs") && (
|
|
462
|
+
<GradeAtom
|
|
463
|
+
grade={venueGrade.grade}
|
|
464
|
+
labels={venueGrade.labels}
|
|
465
|
+
size="2xs"
|
|
466
|
+
hideDescriptor
|
|
467
|
+
className="shrink-0"
|
|
468
|
+
/>
|
|
469
|
+
)}
|
|
470
|
+
{venueDetail && (
|
|
471
|
+
<>
|
|
472
|
+
<span aria-hidden className="text-muted-foreground">
|
|
473
|
+
·
|
|
474
|
+
</span>
|
|
475
|
+
{/* Short by nature (a neighbourhood): it keeps its width, and
|
|
476
|
+
a long place name gives way instead of leaving "Z…". */}
|
|
477
|
+
<span className="shrink-0 font-normal text-muted-foreground">{venueDetail}</span>
|
|
478
|
+
</>
|
|
479
|
+
)}
|
|
480
|
+
{/* Whether the place is open: a dot for the glance, the words for
|
|
481
|
+
the meaning. Green is the success token, closed stays muted:
|
|
482
|
+
at 10 a.m. most bars are closed, and a list of red rows would
|
|
483
|
+
read as an alarm. A long venue gives way; the status never. */}
|
|
484
|
+
{venueStatus && (
|
|
485
|
+
<span
|
|
486
|
+
data-slot="venue-status"
|
|
487
|
+
data-tone={venueStatus.tone}
|
|
488
|
+
className="inline-flex shrink-0 items-center gap-1 font-normal text-muted-foreground"
|
|
489
|
+
>
|
|
490
|
+
<span
|
|
491
|
+
aria-hidden
|
|
492
|
+
className={cn(
|
|
493
|
+
"size-1.5 shrink-0 rounded-full",
|
|
494
|
+
venueStatus.tone === "open" ? "bg-success" : "bg-muted-foreground"
|
|
495
|
+
)}
|
|
496
|
+
/>
|
|
497
|
+
{venueStatus.label}
|
|
498
|
+
</span>
|
|
499
|
+
)}
|
|
500
|
+
</p>
|
|
409
501
|
)}
|
|
410
502
|
|
|
411
503
|
{/* The description gives up a line to the venue rather than the row
|
|
@@ -448,7 +540,14 @@ export const MenuItem = memo(function MenuItem({
|
|
|
448
540
|
<div className="flex min-w-0 items-baseline gap-x-2">
|
|
449
541
|
{minPrice != null && (
|
|
450
542
|
<span className="flex items-baseline gap-1">
|
|
451
|
-
<span
|
|
543
|
+
<span
|
|
544
|
+
className={cn(
|
|
545
|
+
"tabular-nums",
|
|
546
|
+
emphasis === "praise"
|
|
547
|
+
? "text-[13px] font-medium text-muted-foreground"
|
|
548
|
+
: "text-[15px] font-bold text-foreground"
|
|
549
|
+
)}
|
|
550
|
+
>
|
|
452
551
|
{isPriceRange
|
|
453
552
|
? `${formatPrice(minPrice)}–${formatPrice(maxPrice!)}`
|
|
454
553
|
: formatPrice(minPrice)}
|
|
@@ -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
|
)}
|
package/src/lib/grade.ts
CHANGED
|
@@ -58,10 +58,10 @@ export function shouldShowGradeNumber(grade: { show_number?: boolean }): boolean
|
|
|
58
58
|
*/
|
|
59
59
|
export function showsGradeAtom<T extends { overall?: number | null; show_number?: boolean }>(
|
|
60
60
|
grade: T | null | undefined,
|
|
61
|
-
size: "xs" | "sm" | "md" | "lg"
|
|
61
|
+
size: "2xs" | "xs" | "sm" | "md" | "lg"
|
|
62
62
|
): grade is T {
|
|
63
63
|
if (grade?.overall == null) return false;
|
|
64
|
-
return size !== "xs" || shouldShowGradeNumber(grade);
|
|
64
|
+
return (size !== "xs" && size !== "2xs") || shouldShowGradeNumber(grade);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** Tone for a single 0–100 dimension score, driving the page dimension-bar
|