@bearmenu/ui 0.1.1 → 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/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/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/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/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
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
5
|
-
import { Check } from 'lucide-react';
|
|
6
|
-
import { jsx } from 'react/jsx-runtime';
|
|
7
|
-
|
|
8
|
-
var Checkbox = React.forwardRef((_a, ref) => {
|
|
9
|
-
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
10
|
-
return /* @__PURE__ */ jsx(
|
|
11
|
-
CheckboxPrimitive.Root,
|
|
12
|
-
__spreadProps(__spreadValues({
|
|
13
|
-
ref,
|
|
14
|
-
className: cn(
|
|
15
|
-
"peer h-5 w-5 shrink-0 rounded-md border border-border glass transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary data-[state=checked]:shadow-lg data-[state=checked]:shadow-primary/20 hover:border-accent/50 hover:shadow-md",
|
|
16
|
-
className
|
|
17
|
-
)
|
|
18
|
-
}, props), {
|
|
19
|
-
children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { className: cn("flex items-center justify-center text-current"), children: /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 animate-in zoom-in-50 duration-200" }) })
|
|
20
|
-
})
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
24
|
-
|
|
25
|
-
export { Checkbox };
|
|
1
|
+
export { Checkbox } from '../chunk-IQASCH2V.js';
|
|
2
|
+
import '../chunk-FEK5XGZW.js';
|
|
3
|
+
import '../chunk-3GWWZKX7.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Text } from '../chunk-4KOMHDZD.js';
|
|
2
|
-
import { Button } from '../chunk-
|
|
2
|
+
import { Button } from '../chunk-2BMJRXQX.js';
|
|
3
3
|
import '../chunk-R47NEOSI.js';
|
|
4
4
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
5
5
|
import { __objRest, __spreadProps, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { Badge } from '../chunk-NKHH23YK.js';
|
|
5
|
-
import { Button } from '../chunk-
|
|
5
|
+
import { Button } from '../chunk-2BMJRXQX.js';
|
|
6
6
|
import '../chunk-R47NEOSI.js';
|
|
7
7
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
8
8
|
import '../chunk-3GWWZKX7.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
interface MultiSelectMenuOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: React.ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface MultiSelectMenuProps {
|
|
10
|
+
options: MultiSelectMenuOption[];
|
|
11
|
+
selected: string[];
|
|
12
|
+
onSelectedChange: (values: string[]) => void;
|
|
13
|
+
/** Trigger element rendered via Radix `asChild` — typically a Button. */
|
|
14
|
+
trigger: React.ReactNode;
|
|
15
|
+
/** Optional heading rendered above the option list. */
|
|
16
|
+
label?: string;
|
|
17
|
+
align?: "start" | "center" | "end";
|
|
18
|
+
/** Pixel width of the popover content. Defaults to 220. */
|
|
19
|
+
width?: number;
|
|
20
|
+
/** className for the popover content. */
|
|
21
|
+
contentClassName?: string;
|
|
22
|
+
emptyText?: string;
|
|
23
|
+
}
|
|
24
|
+
declare function MultiSelectMenu({ options, selected, onSelectedChange, trigger, label, align, width, contentClassName, emptyText, }: MultiSelectMenuProps): react_jsx_runtime.JSX.Element;
|
|
25
|
+
|
|
26
|
+
export { MultiSelectMenu, type MultiSelectMenuOption, type MultiSelectMenuProps };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../chunk-4UO73SKC.js';
|
|
2
|
+
import { Checkbox } from '../chunk-IQASCH2V.js';
|
|
3
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
4
|
+
import '../chunk-3GWWZKX7.js';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
function MultiSelectMenu({
|
|
9
|
+
options,
|
|
10
|
+
selected,
|
|
11
|
+
onSelectedChange,
|
|
12
|
+
trigger,
|
|
13
|
+
label,
|
|
14
|
+
align = "end",
|
|
15
|
+
width = 220,
|
|
16
|
+
contentClassName,
|
|
17
|
+
emptyText = "No options"
|
|
18
|
+
}) {
|
|
19
|
+
const selectedSet = React.useMemo(() => new Set(selected), [selected]);
|
|
20
|
+
function handleToggle(value) {
|
|
21
|
+
const next = new Set(selectedSet);
|
|
22
|
+
if (next.has(value)) {
|
|
23
|
+
next.delete(value);
|
|
24
|
+
} else {
|
|
25
|
+
next.add(value);
|
|
26
|
+
}
|
|
27
|
+
onSelectedChange(Array.from(next));
|
|
28
|
+
}
|
|
29
|
+
return /* @__PURE__ */ jsxs(Popover, { children: [
|
|
30
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: trigger }),
|
|
31
|
+
/* @__PURE__ */ jsxs(
|
|
32
|
+
PopoverContent,
|
|
33
|
+
{
|
|
34
|
+
align,
|
|
35
|
+
className: cn("p-2", contentClassName),
|
|
36
|
+
style: { width },
|
|
37
|
+
children: [
|
|
38
|
+
label && /* @__PURE__ */ jsx("div", { className: "text-foreground px-2 py-1.5 text-sm font-semibold", children: label }),
|
|
39
|
+
/* @__PURE__ */ jsx("div", { role: "group", "aria-label": label, className: "flex flex-col gap-0.5", children: options.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground px-2 py-1.5 text-sm", children: emptyText }) : options.map((option) => {
|
|
40
|
+
const checked = selectedSet.has(option.value);
|
|
41
|
+
return /* @__PURE__ */ jsxs(
|
|
42
|
+
"label",
|
|
43
|
+
{
|
|
44
|
+
className: cn(
|
|
45
|
+
"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm select-none",
|
|
46
|
+
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
47
|
+
option.disabled && "pointer-events-none opacity-50"
|
|
48
|
+
),
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
Checkbox,
|
|
52
|
+
{
|
|
53
|
+
checked,
|
|
54
|
+
onCheckedChange: () => handleToggle(option.value),
|
|
55
|
+
disabled: option.disabled,
|
|
56
|
+
"aria-label": typeof option.label === "string" ? option.label : option.value
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: option.label })
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
option.value
|
|
63
|
+
);
|
|
64
|
+
}) })
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
] });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { MultiSelectMenu };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buttonVariants } from '../chunk-
|
|
1
|
+
import { buttonVariants } from '../chunk-2BMJRXQX.js';
|
|
2
2
|
import '../chunk-R47NEOSI.js';
|
|
3
3
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
4
4
|
import { __objRest, __spreadValues, __spreadProps } from '../chunk-3GWWZKX7.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface PhoneFrameProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Outer frame width in pixels. Height derives from iPhone 17 Pro Max aspect. Default 220. */
|
|
7
|
+
width?: number;
|
|
8
|
+
/**
|
|
9
|
+
* When set, children are rendered at this virtual viewport width and CSS-scaled
|
|
10
|
+
* to fit the actual frame. Use this to render mobile UIs at native sizing
|
|
11
|
+
* (e.g. 390 for iPhone 13/14/15) while displaying inside a smaller frame.
|
|
12
|
+
*/
|
|
13
|
+
mobileViewportWidth?: number;
|
|
14
|
+
}
|
|
15
|
+
declare function PhoneFrame({ children, className, width, mobileViewportWidth, }: PhoneFrameProps): react_jsx_runtime.JSX.Element;
|
|
16
|
+
|
|
17
|
+
export { PhoneFrame, type PhoneFrameProps };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var SCREEN_ASPECT = 956 / 440;
|
|
6
|
+
var FRAME_PADDING = 6;
|
|
7
|
+
function PhoneFrame({
|
|
8
|
+
children,
|
|
9
|
+
className,
|
|
10
|
+
width = 220,
|
|
11
|
+
mobileViewportWidth
|
|
12
|
+
}) {
|
|
13
|
+
const screenHeight = Math.round(width * SCREEN_ASPECT);
|
|
14
|
+
const innerWidth = width - FRAME_PADDING * 2;
|
|
15
|
+
const scale = mobileViewportWidth ? innerWidth / mobileViewportWidth : 1;
|
|
16
|
+
const viewportHeight = mobileViewportWidth ? screenHeight / scale : screenHeight;
|
|
17
|
+
return /* @__PURE__ */ jsx("div", { className: cn("relative mx-auto shrink-0", className), style: { width }, children: /* @__PURE__ */ jsxs("div", { className: "rounded-[2.5rem] border-[1.5px] border-white/15 bg-black/80 p-1.5 shadow-2xl shadow-black/40", children: [
|
|
18
|
+
/* @__PURE__ */ jsx("div", { className: "absolute top-[10px] left-1/2 -translate-x-1/2 w-[68px] h-[20px] bg-black rounded-full z-10" }),
|
|
19
|
+
/* @__PURE__ */ jsx(
|
|
20
|
+
"div",
|
|
21
|
+
{
|
|
22
|
+
className: "rounded-[2rem] overflow-hidden bg-background relative",
|
|
23
|
+
style: { height: screenHeight },
|
|
24
|
+
children: mobileViewportWidth ? /* @__PURE__ */ jsx(
|
|
25
|
+
"div",
|
|
26
|
+
{
|
|
27
|
+
style: {
|
|
28
|
+
width: mobileViewportWidth,
|
|
29
|
+
height: viewportHeight,
|
|
30
|
+
transform: `scale(${scale})`,
|
|
31
|
+
transformOrigin: "top left"
|
|
32
|
+
},
|
|
33
|
+
children
|
|
34
|
+
}
|
|
35
|
+
) : children
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
] }) });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { PhoneFrame };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { PlacePreview, PlaceAboutTabLabels, PlaceLinkKind } from './place-types.js';
|
|
3
|
+
|
|
4
|
+
interface PlaceAboutTabProps {
|
|
5
|
+
place: PlacePreview;
|
|
6
|
+
labels: PlaceAboutTabLabels;
|
|
7
|
+
/** Slot for an embedded map (FE only). Omit to skip the map row. */
|
|
8
|
+
mapSlot?: React.ReactNode;
|
|
9
|
+
/** Slot for an embedded busyness chart. Rendered only when both this prop and `place.busyness` are present. */
|
|
10
|
+
busynessSlot?: React.ReactNode;
|
|
11
|
+
/** Fires when the user clicks a contact link (phone/website/social). The URL is what the caller passed in. */
|
|
12
|
+
onLinkClick?: (kind: PlaceLinkKind, url: string) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
declare function PlaceAboutTab({ place, labels, mapSlot, busynessSlot, onLinkClick, className, }: PlaceAboutTabProps): react_jsx_runtime.JSX.Element;
|
|
16
|
+
|
|
17
|
+
export { PlaceAboutTab, type PlaceAboutTabProps };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { PlacePreview, PlaceCardLabels } from './place-types.js';
|
|
3
|
+
|
|
4
|
+
interface PlaceCardProps {
|
|
5
|
+
place: PlacePreview;
|
|
6
|
+
labels: PlaceCardLabels;
|
|
7
|
+
/** Locale tag (e.g. `"en"`, `"ro"`). Reserved for future Intl number/distance formatting. */
|
|
8
|
+
locale?: string;
|
|
9
|
+
/** Optional click handler. When omitted the card renders as a non-interactive div. */
|
|
10
|
+
onClick?: (place: PlacePreview) => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Forwarded to the thumbnail's `loading` attribute (`"eager"` when true). */
|
|
13
|
+
priority?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare const PlaceCard: React.NamedExoticComponent<PlaceCardProps>;
|
|
16
|
+
|
|
17
|
+
export { PlaceCard, type PlaceCardProps };
|
|
@@ -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 };
|