@bearmenu/ui 0.7.1 → 0.8.2

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 (123) hide show
  1. package/dist/{chunk-4JOV7FXW.js → chunk-2DEIXTBN.js} +1 -1
  2. package/dist/{chunk-UUJVXN2O.js → chunk-66MAPLXS.js} +3 -1
  3. package/dist/{chunk-Y5D5LP4A.js → chunk-7BWTYWM3.js} +14 -3
  4. package/dist/{chunk-ZFNOCP6W.js → chunk-H3BV4VAG.js} +15 -1
  5. package/dist/{chunk-TL4YU2O5.js → chunk-UKBNQO6Q.js} +2 -2
  6. package/dist/{chunk-UX6SWAKH.js → chunk-UZ3PCIE5.js} +119 -28
  7. package/dist/{chunk-YLNYXPIH.js → chunk-VBA45GLP.js} +1 -1
  8. package/dist/{chunk-A5ONAJCE.js → chunk-VG3PND3D.js} +1 -1
  9. package/dist/{chunk-ZBBUKXM5.js → chunk-X3WMRKOW.js} +1 -1
  10. package/dist/{chunk-HCM6LKM5.js → chunk-XNA2INVQ.js} +2 -2
  11. package/dist/{chunk-AV777R4G.js → chunk-YYGB4G4Q.js} +1 -1
  12. package/dist/components/accordion.js +1 -1
  13. package/dist/components/alert-dialog.js +1 -1
  14. package/dist/components/alert.js +1 -1
  15. package/dist/components/avatar.d.ts +10 -1
  16. package/dist/components/avatar.js +41 -14
  17. package/dist/components/badge.d.ts +11 -2
  18. package/dist/components/badge.js +1 -1
  19. package/dist/components/breadcrumb.js +1 -1
  20. package/dist/components/button.d.ts +1 -1
  21. package/dist/components/button.js +1 -1
  22. package/dist/components/calendar.js +3 -3
  23. package/dist/components/card.js +1 -1
  24. package/dist/components/carousel.js +1 -1
  25. package/dist/components/circular-progress.js +1 -1
  26. package/dist/components/combobox.js +3 -3
  27. package/dist/components/command.js +1 -1
  28. package/dist/components/currency-input.js +2 -2
  29. package/dist/components/date-input.js +1 -1
  30. package/dist/components/description-list.js +1 -1
  31. package/dist/components/drawer.js +5 -1
  32. package/dist/components/dropdown-menu.js +1 -1
  33. package/dist/components/empty-state.js +2 -2
  34. package/dist/components/field.js +2 -2
  35. package/dist/components/filter-category-row.js +2 -2
  36. package/dist/components/form.js +1 -1
  37. package/dist/components/haptic-button.js +1 -1
  38. package/dist/components/input-group.js +1 -1
  39. package/dist/components/input.js +1 -1
  40. package/dist/components/item.js +1 -1
  41. package/dist/components/kbd.js +1 -1
  42. package/dist/components/label.js +1 -1
  43. package/dist/components/link-button.js +1 -1
  44. package/dist/components/multi-select-combobox.js +4 -4
  45. package/dist/components/pagination.js +1 -1
  46. package/dist/components/place-about-tab.d.ts +17 -1
  47. package/dist/components/place-about-tab.js +4 -4
  48. package/dist/components/place-card.js +2 -2
  49. package/dist/components/place-details-mobile.js +7 -7
  50. package/dist/components/place-schedule-week.js +2 -2
  51. package/dist/components/popover.js +1 -1
  52. package/dist/components/searchable-select.js +3 -3
  53. package/dist/components/select.js +2 -2
  54. package/dist/components/selection-bar.js +1 -1
  55. package/dist/components/sheet.d.ts +23 -0
  56. package/dist/components/sheet.js +51 -14
  57. package/dist/components/sliding-panels.js +5 -1
  58. package/dist/components/sonner.js +2 -2
  59. package/dist/components/stepper.js +4 -1
  60. package/dist/components/table.js +1 -1
  61. package/dist/components/tabs.js +1 -1
  62. package/dist/components/tag-group.js +1 -1
  63. package/dist/components/tag.js +1 -1
  64. package/dist/components/text.d.ts +18 -0
  65. package/dist/components/text.js +1 -1
  66. package/dist/components/toggle-group.js +1 -1
  67. package/dist/components/toggle.js +1 -1
  68. package/dist/lib/card-hover.d.ts +75 -0
  69. package/dist/lib/card-hover.js +18 -0
  70. package/package.json +1 -1
  71. package/src/components/accordion.tsx +1 -1
  72. package/src/components/avatar.stories.tsx +59 -26
  73. package/src/components/avatar.test.tsx +129 -0
  74. package/src/components/avatar.tsx +93 -37
  75. package/src/components/badge.tsx +15 -3
  76. package/src/components/breadcrumb.tsx +1 -1
  77. package/src/components/button.tsx +16 -1
  78. package/src/components/calendar.tsx +2 -2
  79. package/src/components/circular-progress.tsx +1 -1
  80. package/src/components/command.tsx +2 -2
  81. package/src/components/currency-input.tsx +1 -1
  82. package/src/components/description-list.tsx +1 -1
  83. package/src/components/drawer.tsx +5 -1
  84. package/src/components/dropdown-menu.tsx +1 -1
  85. package/src/components/field.tsx +1 -1
  86. package/src/components/input-group.tsx +1 -1
  87. package/src/components/input.tsx +1 -1
  88. package/src/components/item.tsx +1 -1
  89. package/src/components/kbd.tsx +1 -1
  90. package/src/components/label.tsx +1 -1
  91. package/src/components/place-about-tab.tsx +131 -18
  92. package/src/components/place-card.tsx +1 -1
  93. package/src/components/place-details-mobile.tsx +2 -2
  94. package/src/components/popover.tsx +9 -1
  95. package/src/components/select.tsx +2 -2
  96. package/src/components/selection-bar.tsx +1 -1
  97. package/src/components/sheet.tsx +72 -12
  98. package/src/components/sliding-panels.stories.tsx +70 -0
  99. package/src/components/sliding-panels.test.tsx +116 -0
  100. package/src/components/sliding-panels.tsx +14 -0
  101. package/src/components/sonner.tsx +2 -2
  102. package/src/components/stepper.tsx +4 -1
  103. package/src/components/table.tsx +1 -1
  104. package/src/components/tabs.tsx +1 -1
  105. package/src/components/tag-group.tsx +1 -1
  106. package/src/components/tag.tsx +1 -1
  107. package/src/components/text.test.tsx +37 -3
  108. package/src/components/text.tsx +32 -3
  109. package/src/components/toggle.tsx +1 -1
  110. package/src/globals.css +53 -11
  111. package/src/lib/card-hover.ts +87 -0
  112. package/dist/chunk-EBAMCZDW.js +0 -112
  113. package/dist/chunk-VQNDZVI7.js +0 -106
  114. package/dist/components/toast.d.ts +0 -18
  115. package/dist/components/toast.js +0 -3
  116. package/dist/components/toaster.d.ts +0 -5
  117. package/dist/components/toaster.js +0 -25
  118. package/dist/components/use-toast.d.ts +0 -42
  119. package/dist/components/use-toast.js +0 -2
  120. package/src/components/toast.stories.tsx +0 -63
  121. package/src/components/toast.tsx +0 -130
  122. package/src/components/toaster.tsx +0 -31
  123. package/src/components/use-toast.ts +0 -180
