@bearmenu/ui 0.8.48 → 0.8.49

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.
Files changed (45) hide show
  1. package/dist/components/alert.d.ts +2 -2
  2. package/dist/components/badge.d.ts +1 -1
  3. package/dist/components/button.d.ts +1 -1
  4. package/dist/components/concept-compare.d.ts +1 -1
  5. package/dist/components/doorway-tiles.d.ts +1 -1
  6. package/dist/components/grade-atom.d.ts +1 -1
  7. package/dist/components/kitchen-dossier.d.ts +1 -1
  8. package/dist/components/menu-dead-end.d.ts +1 -1
  9. package/dist/components/menu-family-block.d.ts +1 -1
  10. package/dist/components/menu-family-card.d.ts +1 -1
  11. package/dist/components/menu-filter-chips.d.ts +1 -1
  12. package/dist/components/menu-group-rows.d.ts +1 -1
  13. package/dist/components/menu-index-header.d.ts +1 -1
  14. package/dist/components/menu-index-list.d.ts +1 -1
  15. package/dist/components/menu-intent-hero.d.ts +1 -1
  16. package/dist/components/menu-item.d.ts +1 -1
  17. package/dist/components/menu-kitchen-list.d.ts +1 -1
  18. package/dist/components/menu-mocks.d.ts +1 -1
  19. package/dist/components/menu-types.d.ts +1 -1
  20. package/dist/components/open-now-list.d.ts +1 -1
  21. package/dist/components/place-about-tab.d.ts +1 -1
  22. package/dist/components/place-card.d.ts +1 -1
  23. package/dist/components/place-card.js +13 -5
  24. package/dist/components/place-details-mobile.d.ts +1 -1
  25. package/dist/components/place-schedule-week.d.ts +1 -1
  26. package/dist/components/place-types.d.ts +1 -1
  27. package/dist/components/qa-block.d.ts +1 -1
  28. package/dist/components/spinner.d.ts +2 -2
  29. package/dist/components/tag.d.ts +2 -2
  30. package/dist/components/text.d.ts +1 -1
  31. package/dist/components/toggle.d.ts +1 -1
  32. package/dist/components/top-comparison.d.ts +1 -1
  33. package/dist/components/top-cross-link.d.ts +1 -1
  34. package/dist/components/top-ranking-entry.d.ts +1 -1
  35. package/dist/components/top-ranking-hero.d.ts +1 -1
  36. package/dist/components/top-types.d.ts +1 -1
  37. package/dist/components/venue-grade-dimensions.d.ts +1 -1
  38. package/dist/components/venue-quotes.d.ts +1 -1
  39. package/dist/components/venue-wall.d.ts +1 -1
  40. package/dist/{place-types-C4rR3LTs.d.ts → place-types-DCVqWmk_.d.ts} +11 -0
  41. package/package.json +217 -23
  42. package/src/components/place-card.stories.tsx +36 -0
  43. package/src/components/place-card.test.tsx +29 -0
  44. package/src/components/place-card.tsx +24 -5
  45. package/src/components/place-types.ts +11 -0
@@ -3,10 +3,10 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const alertVariants: (props?: ({
6
- variant?: "default" | "success" | "warning" | "destructive" | null | undefined;
6
+ variant?: "success" | "warning" | "default" | "destructive" | null | undefined;
7
7
  } & class_variance_authority_types.ClassProp) | undefined) => string;
8
8
  declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
9
- variant?: "default" | "success" | "warning" | "destructive" | null | undefined;
9
+ variant?: "success" | "warning" | "default" | "destructive" | null | undefined;
10
10
  } & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
11
11
  declare const AlertTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLParagraphElement>>;
