@bearmenu/ui 0.6.0 → 0.7.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 (80) hide show
  1. package/dist/chunk-A5ONAJCE.js +102 -0
  2. package/dist/chunk-AV777R4G.js +43 -0
  3. package/dist/{chunk-WKMLZDUT.js → chunk-FRPPIXMY.js} +1 -1
  4. package/dist/{chunk-PSR7N36A.js → chunk-HCM6LKM5.js} +1 -1
  5. package/dist/{chunk-YJ6OBKZJ.js → chunk-TL4YU2O5.js} +10 -3
  6. package/dist/chunk-UX6SWAKH.js +320 -0
  7. package/dist/{chunk-7PPCH42H.js → chunk-VQNDZVI7.js} +1 -1
  8. package/dist/chunk-WSRWFA6K.js +26 -0
  9. package/dist/chunk-YLNYXPIH.js +61 -0
  10. package/dist/components/accordion.js +1 -1
  11. package/dist/components/alert.d.ts +2 -2
  12. package/dist/components/badge.d.ts +3 -2
  13. package/dist/components/badge.js +1 -1
  14. package/dist/components/button.d.ts +1 -1
  15. package/dist/components/card.d.ts +2 -2
  16. package/dist/components/card.js +127 -4
  17. package/dist/components/combobox.js +2 -2
  18. package/dist/components/command.d.ts +15 -15
  19. package/dist/components/command.js +2 -2
  20. package/dist/components/currency-input.js +1 -1
  21. package/dist/components/date-input.d.ts +1 -1
  22. package/dist/components/date-input.js +1 -1
  23. package/dist/components/dialog.js +1 -1
  24. package/dist/components/filter-category-row.d.ts +13 -0
  25. package/dist/components/filter-category-row.js +44 -0
  26. package/dist/components/input-group.d.ts +2 -0
  27. package/dist/components/input-group.js +10 -5
  28. package/dist/components/input.d.ts +1 -0
  29. package/dist/components/input.js +1 -1
  30. package/dist/components/item.d.ts +2 -2
  31. package/dist/components/multi-select-combobox.js +3 -3
  32. package/dist/components/phone-frame.js +1 -1
  33. package/dist/components/place-about-tab.d.ts +2 -2
  34. package/dist/components/place-about-tab.js +4 -3
  35. package/dist/components/place-details-mobile.js +5 -3
  36. package/dist/components/place-schedule-week.d.ts +19 -0
  37. package/dist/components/place-schedule-week.js +4 -0
  38. package/dist/components/place-types.d.ts +18 -9
  39. package/dist/components/scroll-fade.d.ts +15 -0
  40. package/dist/components/scroll-fade.js +121 -0
  41. package/dist/components/searchable-select.js +2 -2
  42. package/dist/components/separator.js +3 -26
  43. package/dist/components/sheet.js +1 -1
  44. package/dist/components/sliding-panels.d.ts +1 -0
  45. package/dist/components/sliding-panels.js +22 -6
  46. package/dist/components/sort-option-list.d.ts +16 -0
  47. package/dist/components/sort-option-list.js +46 -0
  48. package/dist/components/spinner.d.ts +2 -2
  49. package/dist/components/tabs.js +98 -17
  50. package/dist/components/tag.d.ts +2 -2
  51. package/dist/components/tag.js +3 -61
  52. package/dist/components/text.d.ts +3 -3
  53. package/dist/components/toast.js +1 -1
  54. package/dist/components/toaster.js +1 -1
  55. package/package.json +23 -112
  56. package/src/components/accordion.tsx +1 -1
  57. package/src/components/badge.tsx +9 -2
  58. package/src/components/card.tsx +22 -18
  59. package/src/components/dialog.tsx +1 -1
  60. package/src/components/filter-category-row.tsx +52 -0
  61. package/src/components/input-group.tsx +9 -4
  62. package/src/components/input.tsx +8 -3
  63. package/src/components/phone-frame.tsx +1 -1
  64. package/src/components/place-about-tab.stories.tsx +24 -13
  65. package/src/components/place-about-tab.test.tsx +92 -38
  66. package/src/components/place-about-tab.tsx +310 -344
  67. package/src/components/place-schedule-week.tsx +128 -0
  68. package/src/components/place-types.ts +27 -8
  69. package/src/components/scroll-fade.stories.tsx +98 -0
  70. package/src/components/scroll-fade.tsx +132 -0
  71. package/src/components/sheet.tsx +1 -1
  72. package/src/components/sliding-panels.tsx +33 -6
  73. package/src/components/sort-option-list.tsx +57 -0
  74. package/src/components/tabs.tsx +76 -11
  75. package/src/components/toast.tsx +1 -1
  76. package/src/globals.css +77 -19
  77. package/src/lib/motion.ts +137 -0
  78. package/dist/chunk-BAZFVSSG.js +0 -279
  79. package/dist/chunk-RIDXFY6M.js +0 -38
  80. package/dist/chunk-XK2ZDQVE.js +0 -122
