@bearmenu/ui 0.1.0 → 0.5.0

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 (119) hide show
  1. package/README.md +116 -0
  2. package/dist/{chunk-2CUFS5LF.js → chunk-2BMJRXQX.js} +1 -1
  3. package/dist/chunk-HLQ2IRE7.js +279 -0
  4. package/dist/chunk-IQASCH2V.js +25 -0
  5. package/dist/{chunk-UXAYXRFN.js → chunk-QJ54Z7TC.js} +15 -1
  6. package/dist/{chunk-X6GATUI2.js → chunk-UISA4CIG.js} +1 -0
  7. package/dist/components/alert-dialog.js +1 -1
  8. package/dist/components/alert.d.ts +1 -1
  9. package/dist/components/badge.d.ts +1 -1
  10. package/dist/components/button.d.ts +1 -1
  11. package/dist/components/button.js +1 -1
  12. package/dist/components/calendar.d.ts +9 -7
  13. package/dist/components/calendar.js +153 -39
  14. package/dist/components/card.d.ts +2 -1
  15. package/dist/components/card.js +1 -1
  16. package/dist/components/carousel.js +1 -1
  17. package/dist/components/checkbox.js +3 -25
  18. package/dist/components/command.d.ts +13 -13
  19. package/dist/components/dropdown-menu.js +1 -1
  20. package/dist/components/empty-state.js +1 -1
  21. package/dist/components/haptic-button.js +1 -1
  22. package/dist/components/link-button.js +1 -1
  23. package/dist/components/multi-select-combobox.js +1 -1
  24. package/dist/components/multi-select-menu.d.ts +26 -0
  25. package/dist/components/multi-select-menu.js +71 -0
  26. package/dist/components/pagination.js +1 -1
  27. package/dist/components/phone-frame.d.ts +17 -0
  28. package/dist/components/phone-frame.js +41 -0
  29. package/dist/components/place-about-tab.d.ts +17 -0
  30. package/dist/components/place-about-tab.js +6 -0
  31. package/dist/components/place-card.d.ts +17 -0
  32. package/dist/components/place-card.js +145 -0
  33. package/dist/components/place-details-mobile.d.ts +18 -0
  34. package/dist/components/place-details-mobile.js +187 -0
  35. package/dist/components/place-types.d.ts +145 -0
  36. package/dist/components/place-types.js +1 -0
  37. package/dist/components/search-bar.js +1 -1
  38. package/dist/components/searchable-select.js +1 -1
  39. package/dist/components/skeleton-card.js +1 -1
  40. package/dist/components/skeleton.js +1 -1
  41. package/dist/components/sliding-panels.js +1 -1
  42. package/dist/components/sonner.js +11 -2
  43. package/dist/components/tabs.js +1 -1
  44. package/dist/components/text.d.ts +3 -3
  45. package/dist/components/toaster.js +1 -1
  46. package/package.json +28 -9
  47. package/src/components/accordion.stories.tsx +51 -0
  48. package/src/components/alert-dialog.stories.tsx +45 -0
  49. package/src/components/alert.stories.tsx +85 -0
  50. package/src/components/aspect-ratio.stories.tsx +47 -0
  51. package/src/components/avatar.stories.tsx +66 -0
  52. package/src/components/badge.stories.tsx +60 -0
  53. package/src/components/breadcrumb.stories.tsx +63 -0
  54. package/src/components/button.stories.tsx +115 -0
  55. package/src/components/button.tsx +1 -1
  56. package/src/components/calendar.stories.tsx +47 -0
  57. package/src/components/calendar.tsx +160 -40
  58. package/src/components/card.stories.tsx +72 -0
  59. package/src/components/card.tsx +13 -1
  60. package/src/components/carousel.stories.tsx +62 -0
  61. package/src/components/checkbox.stories.tsx +54 -0
  62. package/src/components/checkbox.tsx +1 -1
  63. package/src/components/collapsible.stories.tsx +34 -0
  64. package/src/components/command.stories.tsx +64 -0
  65. package/src/components/currency-input.stories.tsx +50 -0
  66. package/src/components/dialog.stories.tsx +75 -0
  67. package/src/components/drawer.stories.tsx +47 -0
  68. package/src/components/dropdown-menu.stories.tsx +64 -0
  69. package/src/components/dropdown-menu.tsx +1 -1
  70. package/src/components/empty-state.stories.tsx +67 -0
  71. package/src/components/form.stories.tsx +73 -0
  72. package/src/components/haptic-button.stories.tsx +41 -0
  73. package/src/components/input-otp.stories.tsx +42 -0
  74. package/src/components/input.stories.tsx +41 -0
  75. package/src/components/label.stories.tsx +35 -0
  76. package/src/components/markdown-renderer.stories.tsx +45 -0
  77. package/src/components/multi-select-combobox.stories.tsx +78 -0
  78. package/src/components/multi-select-menu.tsx +96 -0
  79. package/src/components/pagination.stories.tsx +48 -0
  80. package/src/components/phone-frame.stories.tsx +51 -0
  81. package/src/components/phone-frame.test.tsx +82 -0
  82. package/src/components/phone-frame.tsx +59 -0
  83. package/src/components/place-about-tab.stories.tsx +164 -0
  84. package/src/components/place-about-tab.test.tsx +325 -0
  85. package/src/components/place-about-tab.tsx +474 -0
  86. package/src/components/place-card.stories.tsx +120 -0
  87. package/src/components/place-card.test.tsx +187 -0
  88. package/src/components/place-card.tsx +187 -0
  89. package/src/components/place-details-mobile.tsx +251 -0
  90. package/src/components/place-types.ts +127 -0
  91. package/src/components/popover.stories.tsx +42 -0
  92. package/src/components/progress.stories.tsx +35 -0
  93. package/src/components/radio-group.stories.tsx +46 -0
  94. package/src/components/rating.stories.tsx +45 -0
  95. package/src/components/scroll-area.stories.tsx +48 -0
  96. package/src/components/search-bar.stories.tsx +92 -0
  97. package/src/components/searchable-select.stories.tsx +55 -0
  98. package/src/components/select.stories.tsx +92 -0
  99. package/src/components/separator.stories.tsx +33 -0
  100. package/src/components/sheet.stories.tsx +95 -0
  101. package/src/components/skeleton-card.stories.tsx +53 -0
  102. package/src/components/skeleton.stories.tsx +42 -0
  103. package/src/components/skeleton.tsx +1 -0
  104. package/src/components/slider.stories.tsx +27 -0
  105. package/src/components/sliding-panels.stories.tsx +68 -0
  106. package/src/components/sonner.tsx +17 -2
  107. package/src/components/sticky-container.stories.tsx +33 -0
  108. package/src/components/switch.stories.tsx +35 -0
  109. package/src/components/table.stories.tsx +60 -0
  110. package/src/components/tabs.stories.tsx +53 -0
  111. package/src/components/tabs.tsx +1 -1
  112. package/src/components/text.stories.tsx +82 -0
  113. package/src/components/textarea.stories.tsx +35 -0
  114. package/src/components/toast.stories.tsx +63 -0
  115. package/src/components/toggle-group.stories.tsx +60 -0
  116. package/src/components/toggle.stories.tsx +48 -0
  117. package/src/components/tooltip.stories.tsx +71 -0
  118. package/src/globals.css +14 -2
  119. package/src/test/setup.ts +9 -0