12
12
  declare const AlertDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const badgeVariants: (props?: ({
7
- variant?: "default" | "secondary" | "success" | "warning" | "destructive" | "outline" | "glass" | "overlay" | "brown" | "code" | "onPrimary" | "confirmed" | "pending" | "completed" | "assignee" | "softSuccess" | "softWarning" | "softDestructive" | "softNeutral" | null | undefined;
7
+ variant?: "secondary" | "success" | "warning" | "default" | "destructive" | "outline" | "glass" | "overlay" | "brown" | "code" | "onPrimary" | "confirmed" | "pending" | "completed" | "assignee" | "softSuccess" | "softWarning" | "softDestructive" | "softNeutral" | null | undefined;
8
8
  size?: "status" | "chip" | null | undefined;
9
9
  live?: boolean | null | undefined;
10
10
  } & class_variance_authority_types.ClassProp) | undefined) => string;
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const buttonVariants: (props?: ({
6
- variant?: "default" | "link" | "secondary" | "warning" | "destructive" | "outline" | "ghost" | "glass" | "overlay" | "ink" | "brown" | "cta" | null | undefined;
6
+ variant?: "link" | "secondary" | "warning" | "default" | "destructive" | "outline" | "ghost" | "glass" | "overlay" | "ink" | "brown" | "cta" | null | undefined;
7
7
  size?: "icon" | "default" | "cta" | "xs" | "sm" | "lg" | "pill" | "cta-sm" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
8
8
  fullWidth?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
@@ -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-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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 { MenuLinkComponent } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  /**
@@ -1,3 +1,3 @@
1
1
  import 'react/jsx-runtime';
2
2
  import '../lib/grade.js';
3
- export { G as GradeAtom, h as GradeAtomLabels, i as GradeAtomProps, j as GradeAtomSize } from '../place-types-C4rR3LTs.js';
3
+ export { G as GradeAtom, h as GradeAtomLabels, i as GradeAtomProps, j as GradeAtomSize } from '../place-types-DCVqWmk_.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, a as PlaceGradeDimensions } from '../place-types-C4rR3LTs.js';
4
+ import { P as PlacePreview, C as CitedText, a as PlaceGradeDimensions } from '../place-types-DCVqWmk_.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 { MenuLinkComponent, MenuLoosenOption } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  /**
@@ -2,7 +2,7 @@ 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
4
  import 'react';
5
- import '../place-types-C4rR3LTs.js';
5
+ import '../place-types-DCVqWmk_.js';
6
6
  import '../lib/grade.js';
7
7
 
8
8
  /**
@@ -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-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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 { MenuFilterChip } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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 { MenuLinkComponent, MenuLinkProps } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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 { MenuIntentFigure, MenuLinkComponent } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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
3
  import { MenuItemPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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 { MenuKitchenPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  /**
@@ -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-C4rR3LTs.js';
3
+ import { P as PlacePreview } from '../place-types-DCVqWmk_.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-C4rR3LTs.js';
2
+ import { M as MenuSizeAndPrice, P as PlacePreview } from '../place-types-DCVqWmk_.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-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.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, b as PlaceAboutTabLabels, c as PlaceLinkKind } from '../place-types-C4rR3LTs.js';
2
+ import { P as PlacePreview, b as PlaceAboutTabLabels, c as PlaceLinkKind } from '../place-types-DCVqWmk_.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, d as PlaceCardLabels } from '../place-types-C4rR3LTs.js';
4
+ import { P as PlacePreview, d as PlaceCardLabels } from '../place-types-DCVqWmk_.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  /**
@@ -56,6 +56,10 @@ function PriceLevel({ level, label }) {
56
56
  /* @__PURE__ */ jsx("span", { className: "opacity-30", children: "$".repeat(4 - filled) })
57
57
  ] });
58
58
  }
59
+ function launchYear(launchedAt) {
60
+ const match = launchedAt ? /^(\d{4})-\d{2}$/.exec(launchedAt) : null;
61
+ return match ? Number(match[1]) : void 0;
62
+ }
59
63
  var STATIC_ROOT_CLASSES = `group relative flex w-full select-none flex-col ${HOVER_MOTION} ${HOVER_LIFT_GRID_GROUP}`;
60
64
  var ROOT_CLASSES = `${STATIC_ROOT_CLASSES} cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:rounded-xl`;
61
65
  var DEFAULT_SIZES = "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw";
