@bearmenu/ui 0.7.1 → 0.8.1
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-4JOV7FXW.js → chunk-2DEIXTBN.js} +1 -1
- package/dist/{chunk-UUJVXN2O.js → chunk-66MAPLXS.js} +3 -1
- package/dist/{chunk-Y5D5LP4A.js → chunk-7BWTYWM3.js} +14 -3
- package/dist/{chunk-ZFNOCP6W.js → chunk-H3BV4VAG.js} +15 -1
- package/dist/{chunk-TL4YU2O5.js → chunk-UKBNQO6Q.js} +2 -2
- package/dist/{chunk-UX6SWAKH.js → chunk-UZ3PCIE5.js} +119 -28
- package/dist/{chunk-YLNYXPIH.js → chunk-VBA45GLP.js} +1 -1
- package/dist/{chunk-A5ONAJCE.js → chunk-VG3PND3D.js} +1 -1
- package/dist/{chunk-ZBBUKXM5.js → chunk-X3WMRKOW.js} +1 -1
- package/dist/{chunk-HCM6LKM5.js → chunk-XNA2INVQ.js} +2 -2
- package/dist/{chunk-AV777R4G.js → chunk-YYGB4G4Q.js} +1 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/alert.js +1 -1
- package/dist/components/avatar.d.ts +10 -1
- package/dist/components/avatar.js +41 -14
- package/dist/components/badge.d.ts +11 -2
- package/dist/components/badge.js +1 -1
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +1 -1
- package/dist/components/carousel.js +1 -1
- package/dist/components/circular-progress.js +1 -1
- package/dist/components/combobox.js +3 -3
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +1 -1
- package/dist/components/currency-input.js +2 -2
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/date-input.js +1 -1
- package/dist/components/description-list.js +1 -1
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/empty-state.js +2 -2
- package/dist/components/field.js +2 -2
- package/dist/components/filter-category-row.js +2 -2
- package/dist/components/form.js +1 -1
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/input-group.js +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +2 -2
- package/dist/components/item.js +1 -1
- package/dist/components/kbd.js +1 -1
- package/dist/components/label.js +1 -1
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +4 -4
- package/dist/components/pagination.js +1 -1
- package/dist/components/place-about-tab.d.ts +17 -1
- package/dist/components/place-about-tab.js +4 -4
- package/dist/components/place-card.js +2 -2
- package/dist/components/place-details-mobile.js +7 -7
- package/dist/components/place-schedule-week.js +2 -2
- package/dist/components/popover.js +1 -1
- package/dist/components/searchable-select.js +3 -3
- package/dist/components/select.js +2 -2
- package/dist/components/selection-bar.js +1 -1
- package/dist/components/sheet.d.ts +23 -0
- package/dist/components/sheet.js +51 -14
- package/dist/components/sliding-panels.js +5 -1
- package/dist/components/sonner.js +2 -2
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/stepper.js +4 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/tag-group.js +1 -1
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +1 -1
- package/dist/components/text.d.ts +21 -3
- package/dist/components/text.js +1 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/lib/card-hover.d.ts +75 -0
- package/dist/lib/card-hover.js +18 -0
- package/package.json +1 -1
- package/src/components/accordion.tsx +1 -1
- package/src/components/avatar.stories.tsx +59 -26
- package/src/components/avatar.test.tsx +129 -0
- package/src/components/avatar.tsx +93 -37
- package/src/components/badge.tsx +15 -3
- package/src/components/breadcrumb.tsx +1 -1
- package/src/components/button.tsx +16 -1
- package/src/components/calendar.tsx +2 -2
- package/src/components/circular-progress.tsx +1 -1
- package/src/components/command.tsx +2 -2
- package/src/components/currency-input.tsx +1 -1
- package/src/components/description-list.tsx +1 -1
- package/src/components/dropdown-menu.tsx +1 -1
- package/src/components/field.tsx +1 -1
- package/src/components/input-group.tsx +1 -1
- package/src/components/input.tsx +1 -1
- package/src/components/item.tsx +1 -1
- package/src/components/kbd.tsx +1 -1
- package/src/components/label.tsx +1 -1
- package/src/components/place-about-tab.tsx +131 -18
- package/src/components/place-card.tsx +1 -1
- package/src/components/place-details-mobile.tsx +2 -2
- package/src/components/popover.tsx +9 -1
- package/src/components/select.tsx +2 -2
- package/src/components/selection-bar.tsx +1 -1
- package/src/components/sheet.tsx +72 -12
- package/src/components/sliding-panels.stories.tsx +70 -0
- package/src/components/sliding-panels.test.tsx +116 -0
- package/src/components/sliding-panels.tsx +14 -0
- package/src/components/sonner.tsx +2 -2
- package/src/components/stepper.tsx +4 -1
- package/src/components/table.tsx +1 -1
- package/src/components/tabs.tsx +1 -1
- package/src/components/tag-group.tsx +1 -1
- package/src/components/tag.tsx +1 -1
- package/src/components/text.test.tsx +37 -3
- package/src/components/text.tsx +32 -3
- package/src/components/toggle.tsx +1 -1
- package/src/globals.css +53 -11
- package/src/lib/card-hover.ts +87 -0
- package/dist/chunk-EBAMCZDW.js +0 -112
- package/dist/chunk-VQNDZVI7.js +0 -106
- package/dist/components/toast.d.ts +0 -18
- package/dist/components/toast.js +0 -3
- package/dist/components/toaster.d.ts +0 -5
- package/dist/components/toaster.js +0 -25
- package/dist/components/use-toast.d.ts +0 -42
- package/dist/components/use-toast.js +0 -2
- package/src/components/toast.stories.tsx +0 -63
- package/src/components/toast.tsx +0 -130
- package/src/components/toaster.tsx +0 -31
- package/src/components/use-toast.ts +0 -180
|
@@ -23,22 +23,32 @@ const avatarVariants = cva(
|
|
|
23
23
|
}
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
|
|
27
|
+
|
|
28
|
+
const AvatarContext = React.createContext<AvatarSize>("default");
|
|
27
29
|
|
|
28
30
|
const Avatar = React.forwardRef<
|
|
29
31
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
30
32
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> &
|
|
31
33
|
VariantProps<typeof avatarVariants>
|
|
32
|
-
>(({ className, size
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
>(({ className, size, ...props }, ref) => {
|
|
35
|
+
// An explicit `size` always wins; otherwise inherit from the nearest
|
|
36
|
+
// AvatarGroup (context defaults to "default", so a standalone Avatar with no
|
|
37
|
+
// size behaves exactly as before).
|
|
38
|
+
const inheritedSize = React.useContext(AvatarContext);
|
|
39
|
+
const resolvedSize = size ?? inheritedSize;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<AvatarContext.Provider value={resolvedSize}>
|
|
43
|
+
<AvatarPrimitive.Root
|
|
44
|
+
ref={ref}
|
|
45
|
+
data-slot="avatar"
|
|
46
|
+
className={cn(avatarVariants({ size: resolvedSize }), className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
</AvatarContext.Provider>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
42
52
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
|
43
53
|
|
|
44
54
|
const AvatarImage = React.forwardRef<
|
|
@@ -62,7 +72,7 @@ const AvatarFallback = React.forwardRef<
|
|
|
62
72
|
ref={ref}
|
|
63
73
|
data-slot="avatar-fallback"
|
|
64
74
|
className={cn(
|
|
65
|
-
"flex size-full items-center justify-center rounded-full bg-muted font-
|
|
75
|
+
"flex size-full items-center justify-center rounded-full bg-muted font-semibold text-muted-foreground",
|
|
66
76
|
className
|
|
67
77
|
)}
|
|
68
78
|
{...props}
|
|
@@ -85,14 +95,14 @@ const AvatarBadge = React.forwardRef<
|
|
|
85
95
|
tone?: "success" | "warning" | "destructive" | "muted";
|
|
86
96
|
}
|
|
87
97
|
>(({ className, placement = "bottom-right", tone = "success", ...props }, ref) => {
|
|
88
|
-
const size = React.useContext(AvatarContext)
|
|
98
|
+
const size = React.useContext(AvatarContext);
|
|
89
99
|
return (
|
|
90
100
|
<span
|
|
91
101
|
ref={ref}
|
|
92
102
|
data-slot="avatar-badge"
|
|
93
103
|
className={cn(
|
|
94
104
|
"absolute block rounded-full ring-background",
|
|
95
|
-
badgeOffset[size
|
|
105
|
+
badgeOffset[size],
|
|
96
106
|
placement === "top-right" ? "right-0 top-0" : "bottom-0 right-0",
|
|
97
107
|
tone === "success" && "bg-success",
|
|
98
108
|
tone === "warning" && "bg-warning",
|
|
@@ -106,37 +116,83 @@ const AvatarBadge = React.forwardRef<
|
|
|
106
116
|
});
|
|
107
117
|
AvatarBadge.displayName = "AvatarBadge";
|
|
108
118
|
|
|
119
|
+
// The "+N" chip tracks the group's avatar size. Its label sits one step below
|
|
120
|
+
// the matching Avatar text rung — a two-glyph "+12" needs more room inside the
|
|
121
|
+
// same circle than a one-glyph initial. `default` reproduces the pre-0.8
|
|
122
|
+
// hardcoded appearance (size-10 text-xs) exactly.
|
|
123
|
+
const avatarGroupOverflowVariants = cva(
|
|
124
|
+
"relative inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-semibold text-muted-foreground ring-2 ring-background",
|
|
125
|
+
{
|
|
126
|
+
variants: {
|
|
127
|
+
size: {
|
|
128
|
+
xs: "size-6 text-[9px]",
|
|
129
|
+
sm: "size-8 text-[10px]",
|
|
130
|
+
default: "size-10 text-xs",
|
|
131
|
+
lg: "size-12 text-sm",
|
|
132
|
+
xl: "size-16 text-base",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
defaultVariants: {
|
|
136
|
+
size: "default",
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
109
141
|
const AvatarGroup = React.forwardRef<
|
|
110
142
|
HTMLDivElement,
|
|
111
|
-
React.HTMLAttributes<HTMLDivElement> & {
|
|
112
|
-
|
|
143
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
144
|
+
max?: number;
|
|
145
|
+
spacing?: "tight" | "default" | "loose";
|
|
146
|
+
/**
|
|
147
|
+
* Size for the whole group: sets the "+N" overflow chip and is inherited by
|
|
148
|
+
* child `Avatar`s that don't set their own `size`. Defaults to "default".
|
|
149
|
+
*/
|
|
150
|
+
size?: AvatarSize;
|
|
151
|
+
}
|
|
152
|
+
>(({ className, children, max, spacing = "default", size, ...props }, ref) => {
|
|
153
|
+
const inheritedSize = React.useContext(AvatarContext);
|
|
154
|
+
const resolvedSize = size ?? inheritedSize;
|
|
113
155
|
const items = React.Children.toArray(children);
|
|
114
156
|
const visible = max ? items.slice(0, max) : items;
|
|
115
157
|
const overflow = max ? items.length - max : 0;
|
|
116
158
|
|
|
117
159
|
return (
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
<AvatarContext.Provider value={resolvedSize}>
|
|
161
|
+
<div
|
|
162
|
+
ref={ref}
|
|
163
|
+
data-slot="avatar-group"
|
|
164
|
+
className={cn(
|
|
165
|
+
"flex items-center",
|
|
166
|
+
spacing === "tight" && "-space-x-3",
|
|
167
|
+
spacing === "default" && "-space-x-2",
|
|
168
|
+
spacing === "loose" && "-space-x-1",
|
|
169
|
+
"[&>*]:ring-2 [&>*]:ring-background",
|
|
170
|
+
className
|
|
171
|
+
)}
|
|
172
|
+
{...props}
|
|
173
|
+
>
|
|
174
|
+
{visible}
|
|
175
|
+
{overflow > 0 && (
|
|
176
|
+
<span
|
|
177
|
+
data-slot="avatar-group-overflow"
|
|
178
|
+
className={avatarGroupOverflowVariants({ size: resolvedSize })}
|
|
179
|
+
>
|
|
180
|
+
+{overflow}
|
|
181
|
+
</span>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</AvatarContext.Provider>
|
|
138
185
|
);
|
|
139
186
|
});
|
|
140
187
|
AvatarGroup.displayName = "AvatarGroup";
|
|
141
188
|
|
|
142
|
-
export {
|
|
189
|
+
export {
|
|
190
|
+
Avatar,
|
|
191
|
+
AvatarImage,
|
|
192
|
+
AvatarFallback,
|
|
193
|
+
AvatarBadge,
|
|
194
|
+
AvatarGroup,
|
|
195
|
+
avatarVariants,
|
|
196
|
+
avatarGroupOverflowVariants,
|
|
197
|
+
};
|
|
198
|
+
export type { AvatarSize };
|
package/src/components/badge.tsx
CHANGED
|
@@ -37,10 +37,22 @@ const badgeVariants = cva(
|
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
export interface BadgeProps
|
|
40
|
-
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
|
|
40
|
+
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
|
|
41
|
+
/**
|
|
42
|
+
* Element to render. Defaults to `div`.
|
|
43
|
+
*
|
|
44
|
+
* Use `"span"` when the badge sits inside a `<button>` or a `<p>` — their
|
|
45
|
+
* content model is phrasing content, so a `div` in there is invalid HTML.
|
|
46
|
+
* That is the common case for a badge sitting inline with a title on a
|
|
47
|
+
* clickable row.
|
|
48
|
+
*/
|
|
49
|
+
as?: "div" | "span";
|
|
50
|
+
}
|
|
41
51
|
|
|
42
|
-
function Badge({ className, variant, live, ...props }: BadgeProps) {
|
|
43
|
-
return
|
|
52
|
+
function Badge({ className, variant, live, as: Tag = "div", ...props }: BadgeProps) {
|
|
53
|
+
return (
|
|
54
|
+
<Tag data-slot="badge" className={cn(badgeVariants({ variant, live }), className)} {...props} />
|
|
55
|
+
);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
export { Badge, badgeVariants };
|
|
@@ -59,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
|
59
59
|
role="link"
|
|
60
60
|
aria-disabled="true"
|
|
61
61
|
aria-current="page"
|
|
62
|
-
className={cn("font-
|
|
62
|
+
className={cn("font-semibold text-foreground", className)}
|
|
63
63
|
{...props}
|
|
64
64
|
/>
|
|
65
65
|
);
|
|
@@ -6,7 +6,7 @@ import { cn } from "../lib/utils";
|
|
|
6
6
|
import { useHaptic } from "../hooks/use-haptic";
|
|
7
7
|
|
|
8
8
|
const buttonVariants = cva(
|
|
9
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-
|
|
9
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer [@media(pointer:coarse)]:active:scale-95",
|
|
10
10
|
{
|
|
11
11
|
variants: {
|
|
12
12
|
variant: {
|
|
@@ -21,6 +21,21 @@ const buttonVariants = cva(
|
|
|
21
21
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
22
22
|
link: "text-primary underline-offset-4 hover:underline",
|
|
23
23
|
glass: "glass glass-hover text-foreground",
|
|
24
|
+
/**
|
|
25
|
+
* Chrome sitting ON a photo — share, close, favourite over a hero or
|
|
26
|
+
* card image. `glass` is the wrong tool there: it resolves
|
|
27
|
+
* `text-foreground`, which is near-black in light mode and disappears
|
|
28
|
+
* into a dark photo.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately NOT theme-aware, and so deliberately different from
|
|
31
|
+
* `Badge`'s `overlay` (`bg-foreground/70 text-background`, which
|
|
32
|
+
* inverts). A photo is equally a photo in both themes — this chrome has
|
|
33
|
+
* to contrast with the image beneath it, not with the app canvas, so it
|
|
34
|
+
* stays dark-on-light-glyph either way. Reconcile Badge to this, not the
|
|
35
|
+
* reverse, if the two are ever unified.
|
|
36
|
+
*/
|
|
37
|
+
overlay:
|
|
38
|
+
"bg-black/40 text-white backdrop-blur-sm hover:bg-black/60 focus-visible:ring-white/70",
|
|
24
39
|
brown:
|
|
25
40
|
"bg-brown text-brown-foreground shadow-sm hover:bg-brown/85 hover:shadow",
|
|
26
41
|
antagonist:
|
|
@@ -58,7 +58,7 @@ function Calendar({
|
|
|
58
58
|
defaultClassNames.month_caption
|
|
59
59
|
),
|
|
60
60
|
dropdowns: cn(
|
|
61
|
-
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-
|
|
61
|
+
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-semibold",
|
|
62
62
|
defaultClassNames.dropdowns
|
|
63
63
|
),
|
|
64
64
|
dropdown_root: cn(
|
|
@@ -67,7 +67,7 @@ function Calendar({
|
|
|
67
67
|
),
|
|
68
68
|
dropdown: cn("absolute inset-0 bg-popover opacity-0", defaultClassNames.dropdown),
|
|
69
69
|
caption_label: cn(
|
|
70
|
-
"font-
|
|
70
|
+
"font-semibold select-none",
|
|
71
71
|
captionLayout === "label"
|
|
72
72
|
? "text-sm"
|
|
73
73
|
: "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
43
|
+
className={cn("text-xs font-semibold uppercase tracking-wide text-muted-foreground", className)}
|
|
44
44
|
{...props}
|
|
45
45
|
/>
|
|
46
46
|
)
|
|
@@ -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-
|
|
164
|
+
className={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
|
|
165
165
|
{...props}
|
|
166
166
|
/>
|
|
167
167
|
);
|
package/src/components/field.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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}
|
package/src/components/input.tsx
CHANGED
|
@@ -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-
|
|
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: {
|
package/src/components/item.tsx
CHANGED
|
@@ -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-
|
|
84
|
+
className={cn("truncate text-sm font-semibold text-foreground", className)}
|
|
85
85
|
{...props}
|
|
86
86
|
/>
|
|
87
87
|
)
|
package/src/components/kbd.tsx
CHANGED
|
@@ -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-
|
|
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: {
|
package/src/components/label.tsx
CHANGED
|
@@ -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-
|
|
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
|
|
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 =>
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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={() =>
|
|
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
|
|
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={() =>
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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-
|
|
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
|
)}
|