@@ -0,0 +1,145 @@
1
+ import { Text } from '../chunk-4KOMHDZD.js';
2
+ import { cn } from '../chunk-FEK5XGZW.js';
3
+ import '../chunk-3GWWZKX7.js';
4
+ import { memo, useState } from 'react';
5
+ import { UtensilsCrossed, Star, Wine, MapPin, Clock } from 'lucide-react';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+
8
+ function formatDistance(meters) {
9
+ return meters ? `${(meters / 1e3).toFixed(1)} km` : "\u2014";
10
+ }
11
+ function formatRating(rating) {
12
+ return (rating == null ? void 0 : rating.rating) ? rating.rating.toFixed(1) : null;
13
+ }
14
+ var PlaceCard = memo(function PlaceCard2({
15
+ place,
16
+ labels,
17
+ onClick,
18
+ className,
19
+ priority
20
+ }) {
21
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
22
+ const [imgError, setImgError] = useState(false);
23
+ const ratingDisplay = formatRating(place.rating);
24
+ const reviewCount = (_b = place.review_count) != null ? _b : (_a = place.rating) == null ? void 0 : _a.count;
25
+ const showHeader = !!place.thumbnail_url && !imgError;
26
+ const distanceMeters = (_d = (_c = place.location) == null ? void 0 : _c.distance) == null ? void 0 : _d.meters;
27
+ const schedule = (_g = (_e = place.today_schedule) == null ? void 0 : _e[0]) != null ? _g : (_f = place.hours) == null ? void 0 : _f[0];
28
+ const scheduleHour = place.open_now ? schedule == null ? void 0 : schedule.closes_at : schedule == null ? void 0 : schedule.opens_at;
29
+ const scheduleLabel = place.open_now ? labels.until : labels.opensAt;
30
+ const sharedClasses = cn(
31
+ "glass rounded-3xl border border-transparent bg-transparent p-0 text-left text-card-foreground transition-transform duration-300 ease-in-out hover:border-border/20 h-full flex flex-col w-full",
32
+ onClick && "cursor-pointer [@media(hover:hover)]:hover:-translate-y-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
33
+ className
34
+ );
35
+ const content = /* @__PURE__ */ jsxs("div", { className: "p-4 flex-1 flex flex-col", children: [
36
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 mb-3", children: [
37
+ /* @__PURE__ */ jsx("div", { className: "w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center overflow-hidden flex-shrink-0", children: showHeader ? (
38
+ // eslint-disable-next-line @next/next/no-img-element
39
+ /* @__PURE__ */ jsx(
40
+ "img",
41
+ {
42
+ src: place.thumbnail_url,
43
+ alt: place.name,
44
+ title: place.name,
45
+ width: 48,
46
+ height: 48,
47
+ loading: priority ? "eager" : "lazy",
48
+ decoding: "async",
49
+ className: "w-full h-full object-cover",
50
+ onError: () => setImgError(true)
51
+ }
52
+ )
53
+ ) : /* @__PURE__ */ jsx(
54
+ UtensilsCrossed,
55
+ {
56
+ className: "w-6 h-6 text-primary/60",
57
+ "aria-hidden": "true"
58
+ }
59
+ ) }),
60
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
61
+ /* @__PURE__ */ jsx(Text, { as: "h3", weight: "bold", leading: "tight", color: "default", className: "text-lg mb-2", children: place.name }),
62
+ ratingDisplay !== null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
63
+ /* @__PURE__ */ jsx(Star, { className: "w-4 h-4 fill-primary text-primary", "aria-hidden": "true" }),
64
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ratingDisplay }),
65
+ reviewCount !== void 0 && reviewCount !== null && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
66
+ "(",
67
+ reviewCount,
68
+ ")"
69
+ ] })
70
+ ] })
71
+ ] })
72
+ ] }),
73
+ (place.price_dishes || place.price_drinks) && /* @__PURE__ */ jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 text-xs", children: [
74
+ ((_h = place.price_dishes) == null ? void 0 : _h.price_range_low) != null && ((_i = place.price_dishes) == null ? void 0 : _i.price_range_high) != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-primary", children: [
75
+ /* @__PURE__ */ jsx(UtensilsCrossed, { className: "w-4 h-4", "aria-hidden": "true" }),
76
+ /* @__PURE__ */ jsxs("span", { children: [
77
+ place.price_dishes.price_range_low,
78
+ "-",
79
+ place.price_dishes.price_range_high,
80
+ " ",
81
+ labels.currency
82
+ ] })
83
+ ] }),
84
+ ((_j = place.price_drinks) == null ? void 0 : _j.price_range_low) != null && ((_k = place.price_drinks) == null ? void 0 : _k.price_range_high) != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-primary", children: [
85
+ /* @__PURE__ */ jsx(Wine, { className: "w-4 h-4", "aria-hidden": "true" }),
86
+ /* @__PURE__ */ jsxs("span", { children: [
87
+ place.price_drinks.price_range_low,
88
+ "-",
89
+ place.price_drinks.price_range_high,
90
+ " ",
91
+ labels.currency
92
+ ] })
93
+ ] })
94
+ ] }) }),
95
+ /* @__PURE__ */ jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-primary", children: [
96
+ distanceMeters !== void 0 && distanceMeters !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
97
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
98
+ /* @__PURE__ */ jsx(MapPin, { className: "w-3 h-3", "aria-hidden": "true" }),
99
+ /* @__PURE__ */ jsx("span", { className: "text-xs", children: formatDistance(distanceMeters) })
100
+ ] }),
101
+ /* @__PURE__ */ jsx("span", { children: "\xB7" })
102
+ ] }),
103
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
104
+ /* @__PURE__ */ jsx(
105
+ Clock,
106
+ {
107
+ className: cn(
108
+ "w-3 h-3",
109
+ place.open_now ? "text-green-500" : "text-muted-foreground"
110
+ ),
111
+ "aria-hidden": "true"
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsx(
115
+ "span",
116
+ {
117
+ className: cn(
118
+ "text-xs",
119
+ place.open_now ? "text-green-500" : "text-muted-foreground"
120
+ ),
121
+ children: place.open_now ? labels.open : labels.closed
122
+ }
123
+ ),
124
+ scheduleHour != null && /* @__PURE__ */ jsx(
125
+ "span",
126
+ {
127
+ className: cn(
128
+ "text-xs",
129
+ place.open_now ? "text-green-500" : "text-muted-foreground"
130
+ ),
131
+ children: `\xB7 ${scheduleLabel} ${String(scheduleHour).padStart(2, "0")}:00`
132
+ }
133
+ )
134
+ ] })
135
+ ] }) }),
136
+ place.description && /* @__PURE__ */ jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsx(Text, { as: "p", variant: "caption", color: "muted", className: "line-clamp-2", children: place.description }) })
137
+ ] });
138
+ if (onClick) {
139
+ return /* @__PURE__ */ jsx("button", { type: "button", className: sharedClasses, onClick: () => onClick(place), children: content });
140
+ }
141
+ return /* @__PURE__ */ jsx("div", { className: sharedClasses, children: content });
142
+ });
143
+ PlaceCard.displayName = "PlaceCard";
144
+
145
+ export { PlaceCard };
@@ -0,0 +1,18 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { PlacePreview, PlaceDetailsMobileLabels } from './place-types.js';
3
+
4
+ interface PlaceDetailsMobileProps {
5
+ place: PlacePreview;
6
+ labels: PlaceDetailsMobileLabels;
7
+ /** Pre-resolved cover image URL. When omitted or it 404s, an icon fallback renders. */
8
+ coverImageUrl?: string;
9
+ className?: string;
10
+ }
11
+ /**
12
+ * Read-only mobile preview of a place's details page with the About tab selected.
13
+ * Used in admin's live profile preview (inside `<PhoneFrame>`). Pure presentational
14
+ * — all i18n via `labels`, no router/analytics. Tabs are visual only.
15
+ */
16
+ declare function PlaceDetailsMobile({ place, labels, coverImageUrl, className, }: PlaceDetailsMobileProps): react_jsx_runtime.JSX.Element;
17
+
18
+ export { PlaceDetailsMobile, type PlaceDetailsMobileProps };
@@ -0,0 +1,187 @@
1
+ import { PlaceAboutTab } from '../chunk-HLQ2IRE7.js';
2
+ import '../chunk-QJ54Z7TC.js';
3
+ import { Text } from '../chunk-4KOMHDZD.js';
4
+ import { Badge } from '../chunk-NKHH23YK.js';
5
+ import { cn } from '../chunk-FEK5XGZW.js';
6
+ import '../chunk-3GWWZKX7.js';
7
+ import { useState } from 'react';
8
+ import { UtensilsCrossed, Star, DollarSign, Clock, Info, MessageSquare, CalendarDays } from 'lucide-react';
9
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
+
11
+ function formatHour(h) {
12
+ return `${h.toString().padStart(2, "0")}:00`;
13
+ }
14
+ function formatRating(rating) {
15
+ return (rating == null ? void 0 : rating.rating) ? rating.rating.toFixed(1) : null;
16
+ }
17
+ function priceLevelFromRanges(place) {
18
+ var _a, _b, _c, _d, _e, _f, _g, _h;
19
+ const dish = (_d = (_c = (_a = place.price_dishes) == null ? void 0 : _a.price_range_high) != null ? _c : (_b = place.price_dishes) == null ? void 0 : _b.price_range_low) != null ? _d : 0;
20
+ const drink = (_h = (_g = (_e = place.price_drinks) == null ? void 0 : _e.price_range_high) != null ? _g : (_f = place.price_drinks) == null ? void 0 : _f.price_range_low) != null ? _h : 0;
21
+ if (dish === 0 && drink === 0) return null;
22
+ const avg = dish > 0 && drink > 0 ? Math.round((dish + drink) / 2) : dish || drink;
23
+ return Math.max(1, Math.min(5, avg));
24
+ }
25
+ function getClosingHour(hours, currentHour) {
26
+ if (!hours.length) return null;
27
+ const active = hours.find((s) => s.opens_at <= currentHour && s.closes_at > currentHour);
28
+ if (active) return active.closes_at;
29
+ const last = hours[hours.length - 1];
30
+ return last ? last.closes_at : null;
31
+ }
32
+ function getNextOpeningToday(hours, currentHour) {
33
+ if (!hours.length) return null;
34
+ const later = hours.find((s) => s.opens_at > currentHour);
35
+ return later ? later.opens_at : null;
36
+ }
37
+ function PlaceDetailsMobile({
38
+ place,
39
+ labels,
40
+ coverImageUrl,
41
+ className
42
+ }) {
43
+ var _a, _b, _c;
44
+ const [imgError, setImgError] = useState(false);
45
+ const ratingDisplay = formatRating(place.rating);
46
+ const reviewCount = (_b = place.review_count) != null ? _b : (_a = place.rating) == null ? void 0 : _a.count;
47
+ const priceLevel = priceLevelFromRanges(place);
48
+ const hours = (_c = place.hours) != null ? _c : [];
49
+ const currentHour = (/* @__PURE__ */ new Date()).getHours();
50
+ const closingHour = place.open_now ? getClosingHour(hours, currentHour) : null;
51
+ const nextOpening = !place.open_now ? getNextOpeningToday(hours, currentHour) : null;
52
+ const showCover = !!coverImageUrl && !imgError;
53
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col bg-background", className), children: [
54
+ /* @__PURE__ */ jsxs("div", { className: "px-4 pt-4", children: [
55
+ /* @__PURE__ */ jsx("div", { className: "rounded-[22px] overflow-hidden relative", children: /* @__PURE__ */ jsxs("div", { className: "h-44 flex items-center justify-center relative overflow-hidden", children: [
56
+ /* @__PURE__ */ jsxs("div", { className: "absolute bottom-2 right-2 z-20 flex items-center gap-1.5 bg-surface border border-border px-2.5 h-7 rounded-[12px] text-[11px] font-medium text-foreground", children: [
57
+ /* @__PURE__ */ jsx(
58
+ "span",
59
+ {
60
+ className: cn(
61
+ "w-1.5 h-1.5 rounded-full flex-shrink-0",
62
+ place.open_now ? "bg-green-500" : "bg-muted-foreground"
63
+ )
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsxs("span", { children: [
67
+ place.open_now ? labels.statusBadge.openNow : labels.statusBadge.closed,
68
+ place.open_now && closingHour != null && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
69
+ " \xB7 ",
70
+ labels.statusBadge.closesAt,
71
+ " ",
72
+ formatHour(closingHour)
73
+ ] }),
74
+ !place.open_now && nextOpening != null && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
75
+ " \xB7 ",
76
+ labels.statusBadge.opensAt,
77
+ " ",
78
+ formatHour(nextOpening)
79
+ ] }),
80
+ !place.open_now && nextOpening == null && hours[0] && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
81
+ " \xB7 ",
82
+ labels.statusBadge.opensTomorrow,
83
+ " ",
84
+ formatHour(hours[0].opens_at)
85
+ ] })
86
+ ] })
87
+ ] }),
88
+ showCover ? (
89
+ // eslint-disable-next-line @next/next/no-img-element
90
+ /* @__PURE__ */ jsx(
91
+ "img",
92
+ {
93
+ src: coverImageUrl,
94
+ alt: place.name,
95
+ className: "absolute inset-0 w-full h-full object-cover",
96
+ onError: () => setImgError(true)
97
+ }
98
+ )
99
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
100
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-primary/20 via-primary/10 to-primary/5" }),
101
+ /* @__PURE__ */ jsxs("div", { className: "relative z-10 flex flex-col items-center justify-center gap-2 h-full", children: [
102
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-full bg-primary/20 flex items-center justify-center", children: /* @__PURE__ */ jsx(
103
+ UtensilsCrossed,
104
+ {
105
+ className: "w-8 h-8 text-primary",
106
+ strokeWidth: 1.5,
107
+ "aria-hidden": true
108
+ }
109
+ ) }),
110
+ /* @__PURE__ */ jsx(Text, { as: "p", variant: "caption", color: "muted", weight: "medium", children: labels.noImageAvailable })
111
+ ] })
112
+ ] })
113
+ ] }) }),
114
+ /* @__PURE__ */ jsxs("div", { className: "pt-4 px-1", children: [
115
+ /* @__PURE__ */ jsx("h1", { className: "text-[26px] leading-[1.05] font-bold tracking-tight text-foreground break-words", children: place.name }),
116
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12px] text-muted-foreground", children: [
117
+ ratingDisplay && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
118
+ /* @__PURE__ */ jsx(Star, { className: "w-3 h-3 text-primary fill-primary", "aria-hidden": true }),
119
+ /* @__PURE__ */ jsx("span", { className: "text-foreground font-semibold", children: ratingDisplay }),
120
+ reviewCount != null && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: labels.reviewCount(reviewCount) })
121
+ ] }),
122
+ priceLevel != null && /* @__PURE__ */ jsx("div", { className: "flex items-center -space-x-0.5", "aria-label": `Price level ${priceLevel} of 5`, children: Array.from({ length: 5 }, (_, i) => /* @__PURE__ */ jsx(
123
+ DollarSign,
124
+ {
125
+ className: cn(
126
+ "w-3 h-3",
127
+ i < priceLevel ? "text-foreground" : "text-muted-foreground/30"
128
+ ),
129
+ strokeWidth: 2.5,
130
+ "aria-hidden": true
131
+ },
132
+ i
133
+ )) }),
134
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
135
+ /* @__PURE__ */ jsx(Clock, { className: "w-3.5 h-3.5", "aria-hidden": true }),
136
+ /* @__PURE__ */ jsx(
137
+ "span",
138
+ {
139
+ className: cn(
140
+ place.open_now ? "text-green-600 dark:text-green-400" : "text-muted-foreground"
141
+ ),
142
+ children: place.open_now ? labels.statusBadge.openNow : labels.statusBadge.closed
143
+ }
144
+ )
145
+ ] })
146
+ ] }),
147
+ place.cuisines && place.cuisines.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-3 flex flex-wrap gap-1.5", children: place.cuisines.map((cuisine) => /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "capitalize text-[11px]", children: cuisine.replace(/-/g, " ") }, cuisine)) })
148
+ ] })
149
+ ] }),
150
+ /* @__PURE__ */ jsx("div", { className: "mt-5 px-4", children: /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxs("div", { className: "flex w-full", children: [
151
+ /* @__PURE__ */ jsx(MobileTabPlaceholder, { icon: /* @__PURE__ */ jsx(UtensilsCrossed, { className: "w-3.5 h-3.5" }), label: labels.tabs.menu }),
152
+ /* @__PURE__ */ jsx(
153
+ MobileTabPlaceholder,
154
+ {
155
+ icon: /* @__PURE__ */ jsx(Info, { className: "w-3.5 h-3.5" }),
156
+ label: labels.tabs.about,
157
+ active: true
158
+ }
159
+ ),
160
+ /* @__PURE__ */ jsx(MobileTabPlaceholder, { icon: /* @__PURE__ */ jsx(MessageSquare, { className: "w-3.5 h-3.5" }), label: labels.tabs.reviews }),
161
+ /* @__PURE__ */ jsx(MobileTabPlaceholder, { icon: /* @__PURE__ */ jsx(CalendarDays, { className: "w-3.5 h-3.5" }), label: labels.tabs.events })
162
+ ] }) }) }),
163
+ /* @__PURE__ */ jsx("div", { className: "pb-6", children: /* @__PURE__ */ jsx(PlaceAboutTab, { place, labels }) })
164
+ ] });
165
+ }
166
+ function MobileTabPlaceholder({
167
+ icon,
168
+ label,
169
+ active
170
+ }) {
171
+ return /* @__PURE__ */ jsxs(
172
+ "div",
173
+ {
174
+ className: cn(
175
+ "flex flex-1 min-w-0 items-center justify-center gap-1 px-1 py-2.5 text-[12px] font-medium border-b-2 -mb-px",
176
+ active ? "border-primary text-foreground" : "border-transparent text-muted-foreground"
177
+ ),
178
+ "aria-hidden": true,
179
+ children: [
180
+ /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", "aria-hidden": true, children: icon }),
181
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
182
+ ]
183
+ }
184
+ );
185
+ }
186
+
187
+ export { PlaceDetailsMobile };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Structural mirror of the openapi `Place` schema, decoupled from any one app's
3
+ * generated types. Both FE and admin map their domain shapes into this type
4
+ * before passing data to `<PlaceCard>` / `<PlaceAboutTab>`.
5
+ */
6
+ type PlacePreview = {
7
+ id: string;
8
+ slug: string;
9
+ name: string;
10
+ description?: string;
11
+ generative_summary?: string;
12
+ review_summary?: string;
13
+ open_now?: boolean;
14
+ rating?: {
15
+ rating: number;
16
+ count: number;
17
+ };
18
+ review_count?: number;
19
+ hours?: PlaceHourBlock[];
20
+ today_schedule?: PlaceHourBlock[];
21
+ location?: {
22
+ address?: string;
23
+ geolocation?: {
24
+ lat: number | string;
25
+ lon: number | string;
26
+ };
27
+ distance?: {
28
+ meters?: number;
29
+ };
30
+ };
31
+ price_dishes?: PlacePriceRange;
32
+ price_drinks?: PlacePriceRange;
33
+ cuisines?: string[];
34
+ amenities?: string[];
35
+ payment_options?: string[];
36
+ accessibility_features?: string[];
37
+ parking_options?: string[];
38
+ landmarks?: Array<{
39
+ name: string;
40
+ distance_meters?: number;
41
+ }>;
42
+ areas?: Array<{
43
+ name: string;
44
+ }>;
45
+ busyness?: Array<{
46
+ hour: number;
47
+ score: number;
48
+ }>;
49
+ type?: string;
50
+ business_status?: 'operational' | 'closed_temporarily' | 'closed_permanently';
51
+ phone?: string;
52
+ website_url?: string;
53
+ socials?: {
54
+ instagram_url?: string;
55
+ facebook_url?: string;
56
+ };
57
+ /**
58
+ * Pre-resolved thumbnail URL. Callers compute this themselves
59
+ * (FE: `https://cdn.bear.menu/places/${slug}-thumbnail.jpg`,
60
+ * admin: form's primary photo URL or blob URL).
61
+ */
62
+ thumbnail_url?: string;
63
+ };
64
+ type PlaceHourBlock = {
65
+ opens_at: number;
66
+ closes_at: number;
67
+ };
68
+ type PlacePriceRange = {
69
+ price_range_low?: number;
70
+ price_range_high?: number;
71
+ };
72
+ /** Labels passed to `<PlaceCard>`. All translation happens in the caller. */
73
+ type PlaceCardLabels = {
74
+ open: string;
75
+ closed: string;
76
+ until: string;
77
+ opensAt: string;
78
+ currency: string;
79
+ };
80
+ /**
81
+ * Labels passed to `<PlaceAboutTab>`. All translation happens in the caller.
82
+ *
83
+ * Dynamic suffix maps (`amenityLabels`, etc.) translate API enum values like
84
+ * `wheelchair_accessible_entrance` into human strings. Missing keys fall back
85
+ * to the raw enum value (with underscores → spaces).
86
+ */
87
+ type PlaceAboutTabLabels = {
88
+ overview: string;
89
+ description: string;
90
+ whatPeopleSay: string;
91
+ contactAndLocation: string;
92
+ address: string;
93
+ instagram: string;
94
+ facebook: string;
95
+ todayHours: string;
96
+ statusOpen: string;
97
+ statusClosed: string;
98
+ typicalBusyness: string;
99
+ cuisines: string;
100
+ amenities: string;
101
+ paymentOptions: string;
102
+ accessibility: string;
103
+ parking: string;
104
+ nearbyLandmarks: string;
105
+ neighborhood: string;
106
+ businessInformation: string;
107
+ type: string;
108
+ status: string;
109
+ amenityLabels: Record<string, string>;
110
+ paymentLabels: Record<string, string>;
111
+ accessibilityLabels: Record<string, string>;
112
+ parkingLabels: Record<string, string>;
113
+ typeLabels: Record<string, string>;
114
+ statusLabels: Record<string, string>;
115
+ };
116
+ /** Discriminator for `<PlaceAboutTab>`'s `onLinkClick` callback. */
117
+ type PlaceLinkKind = 'phone' | 'website' | 'instagram' | 'facebook' | 'address';
118
+ /**
119
+ * Labels passed to `<PlaceDetailsMobile>`. Extends `PlaceAboutTabLabels` with
120
+ * the header/tab-strip strings unique to the mobile place-details surface.
121
+ */
122
+ type PlaceDetailsMobileLabels = PlaceAboutTabLabels & {
123
+ /** Tab strip — "About" is highlighted, the rest are visual placeholders. */
124
+ tabs: {
125
+ menu: string;
126
+ about: string;
127
+ reviews: string;
128
+ events: string;
129
+ };
130
+ /** Status badge over the hero image and the inline open/closed pill in the info row. */
131
+ statusBadge: {
132
+ openNow: string;
133
+ closed: string;
134
+ /** Inserted with surrounding " · " separators. e.g. "Closes at" */
135
+ closesAt: string;
136
+ opensAt: string;
137
+ opensTomorrow: string;
138
+ };
139
+ /** Review count formatter. Pass a translator that handles ICU pluralization. */
140
+ reviewCount: (count: number) => string;
141
+ /** "No image available" fallback shown when no thumbnail is set. */
142
+ noImageAvailable: string;
143
+ };
144
+
145
+ export type { PlaceAboutTabLabels, PlaceCardLabels, PlaceDetailsMobileLabels, PlaceHourBlock, PlaceLinkKind, PlacePreview, PlacePriceRange };
@@ -0,0 +1 @@
1
+
@@ -1,4 +1,4 @@
1
- import { Card } from '../chunk-UXAYXRFN.js';
1
+ import { Card } from '../chunk-QJ54Z7TC.js';
2
2
  import '../chunk-4KOMHDZD.js';
