@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.
- package/README.md +116 -0
- package/dist/{chunk-2CUFS5LF.js → chunk-2BMJRXQX.js} +1 -1
- package/dist/chunk-HLQ2IRE7.js +279 -0
- package/dist/chunk-IQASCH2V.js +25 -0
- package/dist/{chunk-UXAYXRFN.js → chunk-QJ54Z7TC.js} +15 -1
- package/dist/{chunk-X6GATUI2.js → chunk-UISA4CIG.js} +1 -0
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.d.ts +9 -7
- package/dist/components/calendar.js +153 -39
- package/dist/components/card.d.ts +2 -1
- package/dist/components/card.js +1 -1
- package/dist/components/carousel.js +1 -1
- package/dist/components/checkbox.js +3 -25
- package/dist/components/command.d.ts +13 -13
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/empty-state.js +1 -1
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/multi-select-menu.d.ts +26 -0
- package/dist/components/multi-select-menu.js +71 -0
- package/dist/components/pagination.js +1 -1
- package/dist/components/phone-frame.d.ts +17 -0
- package/dist/components/phone-frame.js +41 -0
- package/dist/components/place-about-tab.d.ts +17 -0
- package/dist/components/place-about-tab.js +6 -0
- package/dist/components/place-card.d.ts +17 -0
- package/dist/components/place-card.js +145 -0
- package/dist/components/place-details-mobile.d.ts +18 -0
- package/dist/components/place-details-mobile.js +187 -0
- package/dist/components/place-types.d.ts +145 -0
- package/dist/components/place-types.js +1 -0
- package/dist/components/search-bar.js +1 -1
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.js +1 -1
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/sonner.js +11 -2
- package/dist/components/tabs.js +1 -1
- package/dist/components/text.d.ts +3 -3
- package/dist/components/toaster.js +1 -1
- package/package.json +28 -9
- package/src/components/accordion.stories.tsx +51 -0
- package/src/components/alert-dialog.stories.tsx +45 -0
- package/src/components/alert.stories.tsx +85 -0
- package/src/components/aspect-ratio.stories.tsx +47 -0
- package/src/components/avatar.stories.tsx +66 -0
- package/src/components/badge.stories.tsx +60 -0
- package/src/components/breadcrumb.stories.tsx +63 -0
- package/src/components/button.stories.tsx +115 -0
- package/src/components/button.tsx +1 -1
- package/src/components/calendar.stories.tsx +47 -0
- package/src/components/calendar.tsx +160 -40
- package/src/components/card.stories.tsx +72 -0
- package/src/components/card.tsx +13 -1
- package/src/components/carousel.stories.tsx +62 -0
- package/src/components/checkbox.stories.tsx +54 -0
- package/src/components/checkbox.tsx +1 -1
- package/src/components/collapsible.stories.tsx +34 -0
- package/src/components/command.stories.tsx +64 -0
- package/src/components/currency-input.stories.tsx +50 -0
- package/src/components/dialog.stories.tsx +75 -0
- package/src/components/drawer.stories.tsx +47 -0
- package/src/components/dropdown-menu.stories.tsx +64 -0
- package/src/components/dropdown-menu.tsx +1 -1
- package/src/components/empty-state.stories.tsx +67 -0
- package/src/components/form.stories.tsx +73 -0
- package/src/components/haptic-button.stories.tsx +41 -0
- package/src/components/input-otp.stories.tsx +42 -0
- package/src/components/input.stories.tsx +41 -0
- package/src/components/label.stories.tsx +35 -0
- package/src/components/markdown-renderer.stories.tsx +45 -0
- package/src/components/multi-select-combobox.stories.tsx +78 -0
- package/src/components/multi-select-menu.tsx +96 -0
- package/src/components/pagination.stories.tsx +48 -0
- package/src/components/phone-frame.stories.tsx +51 -0
- package/src/components/phone-frame.test.tsx +82 -0
- package/src/components/phone-frame.tsx +59 -0
- package/src/components/place-about-tab.stories.tsx +164 -0
- package/src/components/place-about-tab.test.tsx +325 -0
- package/src/components/place-about-tab.tsx +474 -0
- package/src/components/place-card.stories.tsx +120 -0
- package/src/components/place-card.test.tsx +187 -0
- package/src/components/place-card.tsx +187 -0
- package/src/components/place-details-mobile.tsx +251 -0
- package/src/components/place-types.ts +127 -0
- package/src/components/popover.stories.tsx +42 -0
- package/src/components/progress.stories.tsx +35 -0
- package/src/components/radio-group.stories.tsx +46 -0
- package/src/components/rating.stories.tsx +45 -0
- package/src/components/scroll-area.stories.tsx +48 -0
- package/src/components/search-bar.stories.tsx +92 -0
- package/src/components/searchable-select.stories.tsx +55 -0
- package/src/components/select.stories.tsx +92 -0
- package/src/components/separator.stories.tsx +33 -0
- package/src/components/sheet.stories.tsx +95 -0
- package/src/components/skeleton-card.stories.tsx +53 -0
- package/src/components/skeleton.stories.tsx +42 -0
- package/src/components/skeleton.tsx +1 -0
- package/src/components/slider.stories.tsx +27 -0
- package/src/components/sliding-panels.stories.tsx +68 -0
- package/src/components/sonner.tsx +17 -2
- package/src/components/sticky-container.stories.tsx +33 -0
- package/src/components/switch.stories.tsx +35 -0
- package/src/components/table.stories.tsx +60 -0
- package/src/components/tabs.stories.tsx +53 -0
- package/src/components/tabs.tsx +1 -1
- package/src/components/text.stories.tsx +82 -0
- package/src/components/textarea.stories.tsx +35 -0
- package/src/components/toast.stories.tsx +63 -0
- package/src/components/toggle-group.stories.tsx +60 -0
- package/src/components/toggle.stories.tsx +48 -0
- package/src/components/tooltip.stories.tsx +71 -0
- package/src/globals.css +14 -2
- 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,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-
|
|
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-
|
|
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,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
|
);
|
package/dist/components/tabs.js
CHANGED
|
@@ -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
|
|
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?: "
|
|
8
|
-
color?: "
|
|
9
|
-
weight?: "bold" | "
|
|
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';
|