@bearmenu/ui 0.8.9 → 0.8.10
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-24RFI4RQ.js +167 -0
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/{chunk-AXV5O2IZ.js → chunk-AKQ57NXY.js} +3 -3
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/chunk-SY6RV476.js +48 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/command.d.ts +7 -7
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +75 -38
- package/dist/components/concept-compare.js +209 -133
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.js +1 -1
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/glass-ground.d.ts +66 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/hub-surface.js +11 -1
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +187 -0
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +99 -0
- package/dist/components/menu-family-block.js +155 -0
- package/dist/components/menu-family-card.d.ts +33 -27
- package/dist/components/menu-family-card.js +43 -85
- package/dist/components/menu-field.js +92 -2
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -0
- package/dist/components/menu-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +37 -2
- package/dist/components/menu-mocks.js +152 -27
- package/dist/components/menu-types.d.ts +182 -6
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +6 -6
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +2 -2
- package/dist/components/place-types.d.ts +2 -2
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/snap-rail.d.ts +24 -1
- package/dist/components/snap-rail.js +3 -109
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/text.d.ts +3 -3
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +3 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +122 -54
- package/src/components/concept-compare.tsx +363 -196
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +129 -0
- package/src/components/hub-surface.tsx +11 -1
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +322 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +314 -0
- package/src/components/menu-family-card.stories.tsx +43 -35
- package/src/components/menu-family-card.tsx +122 -142
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -0
- package/src/components/menu-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +198 -23
- package/src/components/menu-types.ts +189 -6
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +6 -5
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +2 -2
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/snap-rail.tsx +58 -15
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -0
- package/src/components/venue-grade-dimensions.tsx +2 -2
- package/dist/chunk-THOVPZNN.js +0 -37
- package/dist/chunk-YS6BHKZO.js +0 -92
- package/dist/components/ranked-venue-list.d.ts +0 -49
- package/dist/components/ranked-venue-list.js +0 -87
- package/dist/components/signature-line-list.d.ts +0 -45
- package/dist/components/signature-line-list.js +0 -99
- package/dist/components/venue-busyness-strip.d.ts +0 -35
- package/dist/components/venue-busyness-strip.js +0 -37
- package/dist/components/venue-signature-block.d.ts +0 -39
- package/dist/components/venue-signature-block.js +0 -6
- package/dist/components/venue-spotlight.d.ts +0 -74
- package/dist/components/venue-spotlight.js +0 -138
- package/src/components/ranked-venue-list.stories.tsx +0 -80
- package/src/components/ranked-venue-list.tsx +0 -177
- package/src/components/signature-line-list.stories.tsx +0 -85
- package/src/components/signature-line-list.tsx +0 -136
- package/src/components/venue-busyness-strip.stories.tsx +0 -66
- package/src/components/venue-busyness-strip.tsx +0 -64
- package/src/components/venue-signature-block.stories.tsx +0 -68
- package/src/components/venue-signature-block.tsx +0 -71
- package/src/components/venue-spotlight.stories.tsx +0 -141
- package/src/components/venue-spotlight.tsx +0 -236
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuLinkComponent } from './menu-types.js';
|
|
3
|
+
import 'react';
|
|
4
|
+
import './place-types.js';
|
|
5
|
+
|
|
6
|
+
type TopCrossLinkProps = {
|
|
7
|
+
/**
|
|
8
|
+
* `row` (default) — the way OUT of the ranking, full width under the list.
|
|
9
|
+
* `panel` — the way IN, stacked, for a narrow column beside a browse list.
|
|
10
|
+
*/
|
|
11
|
+
variant?: "row" | "panel";
|
|
12
|
+
/** e.g. "RANKED". `panel` only; drawn over the title with `icon` inline. */
|
|
13
|
+
kicker?: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
/**
|
|
17
|
+
* The link's text. Prefer the destination's own headline over a generic
|
|
18
|
+
* "see more": the reader lands on a page whose `<h1>` is the string they
|
|
19
|
+
* tapped, and it is the anchor text the destination actually wants.
|
|
20
|
+
*/
|
|
21
|
+
cta: string;
|
|
22
|
+
href: string;
|
|
23
|
+
/** Finished CSS gradient for the ground. `panel` only. */
|
|
24
|
+
gradient?: string;
|
|
25
|
+
/** Names the DESTINATION. Defaults to `LayoutList` on `row`, `Trophy` on `panel`. */
|
|
26
|
+
icon?: React.ReactNode;
|
|
27
|
+
/** The title's element. `b` by default on `row`, `h2` on `panel`. */
|
|
28
|
+
as?: "h2" | "h3" | "b";
|
|
29
|
+
linkComponent?: MenuLinkComponent;
|
|
30
|
+
className?: string;
|
|
31
|
+
};
|
|
32
|
+
declare function TopCrossLink({ variant, kicker, title, description, cta, href, gradient, icon, as, linkComponent: LinkComponent, className, }: TopCrossLinkProps): react_jsx_runtime.JSX.Element;
|
|
33
|
+
|
|
34
|
+
export { TopCrossLink, type TopCrossLinkProps };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { ArrowRight, Trophy, LayoutList } from 'lucide-react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function DefaultLink(_a) {
|
|
7
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
8
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
9
|
+
}
|
|
10
|
+
var CTA = cn(
|
|
11
|
+
"rounded-xl bg-accent font-bold text-accent-foreground",
|
|
12
|
+
"transition-opacity duration-150 hover:opacity-90",
|
|
13
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
14
|
+
"motion-reduce:transition-none"
|
|
15
|
+
);
|
|
16
|
+
function TopCrossLink({
|
|
17
|
+
variant = "row",
|
|
18
|
+
kicker,
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
cta,
|
|
22
|
+
href,
|
|
23
|
+
gradient,
|
|
24
|
+
icon,
|
|
25
|
+
as,
|
|
26
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
27
|
+
className
|
|
28
|
+
}) {
|
|
29
|
+
const isPanel = variant === "panel";
|
|
30
|
+
const Heading = as != null ? as : isPanel ? "h2" : "b";
|
|
31
|
+
const glyph = icon != null ? icon : isPanel ? /* @__PURE__ */ jsx(Trophy, {}) : /* @__PURE__ */ jsx(LayoutList, {});
|
|
32
|
+
if (!isPanel) {
|
|
33
|
+
return /* @__PURE__ */ jsxs(
|
|
34
|
+
"div",
|
|
35
|
+
{
|
|
36
|
+
className: cn(
|
|
37
|
+
"flex flex-col gap-3 rounded-[18px] border border-border bg-card p-4",
|
|
38
|
+
"lg:flex-row lg:items-center lg:gap-5 lg:p-5",
|
|
39
|
+
className
|
|
40
|
+
),
|
|
41
|
+
children: [
|
|
42
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "shrink-0 text-muted-foreground [&>svg]:h-5 [&>svg]:w-5", children: glyph }),
|
|
43
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
44
|
+
/* @__PURE__ */ jsx(Heading, { className: "block text-[15px] font-bold tracking-[-0.01em]", children: title }),
|
|
45
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-[13px] leading-relaxed text-muted-foreground", children: description })
|
|
46
|
+
] }),
|
|
47
|
+
/* @__PURE__ */ jsxs(
|
|
48
|
+
LinkComponent,
|
|
49
|
+
{
|
|
50
|
+
href,
|
|
51
|
+
className: cn(
|
|
52
|
+
CTA,
|
|
53
|
+
"inline-flex min-h-11 shrink-0 items-center justify-center gap-2 px-5 py-2.5 text-sm"
|
|
54
|
+
),
|
|
55
|
+
children: [
|
|
56
|
+
cta,
|
|
57
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-4 w-4 shrink-0", "aria-hidden": true })
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return /* @__PURE__ */ jsxs(
|
|
66
|
+
"div",
|
|
67
|
+
{
|
|
68
|
+
className: cn(
|
|
69
|
+
"relative overflow-hidden rounded-[18px] border border-border bg-card shadow-[var(--shadow-card)]",
|
|
70
|
+
className
|
|
71
|
+
),
|
|
72
|
+
children: [
|
|
73
|
+
gradient && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-0", style: { backgroundImage: gradient } }),
|
|
74
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-[1] p-4 lg:p-5", children: [
|
|
75
|
+
kicker && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-[10.5px] font-extrabold uppercase tracking-[0.13em] text-muted-foreground", children: [
|
|
76
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "[&>svg]:h-3 [&>svg]:w-3 [&>svg]:shrink-0", children: glyph }),
|
|
77
|
+
kicker
|
|
78
|
+
] }),
|
|
79
|
+
/* @__PURE__ */ jsx(Heading, { className: "mt-2 block font-serif text-[20px] font-bold leading-[1.12] tracking-[-0.02em] text-foreground lg:text-[21px]", children: title }),
|
|
80
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1.5 text-[13px] leading-relaxed text-muted-foreground", children: description }),
|
|
81
|
+
/* @__PURE__ */ jsxs(
|
|
82
|
+
LinkComponent,
|
|
83
|
+
{
|
|
84
|
+
href,
|
|
85
|
+
className: cn(
|
|
86
|
+
CTA,
|
|
87
|
+
"mt-4 flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-[13.5px] leading-snug"
|
|
88
|
+
),
|
|
89
|
+
children: [
|
|
90
|
+
/* @__PURE__ */ jsx("span", { children: cta }),
|
|
91
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-4 w-4 shrink-0", "aria-hidden": true })
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
] })
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { TopCrossLink };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuItemLabels } from './menu-item.js';
|
|
3
|
+
import { MenuItemPreview, MenuLinkComponent } from './menu-types.js';
|
|
4
|
+
import { TopRankEntry } from './top-types.js';
|
|
5
|
+
import 'react';
|
|
6
|
+
import './place-types.js';
|
|
7
|
+
|
|
8
|
+
type TopRankingEntryLabels = {
|
|
9
|
+
/** Heading over the menu rows when the venue's own group is unknown. */
|
|
10
|
+
signature: string;
|
|
11
|
+
/** Accessible name for the dish link. */
|
|
12
|
+
openDish: (name: string) => string;
|
|
13
|
+
/** Passed through to the production `<MenuItem>`. */
|
|
14
|
+
item: MenuItemLabels;
|
|
15
|
+
/**
|
|
16
|
+
* Opens an item's drawer. The DS owns no routing, so the caller composes it.
|
|
17
|
+
*
|
|
18
|
+
* Takes the ITEM, not its slug: a menu row is a dish or a drink and the two
|
|
19
|
+
* resolve through different endpoints, so the caller has to see `kind` to
|
|
20
|
+
* build the right link. `undefined` leaves the row inert, which is the
|
|
21
|
+
* text-only must-try mention with no live menu row behind it.
|
|
22
|
+
*/
|
|
23
|
+
itemHref: (item: MenuItemPreview) => string | undefined;
|
|
24
|
+
};
|
|
25
|
+
type TopRankingEntryProps = {
|
|
26
|
+
entry: TopRankEntry;
|
|
27
|
+
labels: TopRankingEntryLabels;
|
|
28
|
+
formatPrice: (amount: number) => string;
|
|
29
|
+
formatCount?: (value: number) => string;
|
|
30
|
+
/** The category's hue, for the menu rows' colour mark. */
|
|
31
|
+
markGradient?: string;
|
|
32
|
+
linkComponent?: MenuLinkComponent;
|
|
33
|
+
className?: string;
|
|
34
|
+
};
|
|
35
|
+
declare function TopRankingEntry({ entry, labels, formatPrice, formatCount, markGradient, linkComponent: LinkComponent, className, }: TopRankingEntryProps): react_jsx_runtime.JSX.Element;
|
|
36
|
+
/**
|
|
37
|
+
* The entry's own loading state, at the entry's footprint.
|
|
38
|
+
*
|
|
39
|
+
* The cover keeps its 3:2 on the phone and its 4:5 at 300px wide on desktop, so
|
|
40
|
+
* the article does not jump when the photographs land — the one reflow a long,
|
|
41
|
+
* scrolled page cannot afford. (This said "4:3 … 300×225" for a while after the
|
|
42
|
+
* cover changed underneath it, which is the failure mode a skeleton in the same
|
|
43
|
+
* file as its component is supposed to prevent.)
|
|
44
|
+
*/
|
|
45
|
+
declare function TopRankingEntrySkeleton({ className }: {
|
|
46
|
+
className?: string;
|
|
47
|
+
}): react_jsx_runtime.JSX.Element;
|
|
48
|
+
/** A run of entries — the ranking's loading state. */
|
|
49
|
+
declare function TopRankingListSkeleton({ rows }: {
|
|
50
|
+
rows?: number;
|
|
51
|
+
}): react_jsx_runtime.JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { TopRankingEntry, type TopRankingEntryLabels, type TopRankingEntryProps, TopRankingEntrySkeleton, TopRankingListSkeleton };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { MenuItem } from '../chunk-TH6W2PWG.js';
|
|
2
|
+
import '../chunk-Q6QH52LJ.js';
|
|
3
|
+
import { Separator } from '../chunk-WSRWFA6K.js';
|
|
4
|
+
import { Skeleton } from '../chunk-QXHMPJ35.js';
|
|
5
|
+
import '../chunk-UKBNQO6Q.js';
|
|
6
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
7
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
8
|
+
import { MapPin, Star, MessageSquareQuote } from 'lucide-react';
|
|
9
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
10
|
+
|
|
11
|
+
function DefaultLink(_a) {
|
|
12
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
13
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
14
|
+
}
|
|
15
|
+
var ENTRY_COVER_SCRIM = "bg-[linear-gradient(180deg,oklch(0.16_0.012_55_/_0)_0px,oklch(0.16_0.012_55_/_0.06)_16px,oklch(0.16_0.012_55_/_0.3)_32px,oklch(0.16_0.012_55_/_0.72)_48px,oklch(0.16_0.012_55_/_0.95)_100%)]";
|
|
16
|
+
function TopRankingEntry({
|
|
17
|
+
entry,
|
|
18
|
+
labels,
|
|
19
|
+
formatPrice,
|
|
20
|
+
formatCount,
|
|
21
|
+
markGradient,
|
|
22
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
23
|
+
className
|
|
24
|
+
}) {
|
|
25
|
+
var _a, _b, _c;
|
|
26
|
+
const initial = ((_b = (_a = entry.name.match(new RegExp("\\p{L}", "u"))) == null ? void 0 : _a[0]) != null ? _b : "").toLocaleUpperCase();
|
|
27
|
+
return /* @__PURE__ */ jsxs(
|
|
28
|
+
"article",
|
|
29
|
+
{
|
|
30
|
+
id: entry.slug,
|
|
31
|
+
className: cn("grid grid-cols-1 gap-0 py-6 lg:flex lg:flex-row lg:gap-7", className),
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx(
|
|
34
|
+
LinkComponent,
|
|
35
|
+
{
|
|
36
|
+
href: entry.href,
|
|
37
|
+
className: "col-start-1 row-start-1 block shrink-0 overflow-hidden rounded-xl transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 motion-reduce:transition-none lg:sticky lg:top-4 lg:w-[300px] lg:self-start",
|
|
38
|
+
children: /* @__PURE__ */ jsx("span", { className: "relative block aspect-[3/2] w-full bg-[oklch(0.21_0.012_55)] lg:aspect-[4/5] lg:bg-[repeating-linear-gradient(135deg,oklch(0.82_0.014_55)_0_10px,oklch(0.87_0.01_58)_10px_20px)]", children: entry.coverUrl ? (
|
|
39
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
40
|
+
/* @__PURE__ */ jsx(
|
|
41
|
+
"img",
|
|
42
|
+
{
|
|
43
|
+
src: entry.coverUrl,
|
|
44
|
+
alt: "",
|
|
45
|
+
loading: "lazy",
|
|
46
|
+
decoding: "async",
|
|
47
|
+
className: "h-full w-full object-cover"
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
) : (
|
|
51
|
+
/* Never a grey box: the warm ground with the venue's initial, the
|
|
52
|
+
rule `MenuFamilyBlock` states. Phone only — desktop keeps its
|
|
53
|
+
stripes. The stripes are a LIGHT surface in both themes, so at
|
|
54
|
+
this size they would be a pale rectangle under white type.
|
|
55
|
+
The ground stays on the parent span, so a 404 on `coverUrl`
|
|
56
|
+
degrades to the same surface for free. */
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
"span",
|
|
59
|
+
{
|
|
60
|
+
"aria-hidden": true,
|
|
61
|
+
className: "absolute inset-0 flex items-center justify-center lg:hidden",
|
|
62
|
+
children: /* @__PURE__ */ jsx("span", { className: "font-serif text-[64px] font-semibold leading-none text-white/25", children: initial })
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
) })
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
/* @__PURE__ */ jsxs("div", { className: "contents lg:block lg:min-w-0 lg:flex-1", children: [
|
|
69
|
+
/* @__PURE__ */ jsxs(
|
|
70
|
+
"div",
|
|
71
|
+
{
|
|
72
|
+
className: cn(
|
|
73
|
+
/* `relative z-[1]`: without it this block's background is a
|
|
74
|
+
non-positioned box's background, which paints in an earlier phase
|
|
75
|
+
than the cover's replaced `<img>` — so the scrim went UNDER the
|
|
76
|
+
photograph and the white type landed on bare image. Positioning
|
|
77
|
+
it lifts the whole block, background included, above the image.
|
|
78
|
+
Harmless at `lg`, where the two no longer overlap. */
|
|
79
|
+
"pointer-events-none relative z-[1] col-start-1 row-start-1 flex flex-wrap items-end gap-x-3 gap-y-0 self-end",
|
|
80
|
+
"rounded-b-xl px-3.5 pb-3.5 pt-12 [text-shadow:0_1px_2px_oklch(0.16_0.012_55_/_0.45)]",
|
|
81
|
+
ENTRY_COVER_SCRIM,
|
|
82
|
+
"lg:pointer-events-auto lg:flex-nowrap lg:items-start lg:gap-3 lg:rounded-none lg:bg-none lg:p-0 lg:[text-shadow:none]"
|
|
83
|
+
),
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsx(
|
|
86
|
+
"span",
|
|
87
|
+
{
|
|
88
|
+
"aria-hidden": true,
|
|
89
|
+
className: "basis-full text-[11px] font-bold leading-none tabular-nums text-white/70 lg:basis-auto lg:font-serif lg:text-[28px] lg:font-semibold lg:text-muted-foreground",
|
|
90
|
+
children: entry.rank
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
94
|
+
/* @__PURE__ */ jsx("h3", { className: "line-clamp-2 text-lg font-bold leading-tight tracking-[-0.015em] text-white lg:line-clamp-none lg:text-foreground", children: /* @__PURE__ */ jsx(
|
|
95
|
+
LinkComponent,
|
|
96
|
+
{
|
|
97
|
+
href: entry.href,
|
|
98
|
+
className: "pointer-events-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white lg:focus-visible:ring-ring lg:hover:text-accent",
|
|
99
|
+
children: entry.name
|
|
100
|
+
}
|
|
101
|
+
) }),
|
|
102
|
+
entry.district && /* @__PURE__ */ jsxs("span", { className: "mt-0.5 flex items-center gap-1 text-[12.5px] text-white/80 lg:text-muted-foreground", children: [
|
|
103
|
+
/* @__PURE__ */ jsx(MapPin, { className: "h-3 w-3 shrink-0", "aria-hidden": true }),
|
|
104
|
+
entry.district
|
|
105
|
+
] })
|
|
106
|
+
] }),
|
|
107
|
+
entry.gradeLabel ? /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1.5 rounded-full bg-white/95 px-2.5 py-1 backdrop-blur-[2px] lg:gap-2 lg:rounded-lg lg:bg-accent-subtle lg:px-2.5 lg:py-1.5 lg:backdrop-blur-none", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("b", { className: "text-[15px] font-extrabold tabular-nums text-[oklch(0.21_0.012_55)] lg:text-accent", children: entry.gradeLabel }),
|
|
109
|
+
entry.tierLabel && /* @__PURE__ */ jsx("span", { className: "text-[11.5px] text-[oklch(0.21_0.012_55)]/70 lg:text-muted-foreground", children: entry.tierLabel })
|
|
110
|
+
] }) : entry.rating !== void 0 && /* The star fallback inverts the same way as the grade — a white
|
|
111
|
+
pill, deliberately quieter than a grade, because a third-party
|
|
112
|
+
rating is weaker evidence and should look it. Filled, since an
|
|
113
|
+
outlined star at this size disappears over photography. */
|
|
114
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 rounded-full bg-white/95 px-2.5 py-1 text-[13px] font-semibold tabular-nums text-[oklch(0.21_0.012_55)] backdrop-blur-[2px] lg:bg-transparent lg:px-0 lg:py-0 lg:text-foreground lg:backdrop-blur-none", children: [
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
Star,
|
|
117
|
+
{
|
|
118
|
+
className: "h-3.5 w-3.5 shrink-0 fill-[oklch(0.21_0.012_55)] lg:fill-none",
|
|
119
|
+
"aria-hidden": true
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
entry.rating
|
|
123
|
+
] })
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
entry.editorial && /* The article's row gap is 0 on the phone now that the identity block
|
|
128
|
+
lives inside the photograph, so the paragraph owns the space under
|
|
129
|
+
it and gives it back at `lg`. */
|
|
130
|
+
/* @__PURE__ */ jsx("p", { className: "mt-4 font-serif text-[17px] italic leading-[1.65] text-muted-foreground [text-wrap:pretty] lg:mt-3", children: entry.editorial }),
|
|
131
|
+
entry.menuItems && entry.menuItems.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
|
|
132
|
+
/* @__PURE__ */ jsx("span", { className: "block pb-1.5 text-[15px] font-bold tracking-[-0.01em]", children: (_c = entry.menuGroupLabel) != null ? _c : labels.signature }),
|
|
133
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
134
|
+
entry.menuItems.map((item) => /* @__PURE__ */ jsx(
|
|
135
|
+
MenuItem,
|
|
136
|
+
{
|
|
137
|
+
item,
|
|
138
|
+
labels: labels.item,
|
|
139
|
+
formatPrice,
|
|
140
|
+
formatCount,
|
|
141
|
+
markGradient,
|
|
142
|
+
href: labels.itemHref(item),
|
|
143
|
+
linkLabel: labels.openDish(item.name),
|
|
144
|
+
linkComponent: LinkComponent
|
|
145
|
+
},
|
|
146
|
+
item.id
|
|
147
|
+
))
|
|
148
|
+
] }),
|
|
149
|
+
entry.evidence && /* @__PURE__ */ jsxs("figure", { className: "mt-4 border-l-[3px] border-border pl-4", children: [
|
|
150
|
+
entry.evidence.quote && /* @__PURE__ */ jsx("blockquote", { className: "text-[13.5px] leading-relaxed text-muted-foreground [text-wrap:pretty]", children: `\u201C${entry.evidence.quote}\u201D` }),
|
|
151
|
+
/* @__PURE__ */ jsxs(
|
|
152
|
+
"figcaption",
|
|
153
|
+
{
|
|
154
|
+
className: cn(
|
|
155
|
+
"flex items-center gap-1.5 text-[11.5px] text-muted-foreground",
|
|
156
|
+
entry.evidence.quote && "mt-1.5"
|
|
157
|
+
),
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsx(MessageSquareQuote, { className: "h-3 w-3 shrink-0 opacity-60", "aria-hidden": true }),
|
|
160
|
+
entry.evidence.count && /* @__PURE__ */ jsx("b", { className: "font-bold tabular-nums text-foreground", children: entry.evidence.count }),
|
|
161
|
+
entry.evidence.sourceLabel
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
),
|
|
165
|
+
entry.evidence.extraQuotes && entry.evidence.extraQuotes.length > 0 && /* @__PURE__ */ jsxs("details", { className: "mt-2", children: [
|
|
166
|
+
/* @__PURE__ */ jsx("summary", { className: "cursor-pointer text-[11.5px] font-semibold text-accent hover:underline", children: entry.evidence.extraLabel }),
|
|
167
|
+
/* @__PURE__ */ jsx("ul", { className: "mt-2 flex flex-col gap-2", children: entry.evidence.extraQuotes.map((quote) => /* @__PURE__ */ jsx(
|
|
168
|
+
"li",
|
|
169
|
+
{
|
|
170
|
+
className: "text-[13px] leading-relaxed text-muted-foreground [text-wrap:pretty]",
|
|
171
|
+
children: `\u201C${quote}\u201D`
|
|
172
|
+
},
|
|
173
|
+
quote
|
|
174
|
+
)) })
|
|
175
|
+
] })
|
|
176
|
+
] })
|
|
177
|
+
] })
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function TopRankingEntrySkeleton({ className }) {
|
|
183
|
+
return /* @__PURE__ */ jsxs(
|
|
184
|
+
"div",
|
|
185
|
+
{
|
|
186
|
+
className: cn("flex flex-col gap-0 py-6 lg:flex-row lg:gap-7", className),
|
|
187
|
+
"aria-hidden": true,
|
|
188
|
+
children: [
|
|
189
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "aspect-[3/2] w-full shrink-0 rounded-xl lg:aspect-[4/5] lg:w-[300px]" }),
|
|
190
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
191
|
+
/* @__PURE__ */ jsxs("div", { className: "hidden items-start gap-3 lg:flex", children: [
|
|
192
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-6" }),
|
|
193
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
|
|
194
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-[220px]" }),
|
|
195
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-[100px]" })
|
|
196
|
+
] }),
|
|
197
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-[110px] rounded-lg" })
|
|
198
|
+
] }),
|
|
199
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-col gap-2 lg:mt-3", children: [
|
|
200
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
|
|
201
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-[92%]" }),
|
|
202
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-[64%]" })
|
|
203
|
+
] }),
|
|
204
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-4 h-[62px] w-full rounded-lg" })
|
|
205
|
+
] })
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
function TopRankingListSkeleton({ rows = 6 }) {
|
|
211
|
+
return /* @__PURE__ */ jsx("div", { "aria-hidden": true, children: Array.from({ length: rows }, (_, index) => /* @__PURE__ */ jsxs("div", { children: [
|
|
212
|
+
index > 0 && /* @__PURE__ */ jsx(Separator, {}),
|
|
213
|
+
/* @__PURE__ */ jsx(TopRankingEntrySkeleton, {})
|
|
214
|
+
] }, index)) });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { TopRankingEntry, TopRankingEntrySkeleton, TopRankingListSkeleton };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuLinkComponent } from './menu-types.js';
|
|
3
|
+
import { TopExit } from './top-types.js';
|
|
4
|
+
import 'react';
|
|
5
|
+
import './place-types.js';
|
|
6
|
+
|
|
7
|
+
type TopRankingHeroProps = {
|
|
8
|
+
/** e.g. "RANKED · CUISINE". */
|
|
9
|
+
kicker?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
/** One paragraph: the size of the pool, and what the order means. */
|
|
12
|
+
lede?: string;
|
|
13
|
+
/** e.g. "Updated 6 September 2026". */
|
|
14
|
+
updatedLabel?: string;
|
|
15
|
+
/** e.g. "247 kitchens considered · 4,318 reviews read". */
|
|
16
|
+
metaLabel?: string;
|
|
17
|
+
exits?: TopExit[];
|
|
18
|
+
/** Finished CSS gradient for the ground. */
|
|
19
|
+
gradient?: string;
|
|
20
|
+
/** Watermark behind the text — the category's icon, resolved by the caller. */
|
|
21
|
+
icon?: React.ReactNode;
|
|
22
|
+
linkComponent?: MenuLinkComponent;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
declare function TopRankingHero({ kicker, title, lede, updatedLabel, metaLabel, exits, gradient, icon, linkComponent: LinkComponent, className, }: TopRankingHeroProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
/**
|
|
27
|
+
* TopMethodNote — how the ranking was computed, in one block.
|
|
28
|
+
*
|
|
29
|
+
* Under the hero rather than at the foot: a reader deciding whether to trust a
|
|
30
|
+
* list decides it before reading the list, not after.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The note's surface, exported so a caller can put the same look on a TRIGGER.
|
|
34
|
+
*
|
|
35
|
+
* On `/top` the banner opens the grade explainer, and that explainer owns its
|
|
36
|
+
* own `<button>` (Radix clones it, so it cannot be handed a ready-made
|
|
37
|
+
* element). The caller therefore needs the surface without the element — hence
|
|
38
|
+
* a class string rather than a second component. `cn` at the call site lets the
|
|
39
|
+
* button's own `inline-flex` lose to the `flex` here.
|
|
40
|
+
*/
|
|
41
|
+
declare const topMethodNoteSurface = "flex w-full items-start gap-3 rounded-[14px] bg-muted p-4 text-left";
|
|
42
|
+
type TopMethodNoteProps = {
|
|
43
|
+
/** The heading fragment, e.g. "How this is ranked." Rendered bold, inline. */
|
|
44
|
+
heading: string;
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
/** Rendered after the prose — a chevron where the note is a disclosure trigger. */
|
|
47
|
+
affordance?: React.ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The note's CONTENT, with no element of its own.
|
|
52
|
+
*
|
|
53
|
+
* Every tag here is phrasing content — spans, not paragraphs — because this
|
|
54
|
+
* renders inside a `<button>` when the note is a trigger, and a `<p>` inside a
|
|
55
|
+
* button is invalid. Keeping one definition of the content means the static
|
|
56
|
+
* note and the interactive one cannot drift apart.
|
|
57
|
+
*/
|
|
58
|
+
declare function TopMethodNoteBody({ heading, children, affordance, }: Omit<TopMethodNoteProps, "className">): react_jsx_runtime.JSX.Element;
|
|
59
|
+
declare function TopMethodNote({ heading, children, affordance, className }: TopMethodNoteProps): react_jsx_runtime.JSX.Element;
|
|
60
|
+
|
|
61
|
+
export { TopMethodNote, TopMethodNoteBody, type TopMethodNoteProps, TopRankingHero, type TopRankingHeroProps, topMethodNoteSurface };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { Trophy, CalendarCheck, LayoutList, ArrowRight, Info } from 'lucide-react';
|
|
4
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function DefaultLink(_a) {
|
|
7
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
8
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
9
|
+
}
|
|
10
|
+
function TopRankingHero({
|
|
11
|
+
kicker,
|
|
12
|
+
title,
|
|
13
|
+
lede,
|
|
14
|
+
updatedLabel,
|
|
15
|
+
metaLabel,
|
|
16
|
+
exits,
|
|
17
|
+
gradient,
|
|
18
|
+
icon,
|
|
19
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
20
|
+
className
|
|
21
|
+
}) {
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"header",
|
|
24
|
+
{
|
|
25
|
+
className: cn(
|
|
26
|
+
"relative overflow-hidden rounded-[18px] px-5 py-6 lg:px-8 lg:py-8",
|
|
27
|
+
!gradient && "bg-muted",
|
|
28
|
+
className
|
|
29
|
+
),
|
|
30
|
+
children: [
|
|
31
|
+
gradient && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-0", style: { backgroundImage: gradient } }),
|
|
32
|
+
icon && /* @__PURE__ */ jsx(
|
|
33
|
+
"span",
|
|
34
|
+
{
|
|
35
|
+
"aria-hidden": true,
|
|
36
|
+
className: "pointer-events-none absolute -right-6 -top-6 opacity-[0.14] [&>svg]:h-[150px] [&>svg]:w-[150px] lg:[&>svg]:h-[190px] lg:[&>svg]:w-[190px]",
|
|
37
|
+
children: icon
|
|
38
|
+
}
|
|
39
|
+
),
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-[1]", children: [
|
|
41
|
+
kicker && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-background/70 px-3 py-1 text-[11px] font-extrabold uppercase tracking-[0.09em] backdrop-blur-[2px]", children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Trophy, { className: "h-3 w-3 shrink-0", "aria-hidden": true }),
|
|
43
|
+
kicker
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ jsx("h1", { className: "mt-3 max-w-[22ch] font-serif text-[30px] font-bold leading-[1.08] tracking-[-0.025em] lg:text-[44px]", children: title }),
|
|
46
|
+
lede && /* @__PURE__ */ jsx("p", { className: "mt-3 max-w-[62ch] text-[14.5px] leading-relaxed opacity-85", children: lede }),
|
|
47
|
+
(updatedLabel || metaLabel) && /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[12.5px]", children: [
|
|
48
|
+
updatedLabel && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 font-semibold", children: [
|
|
49
|
+
/* @__PURE__ */ jsx(CalendarCheck, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
|
|
50
|
+
updatedLabel
|
|
51
|
+
] }),
|
|
52
|
+
updatedLabel && metaLabel && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-1 w-1 rounded-full bg-current opacity-40" }),
|
|
53
|
+
metaLabel && /* @__PURE__ */ jsx("span", { className: "opacity-75", children: metaLabel })
|
|
54
|
+
] }),
|
|
55
|
+
exits && exits.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-5 hidden flex-col gap-2 sm:flex-row lg:flex", children: exits.map((exit, index) => {
|
|
56
|
+
var _a;
|
|
57
|
+
return /* @__PURE__ */ jsxs(
|
|
58
|
+
LinkComponent,
|
|
59
|
+
{
|
|
60
|
+
href: exit.href,
|
|
61
|
+
className: cn(
|
|
62
|
+
"inline-flex h-11 items-center justify-center gap-2 rounded-xl px-4 text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none",
|
|
63
|
+
index === 0 ? "bg-background text-foreground hover:bg-background/90" : "border border-current/25 hover:bg-background/25"
|
|
64
|
+
),
|
|
65
|
+
children: [
|
|
66
|
+
(_a = exit.icon) != null ? _a : /* @__PURE__ */ jsx(LayoutList, { className: "h-4 w-4 shrink-0", "aria-hidden": true }),
|
|
67
|
+
exit.label,
|
|
68
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true })
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
exit.href
|
|
72
|
+
);
|
|
73
|
+
}) })
|
|
74
|
+
] })
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
var topMethodNoteSurface = "flex w-full items-start gap-3 rounded-[14px] bg-muted p-4 text-left";
|
|
80
|
+
function TopMethodNoteBody({
|
|
81
|
+
heading,
|
|
82
|
+
children,
|
|
83
|
+
affordance
|
|
84
|
+
}) {
|
|
85
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
86
|
+
/* @__PURE__ */ jsx(Info, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
|
|
87
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-[13px] leading-relaxed text-muted-foreground", children: [
|
|
88
|
+
/* @__PURE__ */ jsx("b", { className: "font-bold text-foreground", children: heading }),
|
|
89
|
+
" ",
|
|
90
|
+
children
|
|
91
|
+
] }),
|
|
92
|
+
affordance
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
function TopMethodNote({ heading, children, affordance, className }) {
|
|
96
|
+
return /* @__PURE__ */ jsx("aside", { className: cn(topMethodNoteSurface, className), children: /* @__PURE__ */ jsx(TopMethodNoteBody, { heading, affordance, children }) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { TopMethodNote, TopMethodNoteBody, TopRankingHero, topMethodNoteSurface };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TopRankingLegend — the ranking as a place you are inside of, not a table.
|
|
5
|
+
*
|
|
6
|
+
* ## What it stopped being
|
|
7
|
+
*
|
|
8
|
+
* It was a two-column table: rank, name, grade, with a rule under every row.
|
|
9
|
+
* Three things were wrong with that, and they compounded.
|
|
10
|
+
*
|
|
11
|
+
* The **grade column** was the loudest thing in it and the least useful thing
|
|
12
|
+
* for navigating — twenty-five near-identical numbers in accent colour,
|
|
13
|
+
* duplicating a grade shown at full size two inches to the right, and eating
|
|
14
|
+
* the width that was truncating the names. The one field you navigate by was
|
|
15
|
+
* the one being cut. It is gone.
|
|
16
|
+
*
|
|
17
|
+
* The **dividers** ran full-bleed to the column's right edge, so every rule was
|
|
18
|
+
* a small arrow pointing at the article and nothing ever visually ended the
|
|
19
|
+
* aside. Space separates now; contrast is spent on state instead.
|
|
20
|
+
*
|
|
21
|
+
* And there was **no active row at all** — twenty-five identical entries in a
|
|
22
|
+
* component whose entire job is orientation. That is what the observer below
|
|
23
|
+
* is for.
|
|
24
|
+
*
|
|
25
|
+
* ## The rank numeral is the rail
|
|
26
|
+
*
|
|
27
|
+
* The reference this is drawn from uses a status ring down the left edge. A
|
|
28
|
+
* ring is wrong here: it promises completion state we do not track and would
|
|
29
|
+
* never fill. The rank numeral is already present, already tabular, and is
|
|
30
|
+
* itself the thing being ranked — a better rail, for free.
|
|
31
|
+
*
|
|
32
|
+
* ## Grouped, because twenty-five is not a list you can hold
|
|
33
|
+
*
|
|
34
|
+
* By grade band, which is the one honest grouping available: the tiers are
|
|
35
|
+
* already printed on the entries, so the aside is not inventing a taxonomy. It
|
|
36
|
+
* also says something true — that the top few are a different class.
|
|
37
|
+
*
|
|
38
|
+
* The column will still exceed the viewport at twenty-five, and that is
|
|
39
|
+
* accepted rather than solved: an internally-scrolling sidebar is a second page,
|
|
40
|
+
* and grouping plus a live active row makes the overflow navigable.
|
|
41
|
+
*
|
|
42
|
+
* ## The phone keeps its chips
|
|
43
|
+
*
|
|
44
|
+
* Below `lg` this is a horizontal scroller of the flat list, unchanged. None of
|
|
45
|
+
* the above applies to a chip row — there is nothing to group, no column to
|
|
46
|
+
* inset, and the active state is a press away rather than a scroll away.
|
|
47
|
+
*/
|
|
48
|
+
type TopRankingLegendEntry = {
|
|
49
|
+
rank: number;
|
|
50
|
+
slug: string;
|
|
51
|
+
name: string;
|
|
52
|
+
};
|
|
53
|
+
type TopRankingLegendGroup = {
|
|
54
|
+
key: string;
|
|
55
|
+
/** Grade band, e.g. "Exceptional". Letterspaced caps — structure, not content. */
|
|
56
|
+
label: string;
|
|
57
|
+
entries: TopRankingLegendEntry[];
|
|
58
|
+
};
|
|
59
|
+
type TopRankingLegendProps = {
|
|
60
|
+
groups: TopRankingLegendGroup[];
|
|
61
|
+
/** e.g. "The ranking". */
|
|
62
|
+
heading: string;
|
|
63
|
+
/** The size and freshness of the thing, e.g. "25 places · updated September 2026". */
|
|
64
|
+
meta?: string;
|
|
65
|
+
className?: string;
|
|
66
|
+
};
|
|
67
|
+
declare function TopRankingLegend({ groups, heading, meta, className }: TopRankingLegendProps): react_jsx_runtime.JSX.Element;
|
|
68
|
+
|
|
69
|
+
export { TopRankingLegend, type TopRankingLegendEntry, type TopRankingLegendGroup, type TopRankingLegendProps };
|