3
3
  import { cn } from '../chunk-FEK5XGZW.js';
4
4
  import '../chunk-3GWWZKX7.js';
@@ -1,7 +1,7 @@
1
1
  import { Popover, PopoverTrigger, PopoverContent } from '../chunk-4UO73SKC.js';
2
2
  import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../chunk-7H54PTVH.js';
3
3
  import '../chunk-4AVS6CV4.js';
4
- import { Button } from '../chunk-2CUFS5LF.js';
4
+ import { Button } from '../chunk-2BMJRXQX.js';
5
5
  import '../chunk-R47NEOSI.js';
6
6
  import { cn } from '../chunk-FEK5XGZW.js';
7
7
  import '../chunk-3GWWZKX7.js';
@@ -1,4 +1,4 @@
1
- import { Skeleton } from '../chunk-X6GATUI2.js';
1
+ import { Skeleton } from '../chunk-UISA4CIG.js';
2
2
  import { AspectRatio } from '../chunk-3VMDCCCX.js';
3
3
  import { cn } from '../chunk-FEK5XGZW.js';
4
4
  import { __objRest, __spreadProps, __spreadValues } from '../chunk-3GWWZKX7.js';
@@ -1,3 +1,3 @@
1
- export { Skeleton } from '../chunk-X6GATUI2.js';
1
+ export { Skeleton } from '../chunk-UISA4CIG.js';
2
2
  import '../chunk-FEK5XGZW.js';