@@ -89,7 +89,7 @@ const CircularProgress = React.forwardRef<HTMLDivElement, CircularProgressProps>
89
89
  />
90
90
  </svg>
91
91
  {showLabel && !indeterminate && (
92
- <span className="absolute text-xs font-medium tabular-nums text-foreground">
92
+ <span className="absolute text-xs font-semibold tabular-nums text-foreground">
93
93
  {Math.round(percent)}%
94
94
  </span>
95
95
  )}
@@ -41,7 +41,7 @@ const CommandDialog = ({
41
41
  <DialogTitle>{title}</DialogTitle>
42
42
  <DialogDescription>{description}</DialogDescription>
43
43
  </DialogHeader>
44
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
44
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
45
45
  {children}
46
46
  </Command>
47
47
  </DialogContent>
@@ -110,7 +110,7 @@ const CommandGroup = React.forwardRef<
110
110
  <CommandPrimitive.Group
111
111
  ref={ref}
112
112
  className={cn(
113
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
113
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-muted-foreground",
114
114
  className
115
115
  )}
116
116
  {...props}
@@ -139,7 +139,7 @@ const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
139
139
  <div className="relative">
140
140
  <span
141
141
  aria-hidden
142
- className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm font-medium text-muted-foreground"
142
+ className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm font-semibold text-muted-foreground"
143
143
  >
144
144
  {currency}
145
145
  </span>
@@ -40,7 +40,7 @@ const DescriptionTerm = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLE
40
40
  <dt
41
41
  ref={ref as React.Ref<HTMLElement>}
42
42
  data-slot="description-term"
43
- className={cn("text-xs font-medium uppercase tracking-wide text-muted-foreground", className)}
43
+ className={cn("text-xs font-semibold uppercase tracking-wide text-muted-foreground", className)}
44
44
  {...props}
45
45
  />
46
46
  )
@@ -71,7 +71,11 @@ const DrawerContent = React.forwardRef<
71
71
  className={cn(
72
72
  "fixed z-[1000] flex border-border bg-card shadow-2xl",
73
73
  positionClasses,
74
- isVertical ? "max-h-[90vh]" : "",
74
+ // The single height cap for every vertical drawer. Consumers should
75
+ // NOT restate it: six app call sites carried an identical
76
+ // `!max-h-[90vh]`, so this default governed nothing and changing it
77
+ // here would have reached none of them.
78
+ isVertical ? "max-h-[85vh]" : "",
75
79
  variant === "floating" && "m-3 rounded-2xl border",
76
80
  className
77
81
  )}
@@ -161,7 +161,7 @@ function DropdownMenuLabel({
161
161
  <DropdownMenuPrimitive.Label
162
162
  data-slot="dropdown-menu-label"
163
163
  data-inset={inset}
164
- className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
164
+ className={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
165
165
  {...props}
166
166
  />
167
167
  );
@@ -136,7 +136,7 @@ const FieldLegend = React.forwardRef<HTMLLegendElement, React.HTMLAttributes<HTM
136
136
  <legend
137
137
  ref={ref}
138
138
  data-slot="field-legend"
139
- className={cn("px-1 text-sm font-medium text-foreground", className)}
139
+ className={cn("px-1 text-sm font-semibold text-foreground", className)}
140
140
  {...props}
141
141
  />
142
142
  )
@@ -92,7 +92,7 @@ const InputGroupButton = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAtt
92
92
  type={type}
93
93
  data-slot="input-group-button"
94
94
  className={cn(
95
- "inline-flex h-7 min-w-7 cursor-pointer items-center justify-center gap-1 rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&:has(svg:only-child)]:w-7 [&:has(svg:only-child)]:px-0 [&_svg]:size-4",
95
+ "inline-flex h-7 min-w-7 cursor-pointer items-center justify-center gap-1 rounded-md px-2 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&:has(svg:only-child)]:w-7 [&:has(svg:only-child)]:px-0 [&_svg]:size-4",
96
96
  className
97
97
  )}
98
98
  {...props}
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "../lib/utils";
4
4
 
5
5
  const inputVariants = cva(
6
- "flex w-full border border-border bg-input/50 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:bg-input disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
6
+ "flex w-full border border-border bg-input/50 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-semibold file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:bg-input disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
7
7
  {
8
8
  variants: {
9
9
  size: {
@@ -81,7 +81,7 @@ const ItemTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivE
81
81
  <div
82
82
  ref={ref}
83
83
  data-slot="item-title"
84
- className={cn("truncate text-sm font-medium text-foreground", className)}
84
+ className={cn("truncate text-sm font-semibold text-foreground", className)}
85
85
  {...props}
86
86
  />
87
87
  )
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "../lib/utils";
4
4
 
5
5
  const kbdVariants = cva(
6
- "inline-flex select-none items-center justify-center gap-1 rounded-md border border-border bg-muted font-mono font-medium text-muted-foreground shadow-[0_1px_0_0_var(--border)]",
6
+ "inline-flex select-none items-center justify-center gap-1 rounded-md border border-border bg-muted font-mono font-semibold text-muted-foreground shadow-[0_1px_0_0_var(--border)]",
7
7
  {
8
8
  variants: {
9
9
  size: {
@@ -6,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority";
6
6
  import { cn } from "../lib/utils";
7
7
 
8
8
  const labelVariants = cva(
9
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 transition-all duration-200"
9
+ "text-sm font-semibold leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 transition-all duration-200"
10
10
  );
11
11
 
12
12
  const Label = React.forwardRef<
@@ -49,6 +49,46 @@ import type {
49
49
  PlacePreview,
50
50
  } from "./place-types";
51
51
 
52
+ /**
53
+ * The sheet's reading order, by what a visitor opens it to find out.
54
+ *
55
+ * getting there map (own slot, always first) → hours → contact
56
+ * transacting ordering
57
+ * practical busyness → amenities → accessibility → payment → parking
58
+ * corroboration reputation
59
+ * reference cuisines → landmarks → description
60
+ *
61
+ * `accessibility` sits ahead of `payment` and `parking` on purpose: for the
62
+ * people who need it, it is not a nice-to-have detail to be found after two
63
+ * blocks about how to pay.
64
+ *
65
+ * `description` goes last, not first. A place page already carries a tagline
66
+ * and a review verdict above this sheet, so a third prose block about the same
67
+ * venue is the least new information here — it opened the sheet, pushing the
68
+ * opening hours below the fold.
69
+ */
70
+ const SECTION_ORDER = [
71
+ "hours",
72
+ "contact",
73
+ "ordering",
74
+ "busyness",
75
+ "amenities",
76
+ "accessibility",
77
+ "payment",
78
+ "parking",
79
+ "reputation",
80
+ "cuisines",
81
+ "landmarks",
82
+ "description",
83
+ ] as const;
84
+
85
+ /** A key this list doesn't know sorts to the END, keeping its incoming relative
86
+ * order — so a section added later appears last rather than silently leading. */
87
+ function sectionRank(key: string): number {
88
+ const i = SECTION_ORDER.indexOf(key as (typeof SECTION_ORDER)[number]);
89
+ return i === -1 ? Number.MAX_SAFE_INTEGER : i;
90
+ }
91
+
52
92
  export interface PlaceAboutTabProps {
53
93
  place: PlacePreview;
54
94
  labels: PlaceAboutTabLabels;
@@ -56,6 +96,22 @@ export interface PlaceAboutTabProps {
56
96
  mapSlot?: React.ReactNode;
57
97
  /** Slot for an embedded busyness chart. Rendered only when both this prop and `place.busyness` are present. */
58
98
  busynessSlot?: React.ReactNode;
99
+ /**
100
+ * Slot for "how to order from here" — delivery platforms and the like.
101
+ * Sits directly after contact, because it answers the same question contact
102
+ * does (how do I transact with this venue) and is the most actionable thing
103
+ * the sheet carries.
104
+ *
105
+ * Named for its MEANING, not its position, so a later reordering doesn't turn
106
+ * the prop name into a lie.
107
+ */
108
+ orderingSlot?: React.ReactNode;
109
+ /**
110
+ * Slot for "what the rest of the web says / where else this venue lives" —
111
+ * third-party ratings and the venue's own profiles. Sits after the practical
112
+ * block: it is corroboration, wanted after the facts, not before them.
113
+ */
114
+ reputationSlot?: React.ReactNode;
59
115
  /** Fires when the user clicks a contact link (phone/website/social). */
60
116
  onLinkClick?: (kind: PlaceLinkKind, url: string) => void;
61
117
  className?: string;
@@ -67,7 +123,12 @@ function humanizeEnum(value: string): string {
67
123
 
68
124
  function SectionHeading({ children }: { children: React.ReactNode }) {
69
125
  return (
70
- <Text as="h3" variant="body-sm" weight="semibold" className="text-foreground">
126
+ <Text
127
+ as="h3"
128
+ variant="body-sm"
129
+ weight="semibold"
130
+ className="text-foreground"
131
+ >
71
132
  {children}
72
133
  </Text>
73
134
  );
@@ -132,6 +193,8 @@ export function PlaceAboutTab({
132
193
  labels,
133
194
  mapSlot,
134
195
  busynessSlot,
196
+ orderingSlot,
197
+ reputationSlot,
135
198
  onLinkClick,
136
199
  className,
137
200
  }: PlaceAboutTabProps) {
@@ -147,23 +210,26 @@ export function PlaceAboutTab({
147
210
  labels.accessibilityLabels[key] || humanizeEnum(key);
148
211
  const getParkingLabel = (key: string): string =>
149
212
  labels.parkingLabels[key] || humanizeEnum(key);
150
- const getTypeLabel = (key: string): string => labels.typeLabels[key] || humanizeEnum(key);
213
+ const getTypeLabel = (key: string): string =>
214
+ labels.typeLabels[key] || humanizeEnum(key);
151
215
  const getStatusLabel = (key: string): string =>
152
216
  labels.statusLabels[key] || humanizeEnum(key);
153
217
 
154
218
  const hasDescription = Boolean(
155
- place.generative_summary || place.review_summary || place.description
219
+ place.generative_summary || place.review_summary || place.description,
156
220
  );
157
221
  const hasDescriptionChips = Boolean(place.type || place.business_status);
158
222
  const hasContact = Boolean(
159
223
  place.location?.address ||
160
- place.phone ||
161
- place.website_url ||
162
- place.socials?.instagram_url ||
163
- place.socials?.facebook_url
224
+ place.phone ||
225
+ place.website_url ||
226
+ place.socials?.instagram_url ||
227
+ place.socials?.facebook_url,
164
228
  );
165
229
  const hasSchedule = Boolean(place.schedule);
166
- const hasBusyness = Boolean(busynessSlot && place.busyness && place.busyness.length > 0);
230
+ const hasBusyness = Boolean(
231
+ busynessSlot && place.busyness && place.busyness.length > 0,
232
+ );
167
233
  const hasCuisines = (place.cuisines?.length ?? 0) > 0;
168
234
  const hasAmenities = (place.amenities?.length ?? 0) > 0;
169
235
  const hasPayment = (place.payment_options?.length ?? 0) > 0;
@@ -236,7 +302,12 @@ export function PlaceAboutTab({
236
302
  {place.location.address}
237
303
  </Text>
238
304
  {neighborhood && (
239
- <Text as="p" variant="body-sm" color="muted" className="mt-0.5">
305
+ <Text
306
+ as="p"
307
+ variant="body-sm"
308
+ color="muted"
309
+ className="mt-0.5"
310
+ >
240
311
  {neighborhood}
241
312
  </Text>
242
313
  )}
@@ -248,7 +319,10 @@ export function PlaceAboutTab({
248
319
  onClick={() => handleLinkClick("phone", place.phone)}
249
320
  className="flex items-center gap-2 text-foreground hover:text-primary transition-colors"
250
321
  >
251
- <Phone className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
322
+ <Phone
323
+ className="w-4 h-4 text-muted-foreground shrink-0"
324
+ aria-hidden
325
+ />
252
326
  <Text as="span" variant="body-sm">
253
327
  {place.phone}
254
328
  </Text>
@@ -262,7 +336,10 @@ export function PlaceAboutTab({
262
336
  onClick={() => handleLinkClick("website", place.website_url)}
263
337
  className="flex items-center gap-2 text-foreground hover:text-primary transition-colors break-all"
264
338
  >
265
- <Globe className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
339
+ <Globe
340
+ className="w-4 h-4 text-muted-foreground shrink-0"
341
+ aria-hidden
342
+ />
266
343
  <Text as="span" variant="body-sm" className="break-all">
267
344
  {place.website_url}
268
345
  </Text>
@@ -273,10 +350,15 @@ export function PlaceAboutTab({
273
350
  href={place.socials.instagram_url}
274
351
  target="_blank"
275
352
  rel="noopener noreferrer"
276
- onClick={() => handleLinkClick("instagram", place.socials?.instagram_url)}
353
+ onClick={() =>
354
+ handleLinkClick("instagram", place.socials?.instagram_url)
355
+ }
277
356
  className="flex items-center gap-2 text-foreground hover:text-primary transition-colors"
278
357
  >
279
- <Instagram className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
358
+ <Instagram
359
+ className="w-4 h-4 text-muted-foreground shrink-0"
360
+ aria-hidden
361
+ />
280
362
  <Text as="span" variant="body-sm">
281
363
  {labels.instagram}
282
364
  </Text>
@@ -287,10 +369,15 @@ export function PlaceAboutTab({
287
369
  href={place.socials.facebook_url}
288
370
  target="_blank"
289
371
  rel="noopener noreferrer"
290
- onClick={() => handleLinkClick("facebook", place.socials?.facebook_url)}
372
+ onClick={() =>
373
+ handleLinkClick("facebook", place.socials?.facebook_url)
374
+ }
291
375
  className="flex items-center gap-2 text-foreground hover:text-primary transition-colors"
292
376
  >
293
- <Facebook className="w-4 h-4 text-muted-foreground shrink-0" aria-hidden />
377
+ <Facebook
378
+ className="w-4 h-4 text-muted-foreground shrink-0"
379
+ aria-hidden
380
+ />
294
381
  <Text as="span" variant="body-sm">
295
382
  {labels.facebook}
296
383
  </Text>
@@ -341,7 +428,9 @@ export function PlaceAboutTab({
341
428
  <section className="px-4 space-y-2">
342
429
  <SectionHeading>{labels.cuisines}</SectionHeading>
343
430
  <ChipRow
344
- items={place.cuisines!.map((c) => c.charAt(0).toUpperCase() + c.slice(1))}
431
+ items={place.cuisines!.map(
432
+ (c) => c.charAt(0).toUpperCase() + c.slice(1),
433
+ )}
345
434
  />
346
435
  </section>
347
436
  ),
@@ -378,7 +467,9 @@ export function PlaceAboutTab({
378
467
  node: (
379
468
  <section className="px-4 space-y-2">
380
469
  <SectionHeading>{labels.accessibility}</SectionHeading>
381
- <ChecklistGrid items={place.accessibility_features!.map(getAccessibilityLabel)} />
470
+ <ChecklistGrid
471
+ items={place.accessibility_features!.map(getAccessibilityLabel)}
472
+ />
382
473
  </section>
383
474
  ),
384
475
  });
@@ -404,7 +495,10 @@ export function PlaceAboutTab({
404
495
  <SectionHeading>{labels.nearbyLandmarks}</SectionHeading>
405
496
  <div className="space-y-2">
406
497
  {place.landmarks!.map((landmark, i) => (
407
- <div key={i} className="flex justify-between items-baseline gap-3">
498
+ <div
499
+ key={i}
500
+ className="flex justify-between items-baseline gap-3"
501
+ >
408
502
  <Text as="span" variant="body-sm">
409
503
  {landmark.name}
410
504
  </Text>
@@ -426,6 +520,25 @@ export function PlaceAboutTab({
426
520
  });
427
521
  }
428
522
 
523
+ if (orderingSlot) {
524
+ sections.push({
525
+ key: "ordering",
526
+ node: <section className="px-4">{orderingSlot}</section>,
527
+ });
528
+ }
529
+ if (reputationSlot) {
530
+ sections.push({
531
+ key: "reputation",
532
+ node: <section className="px-4">{reputationSlot}</section>,
533
+ });
534
+ }
535
+
536
+ // Sorted, not pushed in order: the reading order is a product decision and
537
+ // belongs in ONE greppable list, not implied by the physical sequence of a
538
+ // dozen `if` blocks 250 lines apart — which is how it came to open on a
539
+ // description while "is it open" sat fourth.
540
+ sections.sort((a, b) => sectionRank(a.key) - sectionRank(b.key));
541
+
429
542
  return (
430
543
  <div className={cn("space-y-6 pt-4", className)}>
431
544
  {mapSlot}
@@ -83,7 +83,7 @@ export const PlaceCard = memo(function PlaceCard({
83
83
  {ratingDisplay !== null && (
84
84
  <div className="flex items-center gap-1">
85
85
  <Star className="w-4 h-4 fill-primary text-primary" aria-hidden="true" />
86
- <span className="font-medium text-sm">{ratingDisplay}</span>
86
+ <span className="font-semibold text-sm">{ratingDisplay}</span>
87
87
  {reviewCount !== undefined && reviewCount !== null && (
88
88
  <span className="text-xs text-muted-foreground">({reviewCount})</span>
89
89
  )}
@@ -83,7 +83,7 @@ export function PlaceDetailsMobile({
83
83
  <div className="rounded-[22px] overflow-hidden relative">
84
84
  <div className="h-44 flex items-center justify-center relative overflow-hidden">
85
85
  {/* Status badge — bottom right */}
86
- <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">
86
+ <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-semibold text-foreground">
87
87
  <span
88
88
  className={cn(
89
89
  "w-1.5 h-1.5 rounded-full flex-shrink-0",
@@ -235,7 +235,7 @@ function MobileTabPlaceholder({
235
235
  return (
236
236
  <div
237
237
  className={cn(
238
- "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",
238
+ "flex flex-1 min-w-0 items-center justify-center gap-1 px-1 py-2.5 text-[12px] font-semibold border-b-2 -mb-px",
239
239
  active
240
240
  ? "border-primary text-foreground"
241
241
  : "border-transparent text-muted-foreground"
@@ -12,15 +12,23 @@ const PopoverClose = PopoverPrimitive.Close;
12
12
  const PopoverContent = React.forwardRef<
13
13
  React.ElementRef<typeof PopoverPrimitive.Content>,
14
14
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15
- >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
15
+ >(({ className, align = "center", sideOffset = 4, collisionPadding = 12, ...props }, ref) => (
16
16
  <PopoverPrimitive.Portal>
17
17
  <PopoverPrimitive.Content
18
18
  ref={ref}
19
19
  data-slot="popover-content"
20
20
  align={align}
21
21
  sideOffset={sideOffset}
22
+ // Radix flips the side to avoid a collision, but a popover TALLER than the
23
+ // space on either side still overflows — and it overflows upward, off the
24
+ // top of the viewport, where the content cannot be scrolled back into
25
+ // view. `collisionPadding` keeps it off the edges; capping to Radix's
26
+ // measured available height and scrolling makes tall content fit rather
27
+ // than escape.
28
+ collisionPadding={collisionPadding}
22
29
  className={cn(
23
30
  "z-50 w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none",
31
+ "max-h-[var(--radix-popover-content-available-height)] overflow-y-auto overscroll-contain",
24
32
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
25
33
  className
26
34
  )}
@@ -11,7 +11,7 @@ const SelectGroup = SelectPrimitive.Group;
11
11
  const SelectValue = SelectPrimitive.Value;
12
12
 
13
13
  const selectTriggerVariants = cva(
14
- "flex w-full items-center justify-between rounded-xl bg-background border border-border text-sm font-medium text-foreground transition-colors focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 hover:border-foreground/30 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
14
+ "flex w-full items-center justify-between rounded-xl bg-background border border-border text-sm font-semibold text-foreground transition-colors focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 hover:border-foreground/30 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
15
15
  {
16
16
  variants: {
17
17
  size: {
@@ -126,7 +126,7 @@ const SelectItem = React.forwardRef<
126
126
  ref={ref}
127
127
  data-slot="select-item"
128
128
  className={cn(
129
- "relative flex w-full cursor-default select-none items-center rounded-md py-2 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[state=checked]:font-medium",
129
+ "relative flex w-full cursor-default select-none items-center rounded-md py-2 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[state=checked]:font-semibold",
130
130
  className
131
131
  )}
132
132
  {...props}
@@ -40,7 +40,7 @@ const SelectionBar = React.forwardRef<HTMLDivElement, SelectionBarProps>(
40
40
  )}
41
41
  {...props}
42
42
  >
43
- <div className="flex items-center gap-2 text-sm font-medium text-foreground">
43
+ <div className="flex items-center gap-2 text-sm font-semibold text-foreground">
44
44
  {onClear && (
45
45
  <button
46
46
  type="button"
@@ -29,21 +29,66 @@ const SheetOverlay = React.forwardRef<
29
29
  ));
30
30
  SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
31
31
 
32
+ /**
33
+ * `attachment` controls whether the panel is welded to the viewport edge or
34
+ * floats clear of it.
35
+ *
36
+ * - `edge` (default) — the classic sheet: full bleed against its side, only the
37
+ * two corners facing inward rounded. Every existing consumer gets this, so
38
+ * adding `inset` changes nothing already shipped.
39
+ * - `inset` — a floating panel: held off all four edges, rounded on every
40
+ * corner, page visible around it. Reads as a surface laid ON the page rather
41
+ * than one grafted to it, which is what detail surfaces want — the content
42
+ * behind stays legible as context instead of being cropped away.
43
+ *
44
+ * `inset` deliberately drops the border. At an edge the border is the seam
45
+ * between panel and viewport and has work to do; floating, the shadow already
46
+ * separates the two and a border only draws a hard line around the radius.
47
+ */
32
48
  const sheetVariants = cva(
33
- "fixed z-[1000] gap-4 bg-background border-border p-6 shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-[var(--duration-deliberate)] data-[state=closed]:duration-[var(--duration-exit-standard)] data-[state=open]:ease-[var(--ease-enter)] data-[state=closed]:ease-[var(--ease-exit)]",
49
+ "fixed z-[1000] gap-4 bg-background border-border shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-[var(--duration-deliberate)] data-[state=closed]:duration-[var(--duration-exit-standard)] data-[state=open]:ease-[var(--ease-enter)] data-[state=closed]:ease-[var(--ease-exit)]",
34
50
  {
35
51
  variants: {
36
52
  side: {
37
- top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
53
+ top: "inset-x-0 top-0 data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
38
54
  bottom:
39
- "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom rounded-t-2xl",
40
- left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm rounded-r-2xl",
55
+ "inset-x-0 bottom-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
56
+ left: "inset-y-0 left-0 h-full w-3/4 data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
41
57
  right:
42
- "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm rounded-l-2xl",
58
+ "inset-y-0 right-0 h-full w-3/4 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
59
+ },
60
+ attachment: {
61
+ edge: "p-6",
62
+ // Padding is the caller's business here: a detail surface runs a gallery
63
+ // and a sticky action bar to the panel edge, and a fixed p-6 would inset
64
+ // both. `edge` keeps its p-6 so nothing already shipped shifts.
65
+ inset: "rounded-2xl overflow-hidden",
43
66
  },
44
67
  },
68
+ compoundVariants: [
69
+ // `edge` restates exactly what each side carried before `attachment`
70
+ // existed — the seam border and the two inward-facing corners. Kept as
71
+ // compounds rather than left on `side` so `inset` is not fighting them
72
+ // through class-merge order.
73
+ // `sm:max-w-sm` lives here, not on `side`: an inset panel is sized by its
74
+ // caller, and a responsive cap left on `side` cannot be overridden from
75
+ // className — tailwind-merge scopes `sm:max-w-*` and `max-w-*` to
76
+ // different groups, so `max-w-none` loses to it at every width ≥ sm.
77
+ { side: "top", attachment: "edge", class: "border-b" },
78
+ { side: "bottom", attachment: "edge", class: "border-t rounded-t-2xl" },
79
+ { side: "left", attachment: "edge", class: "border-r rounded-r-2xl sm:max-w-sm" },
80
+ { side: "right", attachment: "edge", class: "border-l rounded-l-2xl sm:max-w-sm" },
81
+ // The inset offsets live here rather than on `attachment` because each
82
+ // side has to give up its own edge: a right-side panel clears top, right
83
+ // and bottom but is still anchored to the right.
84
+ { side: "right", attachment: "inset", class: "inset-y-3 right-3 h-auto" },
85
+ { side: "left", attachment: "inset", class: "inset-y-3 left-3 h-auto" },
86
+ { side: "top", attachment: "inset", class: "inset-x-3 top-3" },
87
+ { side: "bottom", attachment: "inset", class: "inset-x-3 bottom-3" },
88
+ ],
45
89
  defaultVariants: {
46
90
  side: "right",
91
+ attachment: "edge",
47
92
  },
48
93
  }
49
94
  );
@@ -51,20 +96,35 @@ const sheetVariants = cva(
51
96
  interface SheetContentProps
52
97
  extends
53
98
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54
- VariantProps<typeof sheetVariants> {}
99
+ VariantProps<typeof sheetVariants> {
100
+ /**
101
+ * Suppress the built-in floating close button. Use when the panel renders its
102
+ * own header row with a close control, which would otherwise stack two close
103
+ * buttons in the same corner.
104
+ */
105
+ hideClose?: boolean;
106
+ }
55
107
 
56
108
  const SheetContent = React.forwardRef<
57
109
  React.ElementRef<typeof SheetPrimitive.Content>,
58
110
  SheetContentProps
59
- >(({ side = "right", className, children, ...props }, ref) => (
111
+ >(({ side = "right", attachment = "edge", className, children, hideClose, ...props }, ref) => (
60
112
  <SheetPortal>
61
113
  <SheetOverlay />
62
- <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
114
+ <SheetPrimitive.Content
115
+ ref={ref}
116
+ className={cn(sheetVariants({ side, attachment }), className)}
117
+ {...props}
118
+ >
63
119
  {children}
64
- <SheetPrimitive.Close className="absolute right-4 top-4 rounded-lg opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary glass-hover p-2">
65
- <X className="h-4 w-4" />
66
- <span className="sr-only">Close</span>
67
- </SheetPrimitive.Close>
120
+ {/* Surfaces that run their own header own the close control there, and
121
+ would otherwise stack two of them in the same corner. */}
122
+ {!hideClose && (
123
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-lg opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary glass-hover p-2">
124
+ <X className="h-4 w-4" />
125
+ <span className="sr-only">Close</span>
126
+ </SheetPrimitive.Close>
127
+ )}
68
128
  </SheetPrimitive.Content>
69
129
  </SheetPortal>
70
130
  ));