@bearmenu/ui 0.8.8 → 0.8.9
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-62ES6N32.js +27 -0
- package/dist/chunk-AXV5O2IZ.js +58 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-THOVPZNN.js +37 -0
- package/dist/chunk-YS6BHKZO.js +92 -0
- package/dist/components/alert.d.ts +2 -2
- 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.d.ts +73 -0
- package/dist/components/concept-compare.js +162 -0
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +471 -174
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/event-list-row.js +1 -1
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +20 -0
- package/dist/components/menu-family-card.d.ts +59 -0
- package/dist/components/menu-family-card.js +121 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +3 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- package/dist/components/menu-mocks.d.ts +55 -0
- package/dist/components/menu-mocks.js +379 -0
- package/dist/components/menu-types.d.ts +245 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-set.js +15 -3
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/ranked-venue-list.d.ts +49 -0
- package/dist/components/ranked-venue-list.js +87 -0
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/signature-line-list.d.ts +45 -0
- package/dist/components/signature-line-list.js +99 -0
- package/dist/components/snap-rail.d.ts +24 -4
- package/dist/components/snap-rail.js +66 -8
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +3 -3
- package/dist/components/venue-busyness-strip.d.ts +35 -0
- package/dist/components/venue-busyness-strip.js +37 -0
- package/dist/components/venue-grade-dimensions.d.ts +36 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-signature-block.d.ts +39 -0
- package/dist/components/venue-signature-block.js +6 -0
- package/dist/components/venue-spotlight.d.ts +74 -0
- package/dist/components/venue-spotlight.js +138 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare.stories.tsx +104 -0
- package/src/components/concept-compare.tsx +276 -0
- package/src/components/event-compact-row.tsx +1 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +40 -0
- package/src/components/menu-family-card.stories.tsx +112 -0
- package/src/components/menu-family-card.tsx +211 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -0
- package/src/components/menu-mocks.ts +417 -0
- package/src/components/menu-types.ts +259 -0
- package/src/components/motion-icon-set.tsx +30 -3
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/ranked-venue-list.stories.tsx +80 -0
- package/src/components/ranked-venue-list.tsx +177 -0
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/signature-line-list.stories.tsx +85 -0
- package/src/components/signature-line-list.tsx +136 -0
- package/src/components/snap-rail.tsx +90 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/venue-busyness-strip.stories.tsx +66 -0
- package/src/components/venue-busyness-strip.tsx +64 -0
- package/src/components/venue-grade-dimensions.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-signature-block.stories.tsx +68 -0
- package/src/components/venue-signature-block.tsx +71 -0
- package/src/components/venue-spotlight.stories.tsx +141 -0
- package/src/components/venue-spotlight.tsx +236 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* "Signature" / "Mentioned" — the marker for a menu item the review digest
|
|
5
|
+
* backs, shared by `<MenuItem>` and the dish/drink cards so the same claim
|
|
6
|
+
* never renders two different ways.
|
|
7
|
+
*
|
|
8
|
+
* When `onEvidence` is supplied the chip becomes a button that opens the
|
|
9
|
+
* reviews behind it, and takes a dashed outline to read as interactive. That is
|
|
10
|
+
* the whole differentiator: every competitor badges items ("Nou", "NEW"), none
|
|
11
|
+
* can show you why. Without evidence it degrades to a plain label rather than
|
|
12
|
+
* offering a tap that would reveal nothing.
|
|
13
|
+
*
|
|
14
|
+
* `stopPropagation` matters on every host: the row is a `role="button"` and the
|
|
15
|
+
* cards carry a stretched crawlable `<a>` overlay, so an un-stopped click would
|
|
16
|
+
* also open the item drawer. On the cards it must additionally out-rank that
|
|
17
|
+
* overlay's `z-[1]`, hence `relative z-[2]` from the caller.
|
|
18
|
+
*
|
|
19
|
+
* `surface="image"` is for card covers, where the chip sits over the photo
|
|
20
|
+
* rather than in the caption block. The accent-on-canvas palette has no
|
|
21
|
+
* contrast guarantee over an arbitrary photo, so it swaps to the same frosted
|
|
22
|
+
* fill the place badge uses there and keeps the dashed edge — which is what
|
|
23
|
+
* says "there is evidence behind this" — in the inverted colour.
|
|
24
|
+
*
|
|
25
|
+
* The label is passed in: the DS owns no translation keys.
|
|
26
|
+
*/
|
|
27
|
+
type SignatureChipProps = {
|
|
28
|
+
/** Already translated. `strong` → "Signature", `weak` → "Mentioned". */
|
|
29
|
+
label: string;
|
|
30
|
+
onEvidence?: () => void;
|
|
31
|
+
/** `image` = over a card cover; `canvas` = in a caption or row. */
|
|
32
|
+
surface?: "canvas" | "image";
|
|
33
|
+
className?: string;
|
|
34
|
+
};
|
|
35
|
+
declare function SignatureChip({ label, onEvidence, surface, className, }: SignatureChipProps): react_jsx_runtime.JSX.Element;
|
|
36
|
+
|
|
37
|
+
export { SignatureChip, type SignatureChipProps };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuItemLabels } from './menu-item.js';
|
|
3
|
+
import { MenuVenuePreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
4
|
+
import 'react';
|
|
5
|
+
import './place-types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SignatureLineList — family, venue, dish, price and reputation on one line.
|
|
9
|
+
*
|
|
10
|
+
* The densest surface on the hub, and the one that most nearly justifies the
|
|
11
|
+
* whole catalogue: every row ties a kitchen to the single thing its reviewers
|
|
12
|
+
* keep naming, with the price and the count that back it.
|
|
13
|
+
*
|
|
14
|
+
* ## Two targets per row, deliberately
|
|
15
|
+
*
|
|
16
|
+
* The venue line goes to the venue; the item row goes to the item across every
|
|
17
|
+
* kitchen that makes it. Those are genuinely different destinations, and
|
|
18
|
+
* collapsing them into one tap loses the comparison that makes the item worth
|
|
19
|
+
* showing. They are separate anchors rather than one anchor with a nested
|
|
20
|
+
* button, which would be invalid markup.
|
|
21
|
+
*
|
|
22
|
+
* Below `lg` the row stacks: venue line first, then the full `<MenuItem>`
|
|
23
|
+
* underneath. The desktop layout puts them side by side. Same markup, one
|
|
24
|
+
* breakpoint — no second component.
|
|
25
|
+
*
|
|
26
|
+
* Server-safe.
|
|
27
|
+
*/
|
|
28
|
+
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
29
|
+
type SignatureLineListLinkProps = MenuLinkProps;
|
|
30
|
+
type SignatureLineListProps = {
|
|
31
|
+
venues: MenuVenuePreview[];
|
|
32
|
+
/** Passed through to each `<MenuItem>`. */
|
|
33
|
+
labels: MenuItemLabels;
|
|
34
|
+
formatPrice: (amount: number) => string;
|
|
35
|
+
formatCount?: (value: number) => string;
|
|
36
|
+
/** Composed meta line per venue, keyed by place slug, e.g. "romanian · Centru". */
|
|
37
|
+
metaLabels?: Record<string, string>;
|
|
38
|
+
/** The row's category mark per venue, keyed by place slug. */
|
|
39
|
+
hues?: Record<string, string>;
|
|
40
|
+
linkComponent?: MenuLinkComponent;
|
|
41
|
+
className?: string;
|
|
42
|
+
};
|
|
43
|
+
declare function SignatureLineList({ venues, labels, formatPrice, formatCount, metaLabels, hues, linkComponent: LinkComponent, className, }: SignatureLineListProps): react_jsx_runtime.JSX.Element;
|
|
44
|
+
|
|
45
|
+
export { SignatureLineList, type SignatureLineListLinkProps, type SignatureLineListProps };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Separator } from '../chunk-WSRWFA6K.js';
|
|
2
|
+
import { MenuItem } from '../chunk-TH6W2PWG.js';
|
|
3
|
+
import '../chunk-Q6QH52LJ.js';
|
|
4
|
+
import '../chunk-UKBNQO6Q.js';
|
|
5
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
6
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
7
|
+
import { Star, ChevronRight } from 'lucide-react';
|
|
8
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
function DefaultLink(_a) {
|
|
11
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
12
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
13
|
+
}
|
|
14
|
+
function SignatureLineList({
|
|
15
|
+
venues,
|
|
16
|
+
labels,
|
|
17
|
+
formatPrice,
|
|
18
|
+
formatCount,
|
|
19
|
+
metaLabels,
|
|
20
|
+
hues,
|
|
21
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
22
|
+
className
|
|
23
|
+
}) {
|
|
24
|
+
return /* @__PURE__ */ jsx("ul", { className: cn("flex flex-col", className), children: venues.map((venue, index) => {
|
|
25
|
+
const { place, signature } = venue;
|
|
26
|
+
return /* @__PURE__ */ jsxs(
|
|
27
|
+
"li",
|
|
28
|
+
{
|
|
29
|
+
className: "grid gap-x-4 py-3.5 lg:grid-cols-[minmax(0,260px)_minmax(0,1fr)] lg:items-center",
|
|
30
|
+
children: [
|
|
31
|
+
index > 0 && /* @__PURE__ */ jsx(Separator, { className: "mb-3.5 lg:col-span-2" }),
|
|
32
|
+
/* @__PURE__ */ jsxs(
|
|
33
|
+
LinkComponent,
|
|
34
|
+
{
|
|
35
|
+
href: venue.href,
|
|
36
|
+
className: "flex items-center gap-3 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
37
|
+
children: [
|
|
38
|
+
/* @__PURE__ */ jsxs("span", { className: "relative h-12 w-12 shrink-0 overflow-hidden rounded-xl bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)] lg:h-16 lg:w-16", children: [
|
|
39
|
+
place.thumbnail_url && // eslint-disable-next-line @next/next/no-img-element
|
|
40
|
+
/* @__PURE__ */ jsx(
|
|
41
|
+
"img",
|
|
42
|
+
{
|
|
43
|
+
src: place.thumbnail_url,
|
|
44
|
+
alt: "",
|
|
45
|
+
className: "h-full w-full object-cover",
|
|
46
|
+
loading: "lazy"
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
place.open_now && /* @__PURE__ */ jsx(
|
|
50
|
+
"i",
|
|
51
|
+
{
|
|
52
|
+
"aria-hidden": true,
|
|
53
|
+
className: "absolute right-1 top-1 h-[7px] w-[7px] rounded-full bg-[var(--free,currentColor)] ring-2 ring-white/80"
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
] }),
|
|
57
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
58
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-[15px] font-bold tracking-[-0.01em]", children: place.name }),
|
|
59
|
+
/* @__PURE__ */ jsxs("span", { className: "mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
60
|
+
place.rating && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
61
|
+
/* @__PURE__ */ jsx(Star, { className: "h-[11px] w-[11px]", "aria-hidden": true }),
|
|
62
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: place.rating.rating })
|
|
63
|
+
] }),
|
|
64
|
+
(metaLabels == null ? void 0 : metaLabels[place.slug]) && /* @__PURE__ */ jsx("span", { className: "truncate", children: metaLabels[place.slug] })
|
|
65
|
+
] })
|
|
66
|
+
] }),
|
|
67
|
+
/* @__PURE__ */ jsx(
|
|
68
|
+
ChevronRight,
|
|
69
|
+
{
|
|
70
|
+
className: "h-4 w-4 shrink-0 text-muted-foreground lg:hidden",
|
|
71
|
+
"aria-hidden": true
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
),
|
|
77
|
+
signature && /* @__PURE__ */ jsx(
|
|
78
|
+
MenuItem,
|
|
79
|
+
{
|
|
80
|
+
item: signature.item,
|
|
81
|
+
labels,
|
|
82
|
+
formatPrice,
|
|
83
|
+
formatCount,
|
|
84
|
+
markGradient: hues == null ? void 0 : hues[place.slug],
|
|
85
|
+
subtitle: signature.why,
|
|
86
|
+
showSignatureBadge: signature.isSignature,
|
|
87
|
+
href: signature.href,
|
|
88
|
+
linkComponent: LinkComponent,
|
|
89
|
+
className: "min-w-0"
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
place.slug
|
|
95
|
+
);
|
|
96
|
+
}) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { SignatureLineList };
|
|
@@ -20,15 +20,35 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
20
20
|
* `card` is the 14px inset of a hub card; `page` is the 16px page gutter.
|
|
21
21
|
*/
|
|
22
22
|
declare const snapRailVariants: (props?: ({
|
|
23
|
-
itemWidth?: "sm" | "lg" | "md" | null | undefined;
|
|
23
|
+
itemWidth?: "sm" | "lg" | "full" | "md" | null | undefined;
|
|
24
24
|
gap?: "default" | "sm" | null | undefined;
|
|
25
25
|
bleed?: "none" | "page" | "card" | null | undefined;
|
|
26
26
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
27
|
-
type SnapRailProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof snapRailVariants
|
|
27
|
+
type SnapRailProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof snapRailVariants> & {
|
|
28
|
+
/**
|
|
29
|
+
* Page dots under the rail, one per child, the current one filled. For
|
|
30
|
+
* `itemWidth="full"`, where no next card peeks in to say there is more.
|
|
31
|
+
* Decorative: the rail itself is the control, and every card is reachable
|
|
32
|
+
* by scrolling, so the dots are `aria-hidden`.
|
|
33
|
+
*/
|
|
34
|
+
dots?: boolean;
|
|
35
|
+
/** Classes for the dots row, e.g. `lg:hidden` where the rail becomes a grid. */
|
|
36
|
+
dotsClassName?: string;
|
|
37
|
+
};
|
|
28
38
|
declare const SnapRail: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
29
|
-
itemWidth?: "sm" | "lg" | "md" | null | undefined;
|
|
39
|
+
itemWidth?: "sm" | "lg" | "full" | "md" | null | undefined;
|
|
30
40
|
gap?: "default" | "sm" | null | undefined;
|
|
31
41
|
bleed?: "none" | "page" | "card" | null | undefined;
|
|
32
|
-
} & class_variance_authority_types.ClassProp) | undefined) => string> &
|
|
42
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
43
|
+
/**
|
|
44
|
+
* Page dots under the rail, one per child, the current one filled. For
|
|
45
|
+
* `itemWidth="full"`, where no next card peeks in to say there is more.
|
|
46
|
+
* Decorative: the rail itself is the control, and every card is reachable
|
|
47
|
+
* by scrolling, so the dots are `aria-hidden`.
|
|
48
|
+
*/
|
|
49
|
+
dots?: boolean;
|
|
50
|
+
/** Classes for the dots row, e.g. `lg:hidden` where the rail becomes a grid. */
|
|
51
|
+
dotsClassName?: string;
|
|
52
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
33
53
|
|
|
34
54
|
export { SnapRail, type SnapRailProps };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
-
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
2
|
+
import { __objRest, __spreadProps, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { cva } from 'class-variance-authority';
|
|
5
|
-
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
var snapRailVariants = cva(
|
|
8
8
|
cn(
|
|
@@ -14,7 +14,9 @@ var snapRailVariants = cva(
|
|
|
14
14
|
itemWidth: {
|
|
15
15
|
sm: "[&>*]:w-[46%]",
|
|
16
16
|
md: "[&>*]:w-[60%]",
|
|
17
|
-
lg: "[&>*]:w-[80%]"
|
|
17
|
+
lg: "[&>*]:w-[80%]",
|
|
18
|
+
/** One card per view — pair with `dots` so the reader knows there is more. */
|
|
19
|
+
full: "[&>*]:w-full"
|
|
18
20
|
},
|
|
19
21
|
gap: {
|
|
20
22
|
sm: "gap-2",
|
|
@@ -35,15 +37,71 @@ var snapRailVariants = cva(
|
|
|
35
37
|
);
|
|
36
38
|
var SnapRail = React.forwardRef(
|
|
37
39
|
(_a, ref) => {
|
|
38
|
-
var _b = _a, { className, itemWidth, gap, bleed } = _b, props = __objRest(_b, ["className", "itemWidth", "gap", "bleed"]);
|
|
39
|
-
|
|
40
|
+
var _b = _a, { className, itemWidth, gap, bleed, dots, dotsClassName, children } = _b, props = __objRest(_b, ["className", "itemWidth", "gap", "bleed", "dots", "dotsClassName", "children"]);
|
|
41
|
+
const railRef = React.useRef(null);
|
|
42
|
+
const [active, setActive] = React.useState(0);
|
|
43
|
+
const count = React.Children.count(children);
|
|
44
|
+
const setRefs = React.useCallback(
|
|
45
|
+
(node) => {
|
|
46
|
+
railRef.current = node;
|
|
47
|
+
if (typeof ref === "function") ref(node);
|
|
48
|
+
else if (ref) ref.current = node;
|
|
49
|
+
},
|
|
50
|
+
[ref]
|
|
51
|
+
);
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (!dots) return;
|
|
54
|
+
const rail2 = railRef.current;
|
|
55
|
+
if (!rail2) return;
|
|
56
|
+
let frame = 0;
|
|
57
|
+
const sync = () => {
|
|
58
|
+
frame = 0;
|
|
59
|
+
const origin = rail2.scrollLeft + parseFloat(getComputedStyle(rail2).scrollPaddingLeft || "0");
|
|
60
|
+
let best = 0;
|
|
61
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
62
|
+
Array.from(rail2.children).forEach((child, index) => {
|
|
63
|
+
const distance = Math.abs(child.offsetLeft - origin);
|
|
64
|
+
if (distance < bestDistance) {
|
|
65
|
+
bestDistance = distance;
|
|
66
|
+
best = index;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
setActive(best);
|
|
70
|
+
};
|
|
71
|
+
const onScroll = () => {
|
|
72
|
+
if (!frame) frame = requestAnimationFrame(sync);
|
|
73
|
+
};
|
|
74
|
+
sync();
|
|
75
|
+
rail2.addEventListener("scroll", onScroll, { passive: true });
|
|
76
|
+
return () => {
|
|
77
|
+
if (frame) cancelAnimationFrame(frame);
|
|
78
|
+
rail2.removeEventListener("scroll", onScroll);
|
|
79
|
+
};
|
|
80
|
+
}, [dots, count]);
|
|
81
|
+
const rail = /* @__PURE__ */ jsx(
|
|
40
82
|
"div",
|
|
41
|
-
__spreadValues({
|
|
42
|
-
ref,
|
|
83
|
+
__spreadProps(__spreadValues({
|
|
84
|
+
ref: setRefs,
|
|
43
85
|
"data-slot": "snap-rail",
|
|
44
86
|
className: cn(snapRailVariants({ itemWidth, gap, bleed }), className)
|
|
45
|
-
}, props)
|
|
87
|
+
}, props), {
|
|
88
|
+
children
|
|
89
|
+
})
|
|
46
90
|
);
|
|
91
|
+
if (!dots) return rail;
|
|
92
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
93
|
+
rail,
|
|
94
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": true, className: cn("flex justify-center gap-1.5", dotsClassName), children: Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx(
|
|
95
|
+
"span",
|
|
96
|
+
{
|
|
97
|
+
className: cn(
|
|
98
|
+
"h-1.5 rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none",
|
|
99
|
+
index === active ? "w-4 bg-foreground" : "w-1.5 bg-foreground/25"
|
|
100
|
+
)
|
|
101
|
+
},
|
|
102
|
+
index
|
|
103
|
+
)) })
|
|
104
|
+
] });
|
|
47
105
|
}
|
|
48
106
|
);
|
|
49
107
|
SnapRail.displayName = "SnapRail";
|
|
@@ -4,11 +4,11 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
|
|
5
5
|
declare const spinnerVariants: (props?: ({
|
|
6
6
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
7
|
-
tone?: "
|
|
7
|
+
tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Spinner: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
10
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
11
|
-
tone?: "
|
|
11
|
+
tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
/** Visually-hidden label for screen readers. Defaults to "Loading". */
|
|
14
14
|
label?: string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
3
|
+
import { VariantProps } from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* StatBanner — one claim about the catalogue, carried by a single number.
|
|
7
|
+
*
|
|
8
|
+
* These sit between the modules that list things and give the page somewhere to
|
|
9
|
+
* state what it knows rather than only showing it. Two side by side is the
|
|
10
|
+
* usual arrangement, which is a caller's grid rather than a `StatBannerPair`
|
|
11
|
+
* component — pairing is layout, and a consumer wanting one, or three, or a
|
|
12
|
+
* different order should not have to fight a wrapper for it.
|
|
13
|
+
*
|
|
14
|
+
* `scale` draws the distribution the figure sits in — low, the figure, high.
|
|
15
|
+
* Pass it only when all three are real numbers from the same measurement; a
|
|
16
|
+
* scale invented around a figure is worse than no scale, because it implies a
|
|
17
|
+
* spread nobody counted.
|
|
18
|
+
*
|
|
19
|
+
* Server-safe.
|
|
20
|
+
*/
|
|
21
|
+
declare const bannerVariants: (props?: ({
|
|
22
|
+
tone?: "dark" | "warm" | null | undefined;
|
|
23
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
24
|
+
type StatBannerProps = VariantProps<typeof bannerVariants> & {
|
|
25
|
+
/** Small caps kicker, e.g. "THE REVIEW CENSUS". */
|
|
26
|
+
kicker?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The headline claim. On the dark tone this is the whole point; on the warm
|
|
29
|
+
* one it usually sits under `figure`.
|
|
30
|
+
*/
|
|
31
|
+
headline?: string;
|
|
32
|
+
body?: string;
|
|
33
|
+
/** The number itself, pre-formatted, e.g. "100". */
|
|
34
|
+
figure?: string;
|
|
35
|
+
/** Unit set small beside the figure, e.g. "%" or "lei". */
|
|
36
|
+
figureUnit?: string;
|
|
37
|
+
/** Oversized watermark numeral behind the content. */
|
|
38
|
+
watermark?: string;
|
|
39
|
+
/** Low, middle and high of the distribution. All three, or none. */
|
|
40
|
+
scale?: [string, string, string];
|
|
41
|
+
/** The action — pass a rendered link or button. */
|
|
42
|
+
action?: React.ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
};
|
|
45
|
+
declare function StatBanner({ tone, kicker, headline, body, figure, figureUnit, watermark, scale, action, className, }: StatBannerProps): react_jsx_runtime.JSX.Element;
|
|
46
|
+
|
|
47
|
+
export { StatBanner, type StatBannerProps };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var bannerVariants = cva(
|
|
7
|
+
"relative flex flex-col gap-2 overflow-hidden rounded-[18px] p-5",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
tone: {
|
|
11
|
+
/** Dark ground, for the claim that needs the most weight. */
|
|
12
|
+
dark: "bg-[oklch(0.21_0.012_55)] text-white",
|
|
13
|
+
/** Tinted ground, for the supporting figure beside it. */
|
|
14
|
+
warm: "bg-accent-subtle text-foreground"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: { tone: "dark" }
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
function StatBanner({
|
|
21
|
+
tone,
|
|
22
|
+
kicker,
|
|
23
|
+
headline,
|
|
24
|
+
body,
|
|
25
|
+
figure,
|
|
26
|
+
figureUnit,
|
|
27
|
+
watermark,
|
|
28
|
+
scale,
|
|
29
|
+
action,
|
|
30
|
+
className
|
|
31
|
+
}) {
|
|
32
|
+
return /* @__PURE__ */ jsxs("div", { className: cn(bannerVariants({ tone }), className), children: [
|
|
33
|
+
watermark && /* @__PURE__ */ jsx(
|
|
34
|
+
"span",
|
|
35
|
+
{
|
|
36
|
+
"aria-hidden": true,
|
|
37
|
+
className: "pointer-events-none absolute -bottom-8 right-2 select-none font-serif text-[150px] font-bold leading-none opacity-[0.08]",
|
|
38
|
+
children: watermark
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-[1] flex flex-1 flex-col gap-2", children: [
|
|
42
|
+
kicker && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-bold tracking-[0.11em] opacity-80", children: kicker }),
|
|
43
|
+
figure && /* @__PURE__ */ jsxs("div", { className: "text-[40px] font-extrabold leading-none tracking-[-0.03em] tabular-nums", children: [
|
|
44
|
+
figure,
|
|
45
|
+
figureUnit && /* @__PURE__ */ jsx("span", { className: "ml-1 text-lg font-extrabold", children: figureUnit })
|
|
46
|
+
] }),
|
|
47
|
+
headline && /* @__PURE__ */ jsx("h3", { className: "font-serif text-xl font-semibold leading-[1.2] tracking-[-0.02em] lg:text-2xl", children: headline }),
|
|
48
|
+
body && /* @__PURE__ */ jsx("p", { className: "text-[13px] leading-[1.5] opacity-85", children: body }),
|
|
49
|
+
scale && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2 text-[11px] tabular-nums opacity-70", children: [
|
|
50
|
+
/* @__PURE__ */ jsx("span", { children: scale[0] }),
|
|
51
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-px flex-1 bg-current opacity-30" }),
|
|
52
|
+
/* @__PURE__ */ jsx("span", { children: scale[1] }),
|
|
53
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-px flex-1 bg-current opacity-30" }),
|
|
54
|
+
/* @__PURE__ */ jsx("span", { children: scale[2] })
|
|
55
|
+
] }),
|
|
56
|
+
action && /* @__PURE__ */ jsx("div", { className: "mt-auto flex items-center pt-3", children: action })
|
|
57
|
+
] })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { StatBanner };
|
package/dist/components/tag.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const tagVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "destructive" | "outline" | "success" | "warning" | "primary" | "quiet" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Tag: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
|
-
variant?: "
|
|
10
|
+
variant?: "default" | "destructive" | "outline" | "success" | "warning" | "primary" | "quiet" | null | undefined;
|
|
11
11
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
onRemove?: () => void;
|
|
@@ -22,9 +22,9 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
22
22
|
* Weights are 400 / 600 / 700. 500 is not in the system.
|
|
23
23
|
*/
|
|
24
24
|
declare const textVariants: (props?: ({
|
|
25
|
-
variant?: "
|
|
26
|
-
color?: "
|
|
27
|
-
weight?: "
|
|
25
|
+
variant?: "small" | "display" | "h2" | "h3" | "label" | "body" | "caption" | "h1" | "h4" | "body-lg" | "body-sm" | "overline" | null | undefined;
|
|
26
|
+
color?: "default" | "destructive" | "inherit" | "muted" | "primary" | "primary-foreground" | null | undefined;
|
|
27
|
+
weight?: "medium" | "bold" | "normal" | "semibold" | null | undefined;
|
|
28
28
|
leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
|
|
29
29
|
tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
|
|
30
30
|
wrap?: "balance" | "pretty" | "nowrap" | null | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VenueBusynessStrip — when a venue actually fills up, as a bar per hour.
|
|
5
|
+
*
|
|
6
|
+
* The API serves one score per hour of the venue's own trading day, so the
|
|
7
|
+
* strip has no fixed length: a bakery closing at three and a club open until
|
|
8
|
+
* four draw different numbers of bars, and padding either to 24 would put empty
|
|
9
|
+
* columns under hours the venue is not open.
|
|
10
|
+
*
|
|
11
|
+
* The rush is derived rather than passed: bars at or above `peakRatio` of the
|
|
12
|
+
* day's own maximum take the accent. A fixed score threshold would mark every
|
|
13
|
+
* hour at a busy venue and none at a quiet one — busyness is only meaningful
|
|
14
|
+
* relative to that venue's own day.
|
|
15
|
+
*
|
|
16
|
+
* `caption` is composed by the caller ("Usually not busy until 18:00 — the rush
|
|
17
|
+
* is nine to eleven"), because the sentence needs the venue's clock, its locale
|
|
18
|
+
* and a plural rule the DS does not own.
|
|
19
|
+
*
|
|
20
|
+
* Server-safe.
|
|
21
|
+
*/
|
|
22
|
+
type VenueBusynessStripProps = {
|
|
23
|
+
busyness: Array<{
|
|
24
|
+
hour: number;
|
|
25
|
+
score: number;
|
|
26
|
+
}>;
|
|
27
|
+
/** Composed sentence beside the bars. */
|
|
28
|
+
caption?: string;
|
|
29
|
+
/** Share of the day's own peak at which an hour counts as the rush. */
|
|
30
|
+
peakRatio?: number;
|
|
31
|
+
className?: string;
|
|
32
|
+
};
|
|
33
|
+
declare function VenueBusynessStrip({ busyness, caption, peakRatio, className, }: VenueBusynessStripProps): react_jsx_runtime.JSX.Element | null;
|
|
34
|
+
|
|
35
|
+
export { VenueBusynessStrip, type VenueBusynessStripProps };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var BAR_MAX_HEIGHT = 22;
|
|
6
|
+
function VenueBusynessStrip({
|
|
7
|
+
busyness,
|
|
8
|
+
caption,
|
|
9
|
+
peakRatio = 0.8,
|
|
10
|
+
className
|
|
11
|
+
}) {
|
|
12
|
+
if (busyness.length === 0) return null;
|
|
13
|
+
const peak = Math.max(...busyness.map((entry) => entry.score));
|
|
14
|
+
if (peak <= 0) return null;
|
|
15
|
+
return /* @__PURE__ */ jsxs(
|
|
16
|
+
"div",
|
|
17
|
+
{
|
|
18
|
+
className: cn("flex items-center gap-2.5 text-[12.5px] text-muted-foreground", className),
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex h-[22px] items-end gap-[3px]", children: busyness.map(({ hour, score }) => /* @__PURE__ */ jsx(
|
|
21
|
+
"i",
|
|
22
|
+
{
|
|
23
|
+
className: cn(
|
|
24
|
+
"w-[7px] rounded-t-sm bg-[oklch(0.88_0.02_45)]",
|
|
25
|
+
score >= peak * peakRatio && "bg-[var(--accent,currentColor)]"
|
|
26
|
+
),
|
|
27
|
+
style: { height: Math.max(2, Math.round(score / peak * BAR_MAX_HEIGHT)) }
|
|
28
|
+
},
|
|
29
|
+
hour
|
|
30
|
+
)) }),
|
|
31
|
+
caption
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { VenueBusynessStrip };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { PlaceGradeDimensions } from './place-types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VenueGradeDimensions — the per-dimension review scores as a row of bars.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this renders what it is given rather than a fixed row
|
|
8
|
+
*
|
|
9
|
+
* A dimension the reviews never mention is ABSENT from the payload, not zero,
|
|
10
|
+
* and exactly one of `food` / `coffee_quality` / `drinks_quality` is scored per
|
|
11
|
+
* place — a coffee bar has no food score and a restaurant has no coffee one.
|
|
12
|
+
* So a component drawing seven fixed columns would invent readings for a third
|
|
13
|
+
* of the catalogue. It iterates `order`, skips what is missing, and lays out
|
|
14
|
+
* however many survive.
|
|
15
|
+
*
|
|
16
|
+
* Scores arrive 0–100 and display on the 0–10 scale the rest of the product
|
|
17
|
+
* grades on, so the bar width is the raw value and the numeral is that value
|
|
18
|
+
* over ten. `formatScore` is injected because a decimal separator is
|
|
19
|
+
* locale-dependent.
|
|
20
|
+
*
|
|
21
|
+
* Server-safe.
|
|
22
|
+
*/
|
|
23
|
+
type VenueGradeDimensionKey = keyof PlaceGradeDimensions;
|
|
24
|
+
type VenueGradeDimensionsProps = {
|
|
25
|
+
dimensions: PlaceGradeDimensions;
|
|
26
|
+
/** Dimension key → translated label. Keys with no label fall back to the key. */
|
|
27
|
+
labels: Partial<Record<VenueGradeDimensionKey, string>>;
|
|
28
|
+
/** Override the canonical order, or narrow which dimensions show at all. */
|
|
29
|
+
order?: VenueGradeDimensionKey[];
|
|
30
|
+
/** 0–100 → display numeral, e.g. `(v) => (v / 10).toFixed(1)`. */
|
|
31
|
+
formatScore: (value: number) => string;
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
declare function VenueGradeDimensions({ dimensions, labels, order, formatScore, className, }: VenueGradeDimensionsProps): react_jsx_runtime.JSX.Element | null;
|
|
35
|
+
|
|
36
|
+
export { type VenueGradeDimensionKey, VenueGradeDimensions, type VenueGradeDimensionsProps };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CitedText } from './place-types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VenueQuotes — the digest's standout lines, as pull-quotes.
|
|
6
|
+
*
|
|
7
|
+
* These are `reviews_digest.highlights`: one to three short positives already
|
|
8
|
+
* resolved into the request locale. They are the only place on a hub where a
|
|
9
|
+
* reviewer speaks in their own register, so they are set in serif italic
|
|
10
|
+
* against the sans everything else uses.
|
|
11
|
+
*
|
|
12
|
+
* `onCite` turns a quote into a button that opens the reviews behind it —
|
|
13
|
+
* `review_ids` is what makes the claim checkable, and a quote nobody can trace
|
|
14
|
+
* is just copy. Without the handler the quote renders as inert prose, which is
|
|
15
|
+
* correct on surfaces that have nowhere to open.
|
|
16
|
+
*
|
|
17
|
+
* No directive: inert quotes render on the server; passing `onCite` needs a
|
|
18
|
+
* client caller.
|
|
19
|
+
*/
|
|
20
|
+
type VenueQuotesProps = {
|
|
21
|
+
quotes: CitedText[];
|
|
22
|
+
/** Opens the reviews behind one quote. Omit to render inert prose. */
|
|
23
|
+
onCite?: (quote: CitedText) => void;
|
|
24
|
+
/** Accessible name for the cite action, e.g. `(n) => "Read 5 reviews"`. */
|
|
25
|
+
citeLabel?: (reviewCount: number) => string;
|
|
26
|
+
/** Cap on how many render. The digest ships at most three. */
|
|
27
|
+
limit?: number;
|
|
28
|
+
className?: string;
|
|
29
|
+
};
|
|
30
|
+
declare function VenueQuotes({ quotes, onCite, citeLabel, limit, className }: VenueQuotesProps): react_jsx_runtime.JSX.Element | null;
|
|
31
|
+
|
|
32
|
+
export { VenueQuotes, type VenueQuotesProps };
|