3
3
  import '../chunk-3GWWZKX7.js';
@@ -1,4 +1,4 @@
1
- import { Button } from '../chunk-2CUFS5LF.js';
1
+ import { Button } from '../chunk-2BMJRXQX.js';
2
2
  import '../chunk-R47NEOSI.js';
3
3
  import { cn } from '../chunk-FEK5XGZW.js';
4
4
  import '../chunk-3GWWZKX7.js';
@@ -1,4 +1,5 @@
1
1
  import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
2
+ import { Loader2Icon, OctagonXIcon, TriangleAlertIcon, InfoIcon, CircleCheckIcon } from 'lucide-react';
2
3
  import { useTheme } from 'next-themes';
3
4
  import { Toaster as Toaster$1 } from 'sonner';
4
5
  import { jsx } from 'react/jsx-runtime';
@@ -9,12 +10,20 @@ var Toaster = (_a) => {
9
10
  return /* @__PURE__ */ jsx(
10
11
  Toaster$1,
11
12
  __spreadValues({
12
- theme,
13
+ theme: theme != null ? theme : "system",
13
14
  className: "toaster group",
15
+ icons: {
16
+ success: /* @__PURE__ */ jsx(CircleCheckIcon, { className: "size-4" }),
17
+ info: /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" }),
18
+ warning: /* @__PURE__ */ jsx(TriangleAlertIcon, { className: "size-4" }),
19
+ error: /* @__PURE__ */ jsx(OctagonXIcon, { className: "size-4" }),
20
+ loading: /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" })
21
+ },
14
22
  style: {
15
23
  "--normal-bg": "var(--popover)",
16
24
  "--normal-text": "var(--popover-foreground)",
17
- "--normal-border": "var(--border)"
25
+ "--normal-border": "var(--border)",
26
+ "--border-radius": "var(--radius)"
18
27
  }
19
28
  }, props)
20
29
  );
@@ -13,7 +13,7 @@ var TabsList = React.forwardRef((_a, ref) => {
13
13
  __spreadProps(__spreadValues({
14
14
  ref,
15
15
  className: cn(
16
- variant === "pill" ? "inline-flex h-12 items-center justify-center gap-2 rounded-3xl glass p-1 text-muted-foreground border border-border/50 shadow-lg" : "inline-flex h-auto items-center gap-0 rounded-none bg-transparent border-b border-border p-0 shadow-none text-muted-foreground",
16
+ variant === "pill" ? "inline-flex h-12 items-center justify-center gap-2 rounded-3xl bg-sidebar p-1 text-muted-foreground border border-border/50 shadow-lg" : "inline-flex h-auto items-center gap-0 rounded-none bg-transparent border-b border-border p-0 shadow-none text-muted-foreground",
17
17
  className
18
18
  )
19
19
  }, props), {
@@ -4,9 +4,9 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const textVariants: (props?: ({
7
- variant?: "h2" | "h3" | "label" | "body" | "caption" | "h1" | "h4" | "display" | "body-lg" | "body-sm" | "overline" | null | undefined;
8
- color?: "inherit" | "default" | "destructive" | "muted" | "primary" | "primary-foreground" | null | undefined;
9
- weight?: "bold" | "medium" | "normal" | "semibold" | null | undefined;
7
+ variant?: "display" | "h1" | "h2" | "h3" | "h4" | "body-lg" | "body" | "body-sm" | "caption" | "label" | "overline" | null | undefined;
8
+ color?: "default" | "destructive" | "inherit" | "muted" | "primary" | "primary-foreground" | null | undefined;
9
+ weight?: "bold" | "normal" | "medium" | "semibold" | null | undefined;
10
10
  leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
11
11
  tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
12
12
  wrap?: "balance" | "pretty" | "nowrap" | null | undefined;
@@ -1,5 +1,5 @@
1
- import { useToast } from '../chunk-EBAMCZDW.js';
2
1
  import { ToastProvider, Toast, ToastTitle, ToastDescription, ToastClose, ToastViewport } from '../chunk-ERMIA7NP.js';
2
+ import { useToast } from '../chunk-EBAMCZDW.js';
3
3
  import '../chunk-FEK5XGZW.js';
4
4
  import { __objRest, __spreadProps, __spreadValues } from '../chunk-3GWWZKX7.js';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';