@@ -72,7 +76,7 @@ var PlaceCard = memo(function PlaceCard2({
72
76
  photoOverlay,
73
77
  className
74
78
  }) {
75
- var _a, _b, _c, _d, _e, _f, _g, _h;
79
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
76
80
  const [imgError, setImgError] = useState(false);
77
81
  const MotionLink = useMemo(() => motion.create(LinkComponent), [LinkComponent]);
78
82
  const coverUrl = imgError ? void 0 : place.cover_url;
@@ -81,6 +85,8 @@ var PlaceCard = memo(function PlaceCard2({
81
85
  const neighbourhood = (_e = (_d = place.areas) == null ? void 0 : _d[0]) == null ? void 0 : _e.name;
82
86
  const priceLevel = (_h = (_f = place.price_dishes) == null ? void 0 : _f.price_score) != null ? _h : (_g = place.price_drinks) == null ? void 0 : _g.price_score;
83
87
  const showPrice = priceLevel !== void 0 && priceLevel > 0;
88
+ const year = labels.established ? launchYear(place.launched_at) : void 0;
89
+ const established = year !== void 0 ? (_i = labels.established) == null ? void 0 : _i.call(labels, year) : void 0;
84
90
  const ariaLabel = labels.viewDetails(place.name);
85
91
  const handleKeyDown = (e) => {
86
92
  if (e.key === "Enter" || e.key === " ") {
@@ -145,10 +151,12 @@ var PlaceCard = memo(function PlaceCard2({
145
151
  /* @__PURE__ */ jsx(HeadlineRating, { place, labels })
146
152
  ] }),
147
153
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
148
- (cuisine || showPrice) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
149
- cuisine && /* @__PURE__ */ jsx("span", { className: "capitalize", children: cuisine }),
150
- cuisine && showPrice && /* @__PURE__ */ jsx("span", { className: "text-border", children: "\xB7" }),
151
- showPrice && /* @__PURE__ */ jsx(PriceLevel, { level: priceLevel, label: labels.priceLevel(priceLevel) })
154
+ (cuisine || showPrice || established) && /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground", children: [
155
+ cuisine && /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate capitalize", children: cuisine }),
156
+ cuisine && showPrice && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-border", children: "\xB7" }),
157
+ showPrice && /* @__PURE__ */ jsx("span", { className: "shrink-0", children: /* @__PURE__ */ jsx(PriceLevel, { level: priceLevel, label: labels.priceLevel(priceLevel) }) }),
158
+ established && (cuisine || showPrice) && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-border", children: "\xB7" }),
159
+ established && /* @__PURE__ */ jsx("span", { className: "shrink-0 whitespace-nowrap", children: established })
152
160
  ] }),
153
161
  distance !== void 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
154
162
  /* @__PURE__ */ jsx(MapPin, { className: "h-3 w-3 shrink-0", "aria-hidden": true }),
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { P as PlacePreview, e as PlaceDetailsMobileLabels } from '../place-types-C4rR3LTs.js';
2
+ import { P as PlacePreview, e as PlaceDetailsMobileLabels } from '../place-types-DCVqWmk_.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 { f as PlaceSchedule, g as PlaceScheduleWeekLabels } from '../place-types-C4rR3LTs.js';
2
+ import { f as PlaceSchedule, g as PlaceScheduleWeekLabels } from '../place-types-DCVqWmk_.js';
3
3
  import '../lib/grade.js';
4
4
 
5
5
  interface PlaceScheduleWeekProps {
@@ -1,3 +1,3 @@
1
- export { C as CitedText, k as CitedValue, M as MenuSizeAndPrice, b as PlaceAboutTabLabels, d as PlaceCardLabels, e as PlaceDetailsMobileLabels, l as PlaceGrade, a as PlaceGradeDimensions, m as PlaceHourBlock, c as PlaceLinkKind, n as PlaceMustTry, P as PlacePreview, o as PlacePriceRange, p as PlaceReviewsDigest, f as PlaceSchedule, g as PlaceScheduleWeekLabels, W as WeekdayKey } from '../place-types-C4rR3LTs.js';
1
+ export { C as CitedText, k as CitedValue, M as MenuSizeAndPrice, b as PlaceAboutTabLabels, d as PlaceCardLabels, e as PlaceDetailsMobileLabels, l as PlaceGrade, a as PlaceGradeDimensions, m as PlaceHourBlock, c as PlaceLinkKind, n as PlaceMustTry, P as PlacePreview, o as PlacePriceRange, p as PlaceReviewsDigest, f as PlaceSchedule, g as PlaceScheduleWeekLabels, W as WeekdayKey } from '../place-types-DCVqWmk_.js';
2
2
  import 'react/jsx-runtime';
3
3
  import '../lib/grade.js';
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { QaItem } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  /**
@@ -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?: "default" | "inherit" | "primary" | "destructive" | null | undefined;
7
+ tone?: "inherit" | "primary" | "default" | "destructive" | 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?: "default" | "inherit" | "primary" | "destructive" | null | undefined;
11
+ tone?: "inherit" | "primary" | "default" | "destructive" | 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;
@@ -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?: "default" | "primary" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
6
+ variant?: "primary" | "success" | "warning" | "default" | "destructive" | "outline" | "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?: "default" | "primary" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
10
+ variant?: "primary" | "success" | "warning" | "default" | "destructive" | "outline" | "quiet" | null | undefined;
11
11
  size?: "default" | "sm" | "lg" | null | undefined;
12
12
  } & class_variance_authority_types.ClassProp) | undefined) => string> & {
13
13
  onRemove?: () => void;
@@ -23,7 +23,7 @@ import { VariantProps } from 'class-variance-authority';
23
23
  */
24
24
  declare const textVariants: (props?: ({
25
25
  variant?: "body" | "small" | "display" | "h2" | "h3" | "label" | "caption" | "h1" | "h4" | "body-lg" | "body-sm" | "overline" | null | undefined;
26
- color?: "default" | "inherit" | "primary" | "destructive" | "muted" | "primary-foreground" | null | undefined;
26
+ color?: "inherit" | "primary" | "default" | "destructive" | "muted" | "primary-foreground" | null | undefined;
27
27
  weight?: "bold" | "medium" | "normal" | "semibold" | null | undefined;
28
28
  leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
29
29
  tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
@@ -11,7 +11,7 @@ import { VariantProps } from 'class-variance-authority';
11
11
  * `data-[state=on]:` utilities sort after `hover:` and still win while on.
12
12
  */
13
13
  declare const toggleVariants: (props?: ({
14
- variant?: "default" | "tab" | "outline" | "chip" | "segmented" | null | undefined;
14
+ variant?: "tab" | "default" | "outline" | "chip" | "segmented" | null | undefined;
15
15
  size?: "default" | "sm" | "lg" | null | undefined;
16
16
  } & class_variance_authority_types.ClassProp) | undefined) => string;
17
17
  /**
@@ -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-C4rR3LTs.js';
5
+ import '../place-types-DCVqWmk_.js';
6
6
  import '../lib/grade.js';
7
7
 
8
8
  type TopComparisonLabels = {
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { MenuLinkComponent } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-C4rR3LTs.js';
4
+ import '../place-types-DCVqWmk_.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  type TopCrossLinkProps = {
@@ -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-C4rR3LTs.js';
6
+ import '../place-types-DCVqWmk_.js';
7
7
  import '../lib/grade.js';
8
8
 
9
9
  type TopRankingEntryLabels = {
@@ -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-C4rR3LTs.js';
5
+ import '../place-types-DCVqWmk_.js';
6
6
  import '../lib/grade.js';
7
7
 
8
8
  type TopRankingHeroProps = {
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { MenuItemPreview } from './menu-types.js';
3
- import '../place-types-C4rR3LTs.js';
3
+ import '../place-types-DCVqWmk_.js';
4
4
  import 'react/jsx-runtime';
5
5
  import '../lib/grade.js';
6
6
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { a as PlaceGradeDimensions } from '../place-types-C4rR3LTs.js';
2
+ import { a as PlaceGradeDimensions } from '../place-types-DCVqWmk_.js';
3
3
  import '../lib/grade.js';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { C as CitedText } from '../place-types-C4rR3LTs.js';
2
+ import { C as CitedText } from '../place-types-DCVqWmk_.js';
3
3
  import '../lib/grade.js';
4
4
 
5
5
  /**
@@ -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-C4rR3LTs.js';
3
+ import { P as PlacePreview } from '../place-types-DCVqWmk_.js';
4
4
  import 'react';
5
5
  import '../lib/grade.js';
6
6
 
@@ -123,6 +123,12 @@ type PlacePreview = {
123
123
  * bare storage key straight to `next/image`'s loader).
124
124
  */
125
125
  cover_url?: string;
126
+ /**
127
+ * Year and month the place launched (`YYYY-MM`), as the schema ships it.
128
+ * The backend approximates it from the earliest review it holds, so it is a
129
+ * floor rather than a founding date. `<PlaceCard>` shows the year only.
130
+ */
131
+ launched_at?: string;
126
132
  };
127
133
  type PlaceHourBlock = {
128
134
  opens_at: number;
@@ -248,6 +254,11 @@ type PlaceCardLabels = {
248
254
  viewDetails: (name: string) => string;
249
255
  /** Accessible name of the `$` price glyphs, e.g. "Price level 2 of 4". */
250
256
  priceLevel: (level: number) => string;
257
+ /**
258
+ * The launch year on the meta line, e.g. "Est. 2020" / "Din 2020". Optional:
259
+ * without it the card shows no year even when `launched_at` is set.
260
+ */
261
+ established?: (year: number) => string;
251
262
  grade: GradeAtomLabels;
252
263
  };
253
264
  /**
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.8.48",
3
+ "version": "0.8.49",
4
+ "packageManager": "pnpm@10.28.1",
4
5
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
6
  "license": "UNLICENSED",
6
7
  "repository": {
@@ -16,20 +17,223 @@
16
17
  "src"
17
18
  ],
18
19
  "exports": {
20
+ "./components/accordion": "./src/components/accordion.tsx",
21
+ "./components/action-bar": "./src/components/action-bar.tsx",
22
+ "./components/activity-row": "./src/components/activity-row.tsx",
23
+ "./components/alert": "./src/components/alert.tsx",
24
+ "./components/alert-dialog": "./src/components/alert-dialog.tsx",
25
+ "./components/aspect-ratio": "./src/components/aspect-ratio.tsx",
26
+ "./components/avatar": "./src/components/avatar.tsx",
27
+ "./components/badge": "./src/components/badge.tsx",
28
+ "./components/banner": "./src/components/banner.tsx",
29
+ "./components/booking-row": "./src/components/booking-row.tsx",
30
+ "./components/booking-summary-card": "./src/components/booking-summary-card.tsx",
31
+ "./components/breadcrumb": "./src/components/breadcrumb.tsx",
32
+ "./components/button": "./src/components/button.tsx",
33
+ "./components/button-group": "./src/components/button-group.tsx",
34
+ "./components/calendar": "./src/components/calendar.tsx",
35
+ "./components/card": "./src/components/card.tsx",
36
+ "./components/carousel": "./src/components/carousel.tsx",
37
+ "./components/category-deck": "./src/components/category-deck.tsx",
38
+ "./components/checkbox": "./src/components/checkbox.tsx",
39
+ "./components/circular-progress": "./src/components/circular-progress.tsx",
40
+ "./components/coach-marks": "./src/components/coach-marks.tsx",
41
+ "./components/collapsible": "./src/components/collapsible.tsx",
42
+ "./components/combobox": "./src/components/combobox.tsx",
43
+ "./components/command": "./src/components/command.tsx",
44
+ "./components/concept-card": "./src/components/concept-card.tsx",
45
+ "./components/concept-compare": "./src/components/concept-compare.tsx",
46
+ "./components/concept-compare-rail": "./src/components/concept-compare-rail.tsx",
47
+ "./components/concept-convergence": "./src/components/concept-convergence.tsx",
48
+ "./components/convergence": "./src/components/convergence.tsx",
49
+ "./components/coverflow": "./src/components/coverflow.tsx",
50
+ "./components/currency-input": "./src/components/currency-input.tsx",
51
+ "./components/date-input": "./src/components/date-input.tsx",
52
+ "./components/day-rail-chip": "./src/components/day-rail-chip.tsx",
53
+ "./components/description-list": "./src/components/description-list.tsx",
54
+ "./components/dialog": "./src/components/dialog.tsx",
55
+ "./components/doorway-tiles": "./src/components/doorway-tiles.tsx",
56
+ "./components/drawer": "./src/components/drawer.tsx",
57
+ "./components/drift-rail": "./src/components/drift-rail.tsx",
58
+ "./components/dropdown-menu": "./src/components/dropdown-menu.tsx",
59
+ "./components/empty-state": "./src/components/empty-state.tsx",
60
+ "./components/event-card": "./src/components/event-card.tsx",
61
+ "./components/event-compact-row": "./src/components/event-compact-row.tsx",
62
+ "./components/event-day-header": "./src/components/event-day-header.tsx",
63
+ "./components/event-day-separator": "./src/components/event-day-separator.tsx",
64
+ "./components/event-detail-panel": "./src/components/event-detail-panel.tsx",
65
+ "./components/event-hub-hero": "./src/components/event-hub-hero.tsx",
66
+ "./components/event-index-row": "./src/components/event-index-row.tsx",
67
+ "./components/event-list-row": "./src/components/event-list-row.tsx",
68
+ "./components/event-list-row-wide": "./src/components/event-list-row-wide.tsx",
69
+ "./components/event-poster-fallback": "./src/components/event-poster-fallback.tsx",
70
+ "./components/event-tonight-strip": "./src/components/event-tonight-strip.tsx",
71
+ "./components/event-types": "./src/components/event-types.ts",
72
+ "./components/events-empty-state": "./src/components/events-empty-state.tsx",
73
+ "./components/explainer-steps": "./src/components/explainer-steps.tsx",
74
+ "./components/facet-group": "./src/components/facet-group.tsx",
75
+ "./components/featured-rail-item": "./src/components/featured-rail-item.tsx",
76
+ "./components/field": "./src/components/field.tsx",
77
+ "./components/filter-category-row": "./src/components/filter-category-row.tsx",
78
+ "./components/form": "./src/components/form.tsx",
79
+ "./components/glass-ground": "./src/components/glass-ground.tsx",
80
+ "./components/grade-atom": "./src/components/grade-atom.tsx",
81
+ "./components/haptic-switch": "./src/components/haptic-switch.tsx",
82
+ "./components/hold-countdown": "./src/components/hold-countdown.tsx",
83
+ "./components/hub-surface": "./src/components/hub-surface.tsx",
84
+ "./components/hue-chip": "./src/components/hue-chip.tsx",
85
+ "./components/icon": "./src/components/icon.tsx",
86
+ "./components/input": "./src/components/input.tsx",
87
+ "./components/input-group": "./src/components/input-group.tsx",
88
+ "./components/input-otp": "./src/components/input-otp.tsx",
89
+ "./components/item": "./src/components/item.tsx",
90
+ "./components/kbd": "./src/components/kbd.tsx",
91
+ "./components/label": "./src/components/label.tsx",
92
+ "./components/kitchen-dossier": "./src/components/kitchen-dossier.tsx",
93
+ "./components/lazy-markdown": "./src/components/lazy-markdown.tsx",
94
+ "./components/link-button": "./src/components/link-button.tsx",
95
+ "./components/markdown-renderer": "./src/components/markdown-renderer.tsx",
96
+ "./components/menu-dead-end": "./src/components/menu-dead-end.tsx",
97
+ "./components/menu-family-block": "./src/components/menu-family-block.tsx",
98
+ "./components/menu-family-card": "./src/components/menu-family-card.tsx",
99
+ "./components/menu-field": "./src/components/menu-field.tsx",
100
+ "./components/menu-filter-chips": "./src/components/menu-filter-chips.tsx",
101
+ "./components/menu-group-rows": "./src/components/menu-group-rows.tsx",
102
+ "./components/menu-hub-hero": "./src/components/menu-hub-hero.tsx",
103
+ "./components/menu-index-header": "./src/components/menu-index-header.tsx",
104
+ "./components/menu-index-list": "./src/components/menu-index-list.tsx",
105
+ "./components/menu-intent-hero": "./src/components/menu-intent-hero.tsx",
106
+ "./components/menu-item": "./src/components/menu-item.tsx",
107
+ "./components/menu-pick-row": "./src/components/menu-pick-row.tsx",
108
+ "./components/menu-kitchen-list": "./src/components/menu-kitchen-list.tsx",
109
+ "./components/menu-mocks": "./src/components/menu-mocks.ts",
110
+ "./components/menu-types": "./src/components/menu-types.ts",
111
+ "./components/motion-icon": "./src/components/motion-icon.tsx",
112
+ "./components/motion-icon-actions": "./src/components/motion-icon-actions.tsx",
113
+ "./components/motion-icon-controls": "./src/components/motion-icon-controls.tsx",
114
+ "./components/motion-icon-set": "./src/components/motion-icon-set.tsx",
115
+ "./components/multi-select-combobox": "./src/components/multi-select-combobox.tsx",
116
+ "./components/multi-select-menu": "./src/components/multi-select-menu.tsx",
117
+ "./components/native-select": "./src/components/native-select.tsx",
118
+ "./components/open-now-list": "./src/components/open-now-list.tsx",
119
+ "./components/pagination": "./src/components/pagination.tsx",
120
+ "./components/pan-zoom-viewport": "./src/components/pan-zoom-viewport.tsx",
121
+ "./components/party-counter": "./src/components/party-counter.tsx",
122
+ "./components/phone-frame": "./src/components/phone-frame.tsx",
123
+ "./components/photo-ground": "./src/components/photo-ground.tsx",
124
+ "./components/place-about-tab": "./src/components/place-about-tab.tsx",
125
+ "./components/place-card": "./src/components/place-card.tsx",
126
+ "./components/place-details-mobile": "./src/components/place-details-mobile.tsx",
127
+ "./components/place-schedule-week": "./src/components/place-schedule-week.tsx",
128
+ "./components/place-types": "./src/components/place-types.ts",
129
+ "./components/popover": "./src/components/popover.tsx",
130
+ "./components/poster-stack": "./src/components/poster-stack.tsx",
131
+ "./components/poster-tape": "./src/components/poster-tape.tsx",
132
+ "./components/poster-wall": "./src/components/poster-wall.tsx",
133
+ "./components/progress": "./src/components/progress.tsx",
134
+ "./components/qa-block": "./src/components/qa-block.tsx",
135
+ "./components/qr-panel": "./src/components/qr-panel.tsx",
136
+ "./components/quiet-stretch": "./src/components/quiet-stretch.tsx",
137
+ "./components/radio-group": "./src/components/radio-group.tsx",
138
+ "./components/reconcile-bar": "./src/components/reconcile-bar.tsx",
139
+ "./components/responsive-overlay": "./src/components/responsive-overlay.tsx",
140
+ "./components/rating": "./src/components/rating.tsx",
141
+ "./components/scroll-area": "./src/components/scroll-area.tsx",
142
+ "./components/scroll-fade": "./src/components/scroll-fade.tsx",
143
+ "./components/search-bar": "./src/components/search-bar.tsx",
144
+ "./components/searchable-select": "./src/components/searchable-select.tsx",
145
+ "./components/section-header": "./src/components/section-header.tsx",
146
+ "./components/segmented-nav": "./src/components/segmented-nav.tsx",
147
+ "./components/select": "./src/components/select.tsx",
148
+ "./components/selection-bar": "./src/components/selection-bar.tsx",
149
+ "./components/separator": "./src/components/separator.tsx",
150
+ "./components/sheet": "./src/components/sheet.tsx",
151
+ "./components/signature-chip": "./src/components/signature-chip.tsx",
152
+ "./components/skeleton": "./src/components/skeleton.tsx",
153
+ "./components/skeleton-card": "./src/components/skeleton-card.tsx",
154
+ "./components/slider": "./src/components/slider.tsx",
155
+ "./components/sliding-panels": "./src/components/sliding-panels.tsx",
156
+ "./components/sonner": "./src/components/sonner.tsx",
157
+ "./components/sort-option-list": "./src/components/sort-option-list.tsx",
158
+ "./components/spinner": "./src/components/spinner.tsx",
159
+ "./components/split-share-row": "./src/components/split-share-row.tsx",
160
+ "./components/stat-banner": "./src/components/stat-banner.tsx",
161
+ "./components/stepper": "./src/components/stepper.tsx",
162
+ "./components/sticky-container": "./src/components/sticky-container.tsx",
163
+ "./components/story": "./src/components/story.tsx",
164
+ "./components/switch": "./src/components/switch.tsx",
165
+ "./components/table": "./src/components/table.tsx",
166
+ "./components/table-ledger-card": "./src/components/table-ledger-card.tsx",
167
+ "./components/table-trigger-band": "./src/components/table-trigger-band.tsx",
168
+ "./components/tabs": "./src/components/tabs.tsx",
169
+ "./components/tag": "./src/components/tag.tsx",
170
+ "./components/tag-group": "./src/components/tag-group.tsx",
171
+ "./components/text": "./src/components/text.tsx",
172
+ "./components/textarea": "./src/components/textarea.tsx",
173
+ "./components/time-slot": "./src/components/time-slot.tsx",
174
+ "./components/timeline-day": "./src/components/timeline-day.tsx",
175
+ "./components/timeline-hour": "./src/components/timeline-hour.tsx",
176
+ "./components/timeline-hour-puck": "./src/components/timeline-hour-puck.tsx",
177
+ "./components/toggle": "./src/components/toggle.tsx",
178
+ "./components/toggle-group": "./src/components/toggle-group.tsx",
179
+ "./components/tooltip": "./src/components/tooltip.tsx",
180
+ "./components/top-comparison": "./src/components/top-comparison.tsx",
181
+ "./components/top-cross-link": "./src/components/top-cross-link.tsx",
182
+ "./components/top-ranking-entry": "./src/components/top-ranking-entry.tsx",
183
+ "./components/top-ranking-hero": "./src/components/top-ranking-hero.tsx",
184
+ "./components/top-ranking-legend": "./src/components/top-ranking-legend.tsx",
185
+ "./components/top-types": "./src/components/top-types.ts",
186
+ "./components/use-mobile": "./src/components/use-mobile.tsx",
187
+ "./components/venue-events-block": "./src/components/venue-events-block.tsx",
188
+ "./components/venue-grade-dimensions": "./src/components/venue-grade-dimensions.tsx",
189
+ "./components/venue-quotes": "./src/components/venue-quotes.tsx",
190
+ "./components/venue-wall": "./src/components/venue-wall.tsx",
191
+ "./components/weekday-chip": "./src/components/weekday-chip.tsx",
192
+ "./components/weekday-theme-banner": "./src/components/weekday-theme-banner.tsx",
193
+ "./components/weekend-panel": "./src/components/weekend-panel.tsx",
19
194
  "./globals.css": "./src/globals.css",
20
- "./components/*": {
21
- "types": "./dist/components/*.d.ts",
22
- "import": "./dist/components/*.js"
23
- },
24
- "./hooks/*": {
25
- "types": "./dist/hooks/*.d.ts",
26
- "import": "./dist/hooks/*.js"
27
- },
28
- "./lib/*": {
29
- "types": "./dist/lib/*.d.ts",
30
- "import": "./dist/lib/*.js"
195
+ "./hooks/use-drag-swipe": "./src/hooks/use-drag-swipe.ts",
196
+ "./hooks/use-haptic": "./src/hooks/use-haptic.ts",
197
+ "./hooks/use-media-query": "./src/hooks/use-media-query.ts",
198
+ "./hooks/use-mobile": "./src/hooks/use-mobile.ts",
199
+ "./hooks/use-mounted-theme": "./src/hooks/use-mounted-theme.ts",
200
+ "./hooks/use-sticky": "./src/hooks/use-sticky.ts",
201
+ "./lib/card-hover": "./src/lib/card-hover.ts",
202
+ "./lib/grade": "./src/lib/grade.ts",
203
+ "./lib/icon-depth": "./src/lib/icon-depth.ts",
204
+ "./lib/icon-registry": "./src/lib/icon-registry.ts",
205
+ "./lib/motion": "./src/lib/motion.ts",
206
+ "./lib/overlay-pill": "./src/lib/overlay-pill.ts",
207
+ "./lib/utils": "./src/lib/utils.ts"
208
+ },
209
+ "publishConfig": {
210
+ "exports": {
211
+ "./globals.css": "./src/globals.css",
212
+ "./components/*": {
213
+ "types": "./dist/components/*.d.ts",
214
+ "import": "./dist/components/*.js"
215
+ },
216
+ "./hooks/*": {
217
+ "types": "./dist/hooks/*.d.ts",
218
+ "import": "./dist/hooks/*.js"
219
+ },
220
+ "./lib/*": {
221
+ "types": "./dist/lib/*.d.ts",
222
+ "import": "./dist/lib/*.js"
223
+ }
31
224
  }
32
225
  },
226
+ "scripts": {
227
+ "storybook": "storybook dev -p 6006",
228
+ "build-storybook": "storybook build",
229
+ "build": "tsup",
230
+ "dev": "tsup --watch",
231
+ "typecheck": "tsc --noEmit",
232
+ "test": "vitest run",
233
+ "test:watch": "vitest",
234
+ "clean": "rm -rf dist",
235
+ "prepublishOnly": "pnpm run build"
236
+ },
33
237
  "peerDependencies": {
34
238
  "@hookform/resolvers": "^3.0.0 || ^5.0.0",
35
239
  "@radix-ui/react-accordion": "*",
@@ -138,15 +342,5 @@
138
342
  "vaul": "^1.1.2",
139
343
  "vitest": "^4.1.5",
140
344
  "zod": "^3.25.67"
141
- },
142
- "scripts": {
143
- "storybook": "storybook dev -p 6006",
144
- "build-storybook": "storybook build",
145
- "build": "tsup",
146
- "dev": "tsup --watch",
147
- "typecheck": "tsc --noEmit",
148
- "test": "vitest run",
149
- "test:watch": "vitest",
150
- "clean": "rm -rf dist"
151
345
  }
152
- }
346
+ }
@@ -10,6 +10,7 @@ const labels: PlaceCardLabels = {
10
10
  closed: "Closed",
11
11
  viewDetails: (name) => `View details for ${name}`,
12
12
  priceLevel: (level) => `Price level ${level} of 4`,
13
+ established: (year) => String(year),
13
14
  grade: {
14
15
  soft: "Worth exploring",
15
16
  bands: {
@@ -35,6 +36,7 @@ const basePlace: PlacePreview = {
35
36
  price_dishes: { price_range_low: 35, price_range_high: 90, price_score: 2 },
36
37
  reviews_digest: { grade: { overall: 8.4 } },
37
38
  cover_url: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&q=80",
39
+ launched_at: "2020-02",
38
40
  };
39
41
 
40
42
  const meta = {
@@ -101,6 +103,40 @@ export const WithFooter: Story = {
101
103
  },
102
104
  };
103
105
 
106
+ /**
107
+ * The launch year closes the meta line, bare — fe's copy; `labels.established`
108
+ * may spell it out ("Est. 2020") where there is room. It is the backend's
109
+ * earliest-review estimate (`launched_at`, `YYYY-MM`). No year without the
110
+ * label or without a well-formed `launched_at`.
111
+ */
112
+ export const Established: Story = {
113
+ args: { place: { ...basePlace, launched_at: "2012-09" } },
114
+ };
115
+
116
+ /** 220px rail with a long cuisine and full price: the cuisine gives way, price and year never do. */
117
+ export const EstablishedNarrowRail: Story = {
118
+ args: {
119
+ place: { ...basePlace, cuisines: ["mediterranean"], price_dishes: { price_score: 4 } },
120
+ },
121
+ decorators: [
122
+ (Story) => (
123
+ <div className="w-[220px]">
124
+ <Story />
125
+ </div>
126
+ ),
127
+ ],
128
+ };
129
+
130
+ /** Spelled out by the label, for a consumer with room for the word. */
131
+ export const EstablishedWithLabel: Story = {
132
+ args: { labels: { ...labels, established: (year) => `Est. ${year}` } },
133
+ };
134
+
135
+ /** The year alone still gets a meta line: no cuisine, no price signal. */
136
+ export const EstablishedOnly: Story = {
137
+ args: { place: { ...basePlace, cuisines: undefined, price_dishes: undefined } },
138
+ };
139
+
104
140
  export const Minimal: Story = {
105
141
  args: { place: { id: "2", slug: "tiny", name: "The Tiny Spot" } },
106
142
  };
@@ -9,6 +9,7 @@ const LABELS: PlaceCardLabels = {
9
9
  closed: "Closed",
10
10
  viewDetails: (name) => `View details for ${name}`,
11
11
  priceLevel: (level) => `Price level ${level} of 4`,
12
+ established: (year) => `Est. ${year}`,
12
13
  grade: {
13
14
  soft: "Worth exploring",
14
15
  bands: {
@@ -104,6 +105,34 @@ describe("PlaceCard", () => {
104
105
  expect(screen.getByLabelText("Price level 2 of 4")).toBeInTheDocument();
105
106
  });
106
107
 
108
+ it("closes the meta line with the launch year", () => {
109
+ render(
110
+ <PlaceCard
111
+ place={makePlace({ cuisines: ["italian"], launched_at: "2020-02" })}
112
+ labels={LABELS}
113
+ />
114
+ );
115
+ expect(screen.getByText("Est. 2020")).toBeInTheDocument();
116
+ });
117
+
118
+ it("shows the launch year on its own when there is no cuisine or price", () => {
119
+ const { container } = render(
120
+ <PlaceCard place={makePlace({ launched_at: "2012-09" })} labels={LABELS} />
121
+ );
122
+ expect(screen.getByText("Est. 2012")).toBeInTheDocument();
123
+ expect(container).not.toHaveTextContent("·");
124
+ });
125
+
126
+ it("shows no year without the label or a well-formed launched_at", () => {
127
+ const { established: _omit, ...withoutEstablished } = LABELS;
128
+ const { rerender } = render(
129
+ <PlaceCard place={makePlace({ launched_at: "2020-02" })} labels={withoutEstablished} />
130
+ );
131
+ expect(screen.queryByText(/2020/)).not.toBeInTheDocument();
132
+ rerender(<PlaceCard place={makePlace({ launched_at: "2020" })} labels={LABELS} />);
133
+ expect(screen.queryByText(/Est\./)).not.toBeInTheDocument();
134
+ });
135
+
107
136
  it("clamps an out-of-range price level instead of throwing", () => {
108
137
  render(<PlaceCard place={makePlace({ price_drinks: { price_score: 7 } })} labels={LABELS} />);
109
138
  expect(screen.getByLabelText("Price level 7 of 4")).toHaveTextContent("$$$$");
@@ -169,6 +169,12 @@ function PriceLevel({ level, label }: { level: number; label: string }) {
169
169
  );
170
170
  }
171
171
 
172
+ /** The year out of the schema's `YYYY-MM`; anything else shows no year. */
173
+ function launchYear(launchedAt: string | undefined): number | undefined {
174
+ const match = launchedAt ? /^(\d{4})-\d{2}$/.exec(launchedAt) : null;
175
+ return match ? Number(match[1]) : undefined;
176
+ }
177
+
172
178
  const STATIC_ROOT_CLASSES = `group relative flex w-full select-none flex-col ${HOVER_MOTION} ${HOVER_LIFT_GRID_GROUP}`;
173
179
  const ROOT_CLASSES = `${STATIC_ROOT_CLASSES} cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:rounded-xl`;
174
180
 
@@ -202,6 +208,8 @@ export const PlaceCard = memo(function PlaceCard({
202
208
  const neighbourhood = place.areas?.[0]?.name;
203
209
  const priceLevel = place.price_dishes?.price_score ?? place.price_drinks?.price_score;
204
210
  const showPrice = priceLevel !== undefined && priceLevel > 0;
211
+ const year = labels.established ? launchYear(place.launched_at) : undefined;
212
+ const established = year !== undefined ? labels.established?.(year) : undefined;
205
213
  const ariaLabel = labels.viewDetails(place.name);
206
214
 
207
215
  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
@@ -277,11 +285,22 @@ export const PlaceCard = memo(function PlaceCard({
277
285
  </div>
278
286
 
279
287
  <div className="flex flex-col gap-0.5">
280
- {(cuisine || showPrice) && (
281
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
282
- {cuisine && <span className="capitalize">{cuisine}</span>}
283
- {cuisine && showPrice && <span className="text-border">·</span>}
284
- {showPrice && <PriceLevel level={priceLevel} label={labels.priceLevel(priceLevel)} />}
288
+ {/* Cuisine · price · launch year. At rail width the three can
289
+ outrun the card, so the cuisine is the part that gives way: the
290
+ price glyphs and the year are short and fixed. */}
291
+ {(cuisine || showPrice || established) && (
292
+ <div className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
293
+ {cuisine && <span className="min-w-0 truncate capitalize">{cuisine}</span>}
294
+ {cuisine && showPrice && <span className="shrink-0 text-border">·</span>}
295
+ {showPrice && (
296
+ <span className="shrink-0">
297
+ <PriceLevel level={priceLevel} label={labels.priceLevel(priceLevel)} />
298
+ </span>
299
+ )}
300
+ {established && (cuisine || showPrice) && (
301
+ <span className="shrink-0 text-border">·</span>
302
+ )}
303
+ {established && <span className="shrink-0 whitespace-nowrap">{established}</span>}
285
304
  </div>
286
305
  )}
287
306
 
@@ -58,6 +58,12 @@ export type PlacePreview = {
58
58
  * bare storage key straight to `next/image`'s loader).
59
59
  */
60
60
  cover_url?: string
61
+ /**
62
+ * Year and month the place launched (`YYYY-MM`), as the schema ships it.
63
+ * The backend approximates it from the earliest review it holds, so it is a
64
+ * floor rather than a founding date. `<PlaceCard>` shows the year only.
65
+ */
66
+ launched_at?: string
61
67
  }
62
68
 
63
69
  export type PlaceHourBlock = { opens_at: number; closes_at: number }
@@ -193,6 +199,11 @@ export type PlaceCardLabels = {
193
199
  viewDetails: (name: string) => string
194
200
  /** Accessible name of the `$` price glyphs, e.g. "Price level 2 of 4". */
195
201
  priceLevel: (level: number) => string
202
+ /**
203
+ * The launch year on the meta line, e.g. "Est. 2020" / "Din 2020". Optional:
204
+ * without it the card shows no year even when `launched_at` is set.
205
+ */
206
+ established?: (year: number) => string
196
207
  grade: GradeAtomLabels
197
208
  }
198
209