@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.
Files changed (113) hide show
  1. package/dist/{chunk-2CUFS5LF.js → chunk-2BMJRXQX.js} +1 -1
  2. package/dist/chunk-HLQ2IRE7.js +279 -0
  3. package/dist/chunk-IQASCH2V.js +25 -0
  4. package/dist/{chunk-UXAYXRFN.js → chunk-QJ54Z7TC.js} +15 -1
  5. package/dist/{chunk-X6GATUI2.js → chunk-UISA4CIG.js} +1 -0
  6. package/dist/components/alert-dialog.js +1 -1
  7. package/dist/components/button.js +1 -1
  8. package/dist/components/calendar.d.ts +9 -7
  9. package/dist/components/calendar.js +153 -39
  10. package/dist/components/card.d.ts +2 -1
  11. package/dist/components/card.js +1 -1
  12. package/dist/components/carousel.js +1 -1
  13. package/dist/components/checkbox.js +3 -25
  14. package/dist/components/dropdown-menu.js +1 -1
  15. package/dist/components/empty-state.js +1 -1
  16. package/dist/components/haptic-button.js +1 -1
  17. package/dist/components/link-button.js +1 -1
  18. package/dist/components/multi-select-combobox.js +1 -1
  19. package/dist/components/multi-select-menu.d.ts +26 -0
  20. package/dist/components/multi-select-menu.js +71 -0
  21. package/dist/components/pagination.js +1 -1
  22. package/dist/components/phone-frame.d.ts +17 -0
  23. package/dist/components/phone-frame.js +41 -0
  24. package/dist/components/place-about-tab.d.ts +17 -0
  25. package/dist/components/place-about-tab.js +6 -0
  26. package/dist/components/place-card.d.ts +17 -0
  27. package/dist/components/place-card.js +145 -0
  28. package/dist/components/place-details-mobile.d.ts +18 -0
  29. package/dist/components/place-details-mobile.js +187 -0
  30. package/dist/components/place-types.d.ts +145 -0
  31. package/dist/components/place-types.js +1 -0
  32. package/dist/components/search-bar.js +1 -1
  33. package/dist/components/searchable-select.js +1 -1
  34. package/dist/components/skeleton-card.js +1 -1
  35. package/dist/components/skeleton.js +1 -1
  36. package/dist/components/sliding-panels.js +1 -1
  37. package/dist/components/sonner.js +11 -2
  38. package/dist/components/tabs.js +1 -1
  39. package/dist/components/toaster.js +1 -1
  40. package/package.json +28 -9
  41. package/src/components/accordion.stories.tsx +51 -0
  42. package/src/components/alert-dialog.stories.tsx +45 -0
  43. package/src/components/alert.stories.tsx +85 -0
  44. package/src/components/aspect-ratio.stories.tsx +47 -0
  45. package/src/components/avatar.stories.tsx +66 -0
  46. package/src/components/badge.stories.tsx +60 -0
  47. package/src/components/breadcrumb.stories.tsx +63 -0
  48. package/src/components/button.stories.tsx +115 -0
  49. package/src/components/button.tsx +1 -1
  50. package/src/components/calendar.stories.tsx +47 -0
  51. package/src/components/calendar.tsx +160 -40
  52. package/src/components/card.stories.tsx +72 -0
  53. package/src/components/card.tsx +13 -1
  54. package/src/components/carousel.stories.tsx +62 -0
  55. package/src/components/checkbox.stories.tsx +54 -0
  56. package/src/components/checkbox.tsx +1 -1
  57. package/src/components/collapsible.stories.tsx +34 -0
  58. package/src/components/command.stories.tsx +64 -0
  59. package/src/components/currency-input.stories.tsx +50 -0
  60. package/src/components/dialog.stories.tsx +75 -0
  61. package/src/components/drawer.stories.tsx +47 -0
  62. package/src/components/dropdown-menu.stories.tsx +64 -0
  63. package/src/components/dropdown-menu.tsx +1 -1
  64. package/src/components/empty-state.stories.tsx +67 -0
  65. package/src/components/form.stories.tsx +73 -0
  66. package/src/components/haptic-button.stories.tsx +41 -0
  67. package/src/components/input-otp.stories.tsx +42 -0
  68. package/src/components/input.stories.tsx +41 -0
  69. package/src/components/label.stories.tsx +35 -0
  70. package/src/components/markdown-renderer.stories.tsx +45 -0
  71. package/src/components/multi-select-combobox.stories.tsx +78 -0
  72. package/src/components/multi-select-menu.tsx +96 -0
  73. package/src/components/pagination.stories.tsx +48 -0
  74. package/src/components/phone-frame.stories.tsx +51 -0
  75. package/src/components/phone-frame.test.tsx +82 -0
  76. package/src/components/phone-frame.tsx +59 -0
  77. package/src/components/place-about-tab.stories.tsx +164 -0
  78. package/src/components/place-about-tab.test.tsx +325 -0
  79. package/src/components/place-about-tab.tsx +474 -0
  80. package/src/components/place-card.stories.tsx +120 -0
  81. package/src/components/place-card.test.tsx +187 -0
  82. package/src/components/place-card.tsx +187 -0
  83. package/src/components/place-details-mobile.tsx +251 -0
  84. package/src/components/place-types.ts +127 -0
  85. package/src/components/popover.stories.tsx +42 -0
  86. package/src/components/progress.stories.tsx +35 -0
  87. package/src/components/radio-group.stories.tsx +46 -0
  88. package/src/components/rating.stories.tsx +45 -0
  89. package/src/components/scroll-area.stories.tsx +48 -0
  90. package/src/components/search-bar.stories.tsx +92 -0
  91. package/src/components/searchable-select.stories.tsx +55 -0
  92. package/src/components/select.stories.tsx +92 -0
  93. package/src/components/separator.stories.tsx +33 -0
  94. package/src/components/sheet.stories.tsx +95 -0
  95. package/src/components/skeleton-card.stories.tsx +53 -0
  96. package/src/components/skeleton.stories.tsx +42 -0
  97. package/src/components/skeleton.tsx +1 -0
  98. package/src/components/slider.stories.tsx +27 -0
  99. package/src/components/sliding-panels.stories.tsx +68 -0
  100. package/src/components/sonner.tsx +17 -2
  101. package/src/components/sticky-container.stories.tsx +33 -0
  102. package/src/components/switch.stories.tsx +35 -0
  103. package/src/components/table.stories.tsx +60 -0
  104. package/src/components/tabs.stories.tsx +53 -0
  105. package/src/components/tabs.tsx +1 -1
  106. package/src/components/text.stories.tsx +82 -0
  107. package/src/components/textarea.stories.tsx +35 -0
  108. package/src/components/toast.stories.tsx +63 -0
  109. package/src/components/toggle-group.stories.tsx +60 -0
  110. package/src/components/toggle.stories.tsx +48 -0
  111. package/src/components/tooltip.stories.tsx +71 -0
  112. package/src/globals.css +14 -2
  113. package/src/test/setup.ts +9 -0