@@ -1,17 +1,7 @@
1
1
  "use client";
2
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";
3
+ import { Fragment } from "react";
4
+ import { Phone, Globe, Check } from "lucide-react";
15
5
  import { cn } from "../lib/utils";
16
6
 
17
7
  // Lucide v1+ removed brand icons; inline SVGs preserve the original visual.
@@ -48,9 +38,11 @@ function Facebook(props: React.SVGProps<SVGSVGElement>) {
48
38
  </svg>
49
39
  );
50
40
  }
51
- import { Card, CardContent, CardHeader, CardTitle } from "./card";
52
- import { Badge } from "./badge";
41
+
42
+ import { Tag } from "./tag";
53
43
  import { Text } from "./text";
44
+ import { Separator } from "./separator";
45
+ import { PlaceScheduleWeek } from "./place-schedule-week";
54
46
  import type {
55
47
  PlaceAboutTabLabels,
56
48
  PlaceLinkKind,
@@ -60,23 +52,81 @@ import type {
60
52
  export interface PlaceAboutTabProps {
61
53
  place: PlacePreview;
62
54
  labels: PlaceAboutTabLabels;
63
- /** Slot for an embedded map (FE only). Omit to skip the map row. */
55
+ /** Slot for an embedded map. Rendered first, full-bleed the caller controls the breakout / gradient treatment. */
64
56
  mapSlot?: React.ReactNode;
65
57
  /** Slot for an embedded busyness chart. Rendered only when both this prop and `place.busyness` are present. */
66
58
  busynessSlot?: React.ReactNode;
67
- /** Fires when the user clicks a contact link (phone/website/social). The URL is what the caller passed in. */
59
+ /** Fires when the user clicks a contact link (phone/website/social). */
68
60
  onLinkClick?: (kind: PlaceLinkKind, url: string) => void;
69
61
  className?: string;
70
62
  }
71
63
 
72
- function formatHour(h: number): string {
73
- return `${h.toString().padStart(2, "0")}:00`;
74
- }
75
-
76
64
  function humanizeEnum(value: string): string {
77
65
  return value.replace(/_/g, " ");
78
66
  }
79
67
 
68
+ function SectionHeading({ children }: { children: React.ReactNode }) {
69
+ return (
70
+ <Text as="h3" variant="body-sm" weight="semibold" className="text-foreground">
71
+ {children}
72
+ </Text>
73
+ );
74
+ }
75
+
76
+ function Divider() {
77
+ return <Separator className="mx-4 w-auto" />;
78
+ }
79
+
80
+ function ChecklistGrid({ items }: { items: string[] }) {
81
+ return (
82
+ <div className="grid grid-cols-2 gap-x-3 gap-y-1.5">
83
+ {items.map((item, i) => (
84
+ <div key={i} className="flex items-start gap-2">
85
+ <Check
86
+ className="w-3.5 h-3.5 mt-0.5 shrink-0 text-muted-foreground"
87
+ aria-hidden
88
+ />
89
+ <Text as="span" variant="body-sm" color="muted">
90
+ {item}
91
+ </Text>
92
+ </div>
93
+ ))}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ function ChipRow({ items }: { items: string[] }) {
99
+ return (
100
+ <div className="flex flex-wrap gap-2">
101
+ {items.map((item, i) => (
102
+ <Tag key={i} variant="default">
103
+ {item}
104
+ </Tag>
105
+ ))}
106
+ </div>
107
+ );
108
+ }
109
+
110
+ function StatusTag({
111
+ status,
112
+ label,
113
+ }: {
114
+ status: "operational" | "closed_temporarily" | "closed_permanently";
115
+ label: string;
116
+ }) {
117
+ const variant: "success" | "warning" | "destructive" =
118
+ status === "operational"
119
+ ? "success"
120
+ : status === "closed_temporarily"
121
+ ? "warning"
122
+ : "destructive";
123
+ return (
124
+ <Tag variant={variant} className="capitalize">
125
+ {label}
126
+ </Tag>
127
+ );
128
+ }
129
+
80
130
  export function PlaceAboutTab({
81
131
  place,
82
132
  labels,
@@ -85,12 +135,6 @@ export function PlaceAboutTab({
85
135
  onLinkClick,
86
136
  className,
87
137
  }: 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
138
  const handleLinkClick = (kind: PlaceLinkKind, url?: string) => {
95
139
  if (onLinkClick && url) onLinkClick(kind, url);
96
140
  };
@@ -104,371 +148,293 @@ export function PlaceAboutTab({
104
148
  const getParkingLabel = (key: string): string =>
105
149
  labels.parkingLabels[key] || humanizeEnum(key);
106
150
  const getTypeLabel = (key: string): string => labels.typeLabels[key] || humanizeEnum(key);
107
- const getStatusLabel = (key: string): string => labels.statusLabels[key] || humanizeEnum(key);
151
+ const getStatusLabel = (key: string): string =>
152
+ labels.statusLabels[key] || humanizeEnum(key);
108
153
 
109
- return (
110
- <div className={cn("space-y-4 pt-4 px-4", className)}>
111
- {mapSlot}
154
+ const hasDescription = Boolean(
155
+ place.generative_summary || place.review_summary || place.description
156
+ );
157
+ const hasDescriptionChips = Boolean(place.type || place.business_status);
158
+ const hasContact = Boolean(
159
+ place.location?.address ||
160
+ place.phone ||
161
+ place.website_url ||
162
+ place.socials?.instagram_url ||
163
+ place.socials?.facebook_url
164
+ );
165
+ const hasSchedule = Boolean(place.schedule);
166
+ const hasBusyness = Boolean(busynessSlot && place.busyness && place.busyness.length > 0);
167
+ const hasCuisines = (place.cuisines?.length ?? 0) > 0;
168
+ const hasAmenities = (place.amenities?.length ?? 0) > 0;
169
+ const hasPayment = (place.payment_options?.length ?? 0) > 0;
170
+ const hasAccessibility = (place.accessibility_features?.length ?? 0) > 0;
171
+ const hasParking = (place.parking_options?.length ?? 0) > 0;
172
+ const hasLandmarks = (place.landmarks?.length ?? 0) > 0;
112
173
 
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">
174
+ const neighborhood =
175
+ place.areas && place.areas.length > 0
176
+ ? place.areas.map((a) => a.name).join(" · ")
177
+ : null;
178
+
179
+ const sections: Array<{ key: string; node: React.ReactNode }> = [];
180
+
181
+ if (hasDescription || hasDescriptionChips) {
182
+ sections.push({
183
+ key: "description",
184
+ node: (
185
+ <section className="px-4 space-y-2">
186
+ <SectionHeading>{labels.description}</SectionHeading>
187
+ {hasDescription && (
188
+ <div className="space-y-2">
189
+ {place.generative_summary && (
190
+ <Text as="p" variant="body-sm" leading="snug" color="muted">
128
191
  {place.generative_summary}
129
192
  </Text>
130
- </div>
131
- )}
132
- {place.description && (
133
- <div>
134
- <Text as="p" variant="body-sm" color="muted" className="mb-2">
135
- {labels.description}
193
+ )}
194
+ {place.review_summary && (
195
+ <Text as="p" variant="body-sm" leading="snug" color="muted">
196
+ {place.review_summary}
136
197
  </Text>
137
- <Text as="p" color="default">
198
+ )}
199
+ {place.description && (
200
+ <Text as="p" variant="body-sm" leading="snug" color="muted">
138
201
  {place.description}
139
202
  </Text>
140
- </div>
141
- )}
142
- {place.review_summary && (
203
+ )}
204
+ </div>
205
+ )}
206
+ {hasDescriptionChips && (
207
+ <div className="flex flex-wrap gap-2">
208
+ {place.type && (
209
+ <Tag variant="default" className="capitalize">
210
+ {getTypeLabel(place.type)}
211
+ </Tag>
212
+ )}
213
+ {place.business_status && (
214
+ <StatusTag
215
+ status={place.business_status}
216
+ label={getStatusLabel(place.business_status)}
217
+ />
218
+ )}
219
+ </div>
220
+ )}
221
+ </section>
222
+ ),
223
+ });
224
+ }
225
+
226
+ if (hasContact) {
227
+ sections.push({
228
+ key: "contact",
229
+ node: (
230
+ <section className="px-4 space-y-2">
231
+ <SectionHeading>{labels.contactAndLocation}</SectionHeading>
232
+ <div className="space-y-2.5">
233
+ {place.location?.address && (
143
234
  <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}
235
+ <Text as="p" variant="body-sm" color="muted">
236
+ {place.location.address}
149
237
  </Text>
238
+ {neighborhood && (
239
+ <Text as="p" variant="body-sm" color="muted" className="mt-0.5">
240
+ {neighborhood}
241
+ </Text>
242
+ )}
150
243
  </div>
151
244
  )}
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" />
245
+ {place.phone && (
177
246
  <a
178
247
  href={`tel:${place.phone}`}
179
248
  onClick={() => handleLinkClick("phone", place.phone)}
180
- className="text-foreground hover:text-primary transition-colors"
249
+ className="flex items-center gap-2 text-foreground hover:text-primary transition-colors"
181
250
  >
182
- {place.phone}
251
+ <Phone className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
252
+ <Text as="span" variant="body-sm">
253
+ {place.phone}
254
+ </Text>
183
255
  </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" />
256
+ )}
257
+ {place.website_url && (
189
258
  <a
190
259
  href={place.website_url}
191
260
  target="_blank"
192
261
  rel="noopener noreferrer"
193
262
  onClick={() => handleLinkClick("website", place.website_url)}
194
- className="text-foreground hover:text-primary transition-colors break-all"
263
+ className="flex items-center gap-2 text-foreground hover:text-primary transition-colors break-all"
195
264
  >
196
- {place.website_url}
265
+ <Globe className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
266
+ <Text as="span" variant="body-sm" className="break-all">
267
+ {place.website_url}
268
+ </Text>
197
269
  </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" />
270
+ )}
271
+ {place.socials?.instagram_url && (
203
272
  <a
204
273
  href={place.socials.instagram_url}
205
274
  target="_blank"
206
275
  rel="noopener noreferrer"
207
276
  onClick={() => handleLinkClick("instagram", place.socials?.instagram_url)}
208
- className="text-foreground hover:text-primary transition-colors"
277
+ className="flex items-center gap-2 text-foreground hover:text-primary transition-colors"
209
278
  >
210
- {labels.instagram}
279
+ <Instagram className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
280
+ <Text as="span" variant="body-sm">
281
+ {labels.instagram}
282
+ </Text>
211
283
  </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" />
284
+ )}
285
+ {place.socials?.facebook_url && (
217
286
  <a
218
287
  href={place.socials.facebook_url}
219
288
  target="_blank"
220
289
  rel="noopener noreferrer"
221
290
  onClick={() => handleLinkClick("facebook", place.socials?.facebook_url)}
222
- className="text-foreground hover:text-primary transition-colors"
291
+ className="flex items-center gap-2 text-foreground hover:text-primary transition-colors"
223
292
  >
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}
293
+ <Facebook className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
294
+ <Text as="span" variant="body-sm">
295
+ {labels.facebook}
254
296
  </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
- )}
297
+ </a>
298
+ )}
299
+ </div>
300
+ </section>
301
+ ),
302
+ });
303
+ }
279
304
 
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
- )}
305
+ if (hasSchedule) {
306
+ sections.push({
307
+ key: "hours",
308
+ node: (
309
+ <section className="px-4 space-y-2">
310
+ <SectionHeading>{labels.todayHours}</SectionHeading>
311
+ <PlaceScheduleWeek
312
+ schedule={place.schedule}
313
+ labels={{
314
+ days: labels.scheduleDays,
315
+ closed: labels.statusClosed,
316
+ noHoursAvailable: labels.noHoursAvailable,
317
+ }}
318
+ autoScrollToToday={false}
319
+ />
320
+ </section>
321
+ ),
322
+ });
323
+ }
299
324
 
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
- )}
325
+ if (hasBusyness) {
326
+ sections.push({
327
+ key: "busyness",
328
+ node: (
329
+ <section className="px-4 space-y-2">
330
+ <SectionHeading>{labels.typicalBusyness}</SectionHeading>
331
+ {busynessSlot}
332
+ </section>
333
+ ),
334
+ });
335
+ }
321
336
 
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
- )}
337
+ if (hasCuisines) {
338
+ sections.push({
339
+ key: "cuisines",
340
+ node: (
341
+ <section className="px-4 space-y-2">
342
+ <SectionHeading>{labels.cuisines}</SectionHeading>
343
+ <ChipRow
344
+ items={place.cuisines!.map((c) => c.charAt(0).toUpperCase() + c.slice(1))}
345
+ />
346
+ </section>
347
+ ),
348
+ });
349
+ }
341
350
 
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
- )}
351
+ if (hasAmenities) {
352
+ sections.push({
353
+ key: "amenities",
354
+ node: (
355
+ <section className="px-4 space-y-2">
356
+ <SectionHeading>{labels.amenities}</SectionHeading>
357
+ <ChecklistGrid items={place.amenities!.map(getAmenityLabel)} />
358
+ </section>
359
+ ),
360
+ });
361
+ }
366
362
 
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
- )}
363
+ if (hasPayment) {
364
+ sections.push({
365
+ key: "payment",
366
+ node: (
367
+ <section className="px-4 space-y-2">
368
+ <SectionHeading>{labels.paymentOptions}</SectionHeading>
369
+ <ChipRow items={place.payment_options!.map(getPaymentLabel)} />
370
+ </section>
371
+ ),
372
+ });
373
+ }
386
374
 
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
- )}
375
+ if (hasAccessibility) {
376
+ sections.push({
377
+ key: "accessibility",
378
+ node: (
379
+ <section className="px-4 space-y-2">
380
+ <SectionHeading>{labels.accessibility}</SectionHeading>
381
+ <ChecklistGrid items={place.accessibility_features!.map(getAccessibilityLabel)} />
382
+ </section>
383
+ ),
384
+ });
385
+ }
414
386
 
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
- )}
387
+ if (hasParking) {
388
+ sections.push({
389
+ key: "parking",
390
+ node: (
391
+ <section className="px-4 space-y-2">
392
+ <SectionHeading>{labels.parking}</SectionHeading>
393
+ <ChipRow items={place.parking_options!.map(getParkingLabel)} />
394
+ </section>
395
+ ),
396
+ });
397
+ }
434
398
 
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}
399
+ if (hasLandmarks) {
400
+ sections.push({
401
+ key: "landmarks",
402
+ node: (
403
+ <section className="px-4 space-y-2">
404
+ <SectionHeading>{labels.nearbyLandmarks}</SectionHeading>
405
+ <div className="space-y-2">
406
+ {place.landmarks!.map((landmark, i) => (
407
+ <div key={i} className="flex justify-between items-baseline gap-3">
408
+ <Text as="span" variant="body-sm">
409
+ {landmark.name}
445
410
  </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>
411
+ {landmark.distance_meters != null && (
412
+ <Text
413
+ as="span"
414
+ variant="body-sm"
415
+ color="muted"
416
+ className="tabular-nums shrink-0"
417
+ >
418
+ {(landmark.distance_meters / 1000).toFixed(1)} km
419
+ </Text>
420
+ )}
467
421
  </div>
468
- )}
469
- </CardContent>
470
- </Card>
471
- )}
422
+ ))}
423
+ </div>
424
+ </section>
425
+ ),
426
+ });
427
+ }
428
+
429
+ return (
430
+ <div className={cn("space-y-6 pt-4", className)}>
431
+ {mapSlot}
432
+ {sections.map((s, i) => (
433
+ <Fragment key={s.key}>
434
+ {(i > 0 || mapSlot) && <Divider />}
435
+ {s.node}
436
+ </Fragment>
437
+ ))}
472
438
  </div>
473
439
  );
474
440
  }