@@ -0,0 +1,474 @@
1
+ "use client";
2
+
3
+ import {
4
+ MapPin,
5
+ Phone,
6
+ Globe,
7
+ Clock,
8
+ UtensilsCrossed,
9
+ CreditCard,
10
+ Shield,
11
+ Car,
12
+ Building2,
13
+ Info,
14
+ } from "lucide-react";
15
+ import { cn } from "../lib/utils";
16
+
17
+ // Lucide v1+ removed brand icons; inline SVGs preserve the original visual.
18
+ function Instagram(props: React.SVGProps<SVGSVGElement>) {
19
+ return (
20
+ <svg
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ strokeWidth="2"
25
+ strokeLinecap="round"
26
+ strokeLinejoin="round"
27
+ {...props}
28
+ >
29
+ <rect width="20" height="20" x="2" y="2" rx="5" ry="5" />
30
+ <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
31
+ <line x1="17.5" x2="17.51" y1="6.5" y2="6.5" />
32
+ </svg>
33
+ );
34
+ }
35
+
36
+ function Facebook(props: React.SVGProps<SVGSVGElement>) {
37
+ return (
38
+ <svg
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ strokeWidth="2"
43
+ strokeLinecap="round"
44
+ strokeLinejoin="round"
45
+ {...props}
46
+ >
47
+ <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />
48
+ </svg>
49
+ );
50
+ }
51
+ import { Card, CardContent, CardHeader, CardTitle } from "./card";
52
+ import { Badge } from "./badge";
53
+ import { Text } from "./text";
54
+ import type {
55
+ PlaceAboutTabLabels,
56
+ PlaceLinkKind,
57
+ PlacePreview,
58
+ } from "./place-types";
59
+
60
+ export interface PlaceAboutTabProps {
61
+ place: PlacePreview;
62
+ labels: PlaceAboutTabLabels;
63
+ /** Slot for an embedded map (FE only). Omit to skip the map row. */
64
+ mapSlot?: React.ReactNode;
65
+ /** Slot for an embedded busyness chart. Rendered only when both this prop and `place.busyness` are present. */
66
+ busynessSlot?: React.ReactNode;
67
+ /** Fires when the user clicks a contact link (phone/website/social). The URL is what the caller passed in. */
68
+ onLinkClick?: (kind: PlaceLinkKind, url: string) => void;
69
+ className?: string;
70
+ }
71
+
72
+ function formatHour(h: number): string {
73
+ return `${h.toString().padStart(2, "0")}:00`;
74
+ }
75
+
76
+ function humanizeEnum(value: string): string {
77
+ return value.replace(/_/g, " ");
78
+ }
79
+
80
+ export function PlaceAboutTab({
81
+ place,
82
+ labels,
83
+ mapSlot,
84
+ busynessSlot,
85
+ onLinkClick,
86
+ className,
87
+ }: PlaceAboutTabProps) {
88
+ const todayHours =
89
+ place.hours && place.hours.length > 0
90
+ ? place.hours.map((slot) => `${formatHour(slot.opens_at)} - ${formatHour(slot.closes_at)}`)
91
+ : null;
92
+
93
+ const formattedAddress = place.location?.address;
94
+ const handleLinkClick = (kind: PlaceLinkKind, url?: string) => {
95
+ if (onLinkClick && url) onLinkClick(kind, url);
96
+ };
97
+
98
+ const getAmenityLabel = (key: string): string =>
99
+ labels.amenityLabels[key] || humanizeEnum(key);
100
+ const getPaymentLabel = (key: string): string =>
101
+ labels.paymentLabels[key] || humanizeEnum(key);
102
+ const getAccessibilityLabel = (key: string): string =>
103
+ labels.accessibilityLabels[key] || humanizeEnum(key);
104
+ const getParkingLabel = (key: string): string =>
105
+ labels.parkingLabels[key] || humanizeEnum(key);
106
+ const getTypeLabel = (key: string): string => labels.typeLabels[key] || humanizeEnum(key);
107
+ const getStatusLabel = (key: string): string => labels.statusLabels[key] || humanizeEnum(key);
108
+
109
+ return (
110
+ <div className={cn("space-y-4 pt-4 px-4", className)}>
111
+ {mapSlot}
112
+
113
+ {(place.description || place.generative_summary || place.review_summary) && (
114
+ <Card>
115
+ <CardHeader>
116
+ <CardTitle className="flex items-center gap-2">
117
+ <Info className="w-5 h-5" aria-hidden="true" />
118
+ {labels.description}
119
+ </CardTitle>
120
+ </CardHeader>
121
+ <CardContent className="space-y-4">
122
+ {place.generative_summary && (
123
+ <div>
124
+ <Text as="p" variant="body-sm" color="muted" className="mb-2">
125
+ {labels.overview}
126
+ </Text>
127
+ <Text as="p" color="default">
128
+ {place.generative_summary}
129
+ </Text>
130
+ </div>
131
+ )}
132
+ {place.description && (
133
+ <div>
134
+ <Text as="p" variant="body-sm" color="muted" className="mb-2">
135
+ {labels.description}
136
+ </Text>
137
+ <Text as="p" color="default">
138
+ {place.description}
139
+ </Text>
140
+ </div>
141
+ )}
142
+ {place.review_summary && (
143
+ <div>
144
+ <Text as="p" variant="body-sm" color="muted" className="mb-2">
145
+ {labels.whatPeopleSay}
146
+ </Text>
147
+ <Text as="p" color="default">
148
+ {place.review_summary}
149
+ </Text>
150
+ </div>
151
+ )}
152
+ </CardContent>
153
+ </Card>
154
+ )}
155
+
156
+ <Card>
157
+ <CardHeader>
158
+ <CardTitle className="flex items-center gap-2">
159
+ <MapPin className="w-5 h-5" aria-hidden="true" />
160
+ {labels.contactAndLocation}
161
+ </CardTitle>
162
+ </CardHeader>
163
+ <CardContent className="space-y-4">
164
+ {formattedAddress && (
165
+ <div>
166
+ <Text as="p" variant="body-sm" color="muted" className="mb-1">
167
+ {labels.address}
168
+ </Text>
169
+ <Text as="p" color="default">
170
+ {formattedAddress}
171
+ </Text>
172
+ </div>
173
+ )}
174
+ {place.phone && (
175
+ <div className="flex items-center gap-2">
176
+ <Phone className="w-4 h-4 text-muted-foreground" aria-hidden="true" />
177
+ <a
178
+ href={`tel:${place.phone}`}
179
+ onClick={() => handleLinkClick("phone", place.phone)}
180
+ className="text-foreground hover:text-primary transition-colors"
181
+ >
182
+ {place.phone}
183
+ </a>
184
+ </div>
185
+ )}
186
+ {place.website_url && (
187
+ <div className="flex items-center gap-2">
188
+ <Globe className="w-4 h-4 text-muted-foreground" aria-hidden="true" />
189
+ <a
190
+ href={place.website_url}
191
+ target="_blank"
192
+ rel="noopener noreferrer"
193
+ onClick={() => handleLinkClick("website", place.website_url)}
194
+ className="text-foreground hover:text-primary transition-colors break-all"
195
+ >
196
+ {place.website_url}
197
+ </a>
198
+ </div>
199
+ )}
200
+ {place.socials?.instagram_url && (
201
+ <div className="flex items-center gap-2">
202
+ <Instagram className="w-4 h-4 text-muted-foreground" aria-hidden="true" />
203
+ <a
204
+ href={place.socials.instagram_url}
205
+ target="_blank"
206
+ rel="noopener noreferrer"
207
+ onClick={() => handleLinkClick("instagram", place.socials?.instagram_url)}
208
+ className="text-foreground hover:text-primary transition-colors"
209
+ >
210
+ {labels.instagram}
211
+ </a>
212
+ </div>
213
+ )}
214
+ {place.socials?.facebook_url && (
215
+ <div className="flex items-center gap-2">
216
+ <Facebook className="w-4 h-4 text-muted-foreground" aria-hidden="true" />
217
+ <a
218
+ href={place.socials.facebook_url}
219
+ target="_blank"
220
+ rel="noopener noreferrer"
221
+ onClick={() => handleLinkClick("facebook", place.socials?.facebook_url)}
222
+ className="text-foreground hover:text-primary transition-colors"
223
+ >
224
+ {labels.facebook}
225
+ </a>
226
+ </div>
227
+ )}
228
+ </CardContent>
229
+ </Card>
230
+
231
+ {todayHours && todayHours.length > 0 && (
232
+ <Card>
233
+ <CardHeader>
234
+ <CardTitle className="flex items-center gap-2">
235
+ <Clock className="w-5 h-5" aria-hidden="true" />
236
+ {labels.todayHours}
237
+ </CardTitle>
238
+ </CardHeader>
239
+ <CardContent>
240
+ <div className="space-y-3">
241
+ <div className="flex items-center gap-2">
242
+ <span
243
+ className={cn(
244
+ "w-2 h-2 rounded-full",
245
+ place.open_now ? "bg-green-500" : "bg-muted-foreground"
246
+ )}
247
+ />
248
+ <Text
249
+ as="span"
250
+ weight="medium"
251
+ className={place.open_now ? "text-green-600 dark:text-green-400" : ""}
252
+ >
253
+ {place.open_now ? labels.statusOpen : labels.statusClosed}
254
+ </Text>
255
+ </div>
256
+ {todayHours.map((range, index) => (
257
+ <div key={index} className="flex items-center gap-2">
258
+ <Text as="span" color="muted">
259
+ {range}
260
+ </Text>
261
+ </div>
262
+ ))}
263
+ </div>
264
+ </CardContent>
265
+ </Card>
266
+ )}
267
+
268
+ {busynessSlot && place.busyness && place.busyness.length > 0 && (
269
+ <Card>
270
+ <CardHeader>
271
+ <CardTitle className="flex items-center gap-2">
272
+ <Clock className="w-5 h-5" aria-hidden="true" />
273
+ {labels.typicalBusyness}
274
+ </CardTitle>
275
+ </CardHeader>
276
+ <CardContent>{busynessSlot}</CardContent>
277
+ </Card>
278
+ )}
279
+
280
+ {place.cuisines && place.cuisines.length > 0 && (
281
+ <Card>
282
+ <CardHeader>
283
+ <CardTitle className="flex items-center gap-2">
284
+ <UtensilsCrossed className="w-5 h-5" aria-hidden="true" />
285
+ {labels.cuisines}
286
+ </CardTitle>
287
+ </CardHeader>
288
+ <CardContent>
289
+ <div className="flex flex-wrap gap-2">
290
+ {place.cuisines.map((cuisine, index) => (
291
+ <Badge key={index} variant="secondary">
292
+ {cuisine.charAt(0).toUpperCase() + cuisine.slice(1)}
293
+ </Badge>
294
+ ))}
295
+ </div>
296
+ </CardContent>
297
+ </Card>
298
+ )}
299
+
300
+ {place.amenities && place.amenities.length > 0 && (
301
+ <Card>
302
+ <CardHeader>
303
+ <CardTitle>{labels.amenities}</CardTitle>
304
+ </CardHeader>
305
+ <CardContent>
306
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
307
+ {place.amenities.map((amenity, index) => (
308
+ <div key={index} className="flex items-center gap-2 text-sm">
309
+ <Text as="span" color="muted">
310
+
311
+ </Text>
312
+ <Text as="span" color="default">
313
+ {getAmenityLabel(amenity)}
314
+ </Text>
315
+ </div>
316
+ ))}
317
+ </div>
318
+ </CardContent>
319
+ </Card>
320
+ )}
321
+
322
+ {place.payment_options && place.payment_options.length > 0 && (
323
+ <Card>
324
+ <CardHeader>
325
+ <CardTitle className="flex items-center gap-2">
326
+ <CreditCard className="w-5 h-5" aria-hidden="true" />
327
+ {labels.paymentOptions}
328
+ </CardTitle>
329
+ </CardHeader>
330
+ <CardContent>
331
+ <div className="flex flex-wrap gap-2">
332
+ {place.payment_options.map((payment, index) => (
333
+ <Badge key={index} variant="outline">
334
+ {getPaymentLabel(payment)}
335
+ </Badge>
336
+ ))}
337
+ </div>
338
+ </CardContent>
339
+ </Card>
340
+ )}
341
+
342
+ {place.accessibility_features && place.accessibility_features.length > 0 && (
343
+ <Card>
344
+ <CardHeader>
345
+ <CardTitle className="flex items-center gap-2">
346
+ <Shield className="w-5 h-5" aria-hidden="true" />
347
+ {labels.accessibility}
348
+ </CardTitle>
349
+ </CardHeader>
350
+ <CardContent>
351
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
352
+ {place.accessibility_features.map((feature, index) => (
353
+ <div key={index} className="flex items-center gap-2 text-sm">
354
+ <Text as="span" color="muted">
355
+
356
+ </Text>
357
+ <Text as="span" color="default">
358
+ {getAccessibilityLabel(feature)}
359
+ </Text>
360
+ </div>
361
+ ))}
362
+ </div>
363
+ </CardContent>
364
+ </Card>
365
+ )}
366
+
367
+ {place.parking_options && place.parking_options.length > 0 && (
368
+ <Card>
369
+ <CardHeader>
370
+ <CardTitle className="flex items-center gap-2">
371
+ <Car className="w-5 h-5" aria-hidden="true" />
372
+ {labels.parking}
373
+ </CardTitle>
374
+ </CardHeader>
375
+ <CardContent>
376
+ <div className="flex flex-wrap gap-2">
377
+ {place.parking_options.map((parking, index) => (
378
+ <Badge key={index} variant="outline">
379
+ {getParkingLabel(parking)}
380
+ </Badge>
381
+ ))}
382
+ </div>
383
+ </CardContent>
384
+ </Card>
385
+ )}
386
+
387
+ {place.landmarks && place.landmarks.length > 0 && (
388
+ <Card>
389
+ <CardHeader>
390
+ <CardTitle className="flex items-center gap-2">
391
+ <MapPin className="w-5 h-5" aria-hidden="true" />
392
+ {labels.nearbyLandmarks}
393
+ </CardTitle>
394
+ </CardHeader>
395
+ <CardContent>
396
+ <div className="space-y-2">
397
+ {place.landmarks.map((landmark, index) => (
398
+ <div key={index} className="flex justify-between items-center">
399
+ <Text as="span" color="default">
400
+ {landmark.name}
401
+ </Text>
402
+ {landmark.distance_meters !== undefined &&
403
+ landmark.distance_meters !== null && (
404
+ <Text as="span" variant="body-sm" color="muted">
405
+ {(landmark.distance_meters / 1000).toFixed(1)} km
406
+ </Text>
407
+ )}
408
+ </div>
409
+ ))}
410
+ </div>
411
+ </CardContent>
412
+ </Card>
413
+ )}
414
+
415
+ {place.areas && place.areas.length > 0 && (
416
+ <Card>
417
+ <CardHeader>
418
+ <CardTitle className="flex items-center gap-2">
419
+ <Building2 className="w-5 h-5" aria-hidden="true" />
420
+ {labels.neighborhood}
421
+ </CardTitle>
422
+ </CardHeader>
423
+ <CardContent>
424
+ <div className="flex flex-wrap gap-2">
425
+ {place.areas.map((area, index) => (
426
+ <Badge key={index} variant="secondary">
427
+ {area.name}
428
+ </Badge>
429
+ ))}
430
+ </div>
431
+ </CardContent>
432
+ </Card>
433
+ )}
434
+
435
+ {(place.type || place.business_status) && (
436
+ <Card>
437
+ <CardHeader>
438
+ <CardTitle>{labels.businessInformation}</CardTitle>
439
+ </CardHeader>
440
+ <CardContent className="space-y-2">
441
+ {place.type && (
442
+ <div className="flex justify-between items-center">
443
+ <Text as="span" variant="body-sm" color="muted">
444
+ {labels.type}
445
+ </Text>
446
+ <Text as="span" color="default">
447
+ {getTypeLabel(place.type)}
448
+ </Text>
449
+ </div>
450
+ )}
451
+ {place.business_status && (
452
+ <div className="flex justify-between items-center">
453
+ <Text as="span" variant="body-sm" color="muted">
454
+ {labels.status}
455
+ </Text>
456
+ <Badge
457
+ variant={
458
+ place.business_status === "operational"
459
+ ? "default"
460
+ : place.business_status === "closed_temporarily"
461
+ ? "secondary"
462
+ : "destructive"
463
+ }
464
+ >
465
+ {getStatusLabel(place.business_status)}
466
+ </Badge>
467
+ </div>
468
+ )}
469
+ </CardContent>
470
+ </Card>
471
+ )}
472
+ </div>
473
+ );
474
+ }
@@ -0,0 +1,120 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { PlaceCard } from "./place-card";
3
+ import type { PlaceCardLabels, PlacePreview } from "./place-types";
4
+
5
+ const labels: PlaceCardLabels = {
6
+ open: "Open",
7
+ closed: "Closed",
8
+ until: "until",
9
+ opensAt: "opens at",
10
+ currency: "RON",
11
+ };
12
+
13
+ const basePlace: PlacePreview = {
14
+ id: "1",
15
+ slug: "casa-gri",
16
+ name: "Casa Gri",
17
+ description:
18
+ "A modern bistro serving thoughtful European plates with a Mediterranean accent. Open daily until late.",
19
+ open_now: true,
20
+ rating: { rating: 4.6, count: 342 },
21
+ hours: [{ opens_at: 11, closes_at: 23 }],
22
+ today_schedule: [{ opens_at: 11, closes_at: 23 }],
23
+ location: { distance: { meters: 1240 } },
24
+ price_dishes: { price_range_low: 35, price_range_high: 90 },
25
+ price_drinks: { price_range_low: 15, price_range_high: 45 },
26
+ };
27
+
28
+ const meta = {
29
+ title: "Components/PlaceCard",
30
+ component: PlaceCard,
31
+ tags: ["autodocs"],
32
+ parameters: { layout: "centered" },
33
+ args: {
34
+ labels,
35
+ place: basePlace,
36
+ onClick: undefined,
37
+ },
38
+ } satisfies Meta<typeof PlaceCard>;
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ export const Default: Story = {
44
+ decorators: [
45
+ (Story) => (
46
+ <div className="w-[360px]">
47
+ <Story />
48
+ </div>
49
+ ),
50
+ ],
51
+ };
52
+
53
+ export const Closed: Story = {
54
+ args: {
55
+ place: { ...basePlace, open_now: false },
56
+ },
57
+ decorators: [
58
+ (Story) => (
59
+ <div className="w-[360px]">
60
+ <Story />
61
+ </div>
62
+ ),
63
+ ],
64
+ };
65
+
66
+ export const NoRating: Story = {
67
+ args: {
68
+ place: { ...basePlace, rating: undefined, review_count: undefined },
69
+ },
70
+ decorators: [
71
+ (Story) => (
72
+ <div className="w-[360px]">
73
+ <Story />
74
+ </div>
75
+ ),
76
+ ],
77
+ };
78
+
79
+ export const NoThumbnail: Story = {
80
+ args: {
81
+ place: { ...basePlace, thumbnail_url: undefined },
82
+ },
83
+ decorators: [
84
+ (Story) => (
85
+ <div className="w-[360px]">
86
+ <Story />
87
+ </div>
88
+ ),
89
+ ],
90
+ };
91
+
92
+ export const Interactive: Story = {
93
+ args: {
94
+ onClick: (place) => alert(`Clicked: ${place.name}`),
95
+ },
96
+ decorators: [
97
+ (Story) => (
98
+ <div className="w-[360px]">
99
+ <Story />
100
+ </div>
101
+ ),
102
+ ],
103
+ };
104
+
105
+ export const Minimal: Story = {
106
+ args: {
107
+ place: {
108
+ id: "2",
109
+ slug: "tiny",
110
+ name: "The Tiny Spot",
111
+ },
112
+ },
113
+ decorators: [
114
+ (Story) => (
115
+ <div className="w-[360px]">
116
+ <Story />
117
+ </div>
118
+ ),
119
+ ],
120
+ };