@bearmenu/ui 0.8.35 → 0.8.37
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-EGU6EG73.js +39 -0
- package/dist/{chunk-NIAL4ZU2.js → chunk-FQAIYKAS.js} +45 -8
- package/dist/{chunk-TVEIUTBQ.js → chunk-IQIJCMG2.js} +1 -1
- package/dist/{chunk-6W25S5CF.js → chunk-KGXTBTCJ.js} +1 -1
- package/dist/chunk-LAEVMNJN.js +135 -0
- package/dist/{chunk-VCRXL7H5.js → chunk-PGQXVJXW.js} +2 -2
- package/dist/{chunk-F3XK34DV.js → chunk-TB2K5JN3.js} +2 -2
- package/dist/{chunk-XNBDACL7.js → chunk-ZUBBMLBD.js} +1 -1
- package/dist/components/badge.d.ts +8 -2
- package/dist/components/badge.js +1 -1
- package/dist/components/banner.d.ts +17 -2
- package/dist/components/banner.js +10 -7
- package/dist/components/booking-row.js +1 -1
- package/dist/components/button-group.js +3 -39
- package/dist/components/calendar.js +1 -1
- package/dist/components/combobox.js +3 -3
- package/dist/components/command.js +2 -2
- package/dist/components/concept-convergence.js +1 -1
- package/dist/components/dialog.js +1 -1
- package/dist/components/dropdown-menu.js +6 -6
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/input-group.js +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/multi-select-combobox.js +10 -5
- package/dist/components/multi-select-menu.js +1 -1
- package/dist/components/pan-zoom-viewport.d.ts +137 -0
- package/dist/components/pan-zoom-viewport.js +531 -0
- package/dist/components/place-card.js +2 -2
- package/dist/components/place-details-mobile.js +1 -1
- package/dist/components/responsive-overlay.d.ts +29 -1
- package/dist/components/responsive-overlay.js +46 -7
- 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 +6 -0
- package/dist/components/sheet.js +3 -133
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/tabs.d.ts +16 -1
- package/dist/components/tabs.js +21 -3
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle.d.ts +6 -0
- package/dist/components/toggle.js +1 -1
- package/dist/components/top-ranking-entry.js +3 -3
- package/package.json +1 -1
- package/src/components/badge.stories.tsx +32 -1
- package/src/components/badge.test.tsx +45 -0
- package/src/components/badge.tsx +41 -3
- package/src/components/banner.stories.tsx +32 -0
- package/src/components/banner.test.tsx +58 -0
- package/src/components/banner.tsx +28 -4
- package/src/components/calendar.tsx +1 -1
- package/src/components/combobox.tsx +1 -1
- package/src/components/command.test.tsx +10 -0
- package/src/components/command.tsx +1 -1
- package/src/components/dialog.tsx +1 -1
- package/src/components/dropdown-menu.test.tsx +31 -0
- package/src/components/dropdown-menu.tsx +6 -6
- package/src/components/input-group.tsx +1 -1
- package/src/components/multi-select-combobox.tsx +7 -1
- package/src/components/multi-select-menu.tsx +1 -1
- package/src/components/pan-zoom-viewport.stories.tsx +134 -0
- package/src/components/pan-zoom-viewport.test.tsx +381 -0
- package/src/components/pan-zoom-viewport.tsx +727 -0
- package/src/components/responsive-overlay.stories.tsx +61 -0
- package/src/components/responsive-overlay.test.tsx +104 -0
- package/src/components/responsive-overlay.tsx +87 -22
- package/src/components/searchable-select.tsx +1 -1
- package/src/components/select.tsx +2 -2
- package/src/components/selection-bar.tsx +1 -1
- package/src/components/sheet.tsx +14 -3
- package/src/components/tabs.stories.tsx +23 -0
- package/src/components/tabs.test.tsx +64 -0
- package/src/components/tabs.tsx +36 -3
- package/src/components/toggle.test.tsx +19 -0
- package/src/components/toggle.tsx +8 -2
- package/src/globals.css +28 -19
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
-
import { Info, TriangleAlert } from "lucide-react";
|
|
3
|
+
import { CircleAlert, Info, TriangleAlert } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -21,6 +21,14 @@ import { cn } from "../lib/utils";
|
|
|
21
21
|
* - `warning`: a 16% amber wash with the design's amber ink — darker than
|
|
22
22
|
* the solid `--warning`, which falls short of AA on its own wash at this
|
|
23
23
|
* size; dark mode uses the lifted token.
|
|
24
|
+
* - `destructive`: something did not load or did not happen — "The queue
|
|
25
|
+
* didn't load", "Walk-ins didn't load" (today/04-d3, floor/02-s2,
|
|
26
|
+
* reports/03-p9). A red wash with `--destructive-ink`, and the one tone
|
|
27
|
+
* with a hairline: the booking frames draw it, because a failure has to
|
|
28
|
+
* read as a boundary on a page already full of soft grounds.
|
|
29
|
+
*
|
|
30
|
+
* `action` takes the trailing control — the frames' "Retry". It sits on the
|
|
31
|
+
* right edge, centred on the text, and never wraps under it.
|
|
24
32
|
*/
|
|
25
33
|
const bannerVariants = cva(
|
|
26
34
|
"flex gap-2.5 rounded-xl px-3.5 py-3 text-[12.5px] leading-normal [&>svg]:mt-0.5 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
@@ -30,6 +38,7 @@ const bannerVariants = cva(
|
|
|
30
38
|
info: "bg-surface-2 text-ink-secondary",
|
|
31
39
|
warning:
|
|
32
40
|
"bg-[oklch(0.62_0.15_70/0.16)] text-[oklch(0.42_0.1_70)] dark:bg-warning/15 dark:text-warning",
|
|
41
|
+
destructive: "border border-destructive/25 bg-destructive/10 text-destructive-ink",
|
|
33
42
|
},
|
|
34
43
|
},
|
|
35
44
|
defaultVariants: { tone: "info" },
|
|
@@ -45,22 +54,37 @@ export type BannerProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> &
|
|
|
45
54
|
icon?: React.ReactNode;
|
|
46
55
|
/** The bold first line — "19:30 just went". */
|
|
47
56
|
title?: React.ReactNode;
|
|
57
|
+
/**
|
|
58
|
+
* A trailing control — "Retry". Pass a real button (a `Button
|
|
59
|
+
* variant="link"` reads as the frames draw it); the banner only places it.
|
|
60
|
+
* A banner that announces a failure the user did not just cause is
|
|
61
|
+
* usually worth `role="status"` from the caller.
|
|
62
|
+
*/
|
|
63
|
+
action?: React.ReactNode;
|
|
48
64
|
};
|
|
49
65
|
|
|
50
66
|
/** Each tone's own glyph, so a caller never has to remember which. */
|
|
51
67
|
const TONE_ICON = {
|
|
52
68
|
info: <Info aria-hidden />,
|
|
53
69
|
warning: <TriangleAlert aria-hidden />,
|
|
70
|
+
destructive: <CircleAlert aria-hidden />,
|
|
54
71
|
} as const;
|
|
55
72
|
|
|
56
|
-
function Banner({ tone, icon, title, className, children, ...props }: BannerProps) {
|
|
73
|
+
function Banner({ tone, icon, title, action, className, children, ...props }: BannerProps) {
|
|
57
74
|
return (
|
|
58
75
|
<div data-slot="banner" className={cn(bannerVariants({ tone }), className)} {...props}>
|
|
59
76
|
{icon === undefined ? TONE_ICON[tone ?? "info"] : icon}
|
|
60
|
-
<div className="min-w-0">
|
|
77
|
+
<div className="min-w-0 flex-1">
|
|
61
78
|
{title ? <b className="mb-[3px] block text-[13px] font-bold">{title}</b> : null}
|
|
62
|
-
<div className={cn(tone === "warning" && title ? "opacity-90" : undefined)}>
|
|
79
|
+
<div className={cn((tone === "warning" || tone === "destructive") && title ? "opacity-90" : undefined)}>
|
|
80
|
+
{children}
|
|
81
|
+
</div>
|
|
63
82
|
</div>
|
|
83
|
+
{action ? (
|
|
84
|
+
<div data-slot="banner-action" className="shrink-0 self-center">
|
|
85
|
+
{action}
|
|
86
|
+
</div>
|
|
87
|
+
) : null}
|
|
64
88
|
</div>
|
|
65
89
|
);
|
|
66
90
|
}
|
|
@@ -238,7 +238,7 @@ function CalendarDayButton({
|
|
|
238
238
|
data-range-end={modifiers.range_end}
|
|
239
239
|
data-range-middle={modifiers.range_middle}
|
|
240
240
|
className={cn(
|
|
241
|
-
"group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground
|
|
241
|
+
"group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground flex aspect-square h-(--cell-size) w-(--cell-size) size-auto min-w-0 flex-col items-center justify-center gap-1 rounded-md p-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
|
242
242
|
defaultClassNames.day,
|
|
243
243
|
className
|
|
244
244
|
)}
|
|
@@ -124,7 +124,7 @@ function Combobox({
|
|
|
124
124
|
e.stopPropagation();
|
|
125
125
|
onValueChange(undefined);
|
|
126
126
|
}}
|
|
127
|
-
className="absolute right-9 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-
|
|
127
|
+
className="absolute right-9 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
128
128
|
>
|
|
129
129
|
<X className="size-3.5" />
|
|
130
130
|
</button>
|
|
@@ -88,4 +88,14 @@ describe("Command", () => {
|
|
|
88
88
|
await user.keyboard("{ArrowDown}{Enter}");
|
|
89
89
|
expect(onSelect).toHaveBeenCalledWith("banana");
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
it("highlights the selected row with the subtle well, not the accent fill", () => {
|
|
93
|
+
render(<CommandSample />);
|
|
94
|
+
const row = screen.getByRole("option", { name: "Apple" });
|
|
95
|
+
expect(row).toHaveClass(
|
|
96
|
+
"data-[selected=true]:bg-accent-subtle",
|
|
97
|
+
"data-[selected=true]:text-foreground"
|
|
98
|
+
);
|
|
99
|
+
expect(row).not.toHaveClass("data-[selected=true]:bg-accent");
|
|
100
|
+
});
|
|
91
101
|
});
|
|
@@ -138,7 +138,7 @@ const CommandItem = React.forwardRef<
|
|
|
138
138
|
ref={ref}
|
|
139
139
|
className={cn(
|
|
140
140
|
"relative flex cursor-default select-none items-center rounded-lg px-2 py-1.5 text-sm outline-none transition-colors",
|
|
141
|
-
"data-[selected=true]:bg-accent data-[selected=true]:text-
|
|
141
|
+
"data-[selected=true]:bg-accent-subtle data-[selected=true]:text-foreground",
|
|
142
142
|
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
|
|
143
143
|
className
|
|
144
144
|
)}
|
|
@@ -79,7 +79,7 @@ const DialogContent = React.forwardRef<
|
|
|
79
79
|
>
|
|
80
80
|
{children}
|
|
81
81
|
{showCloseButton && (
|
|
82
|
-
<DialogPrimitive.Close className="absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground opacity-70 transition-opacity hover:bg-accent hover:text-
|
|
82
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground opacity-70 transition-opacity hover:bg-accent-subtle hover:text-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none">
|
|
83
83
|
<X className="size-4" />
|
|
84
84
|
<span className="sr-only">Close</span>
|
|
85
85
|
</DialogPrimitive.Close>
|
|
@@ -177,4 +177,35 @@ describe("DropdownMenu", () => {
|
|
|
177
177
|
expect(screen.getByText("Group")).toBeInTheDocument();
|
|
178
178
|
expect(document.querySelector("[data-slot=dropdown-menu-separator]")).not.toBeNull();
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
// A highlighted row is a tinted well with ink type; terracotta is kept for
|
|
182
|
+
// the checked indicators. A solid-accent row read as a second primary.
|
|
183
|
+
it("highlights rows with the subtle well and marks checked items in the accent", async () => {
|
|
184
|
+
const user = userEvent.setup();
|
|
185
|
+
render(
|
|
186
|
+
<DropdownMenu>
|
|
187
|
+
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
|
188
|
+
<DropdownMenuContent>
|
|
189
|
+
<DropdownMenuItem>One</DropdownMenuItem>
|
|
190
|
+
<DropdownMenuCheckboxItem checked>Two</DropdownMenuCheckboxItem>
|
|
191
|
+
<DropdownMenuRadioGroup value="a">
|
|
192
|
+
<DropdownMenuRadioItem value="a">Three</DropdownMenuRadioItem>
|
|
193
|
+
</DropdownMenuRadioGroup>
|
|
194
|
+
</DropdownMenuContent>
|
|
195
|
+
</DropdownMenu>
|
|
196
|
+
);
|
|
197
|
+
await user.click(screen.getByText("Open"));
|
|
198
|
+
const rows = [
|
|
199
|
+
screen.getByRole("menuitem", { name: "One" }),
|
|
200
|
+
screen.getByRole("menuitemcheckbox", { name: "Two" }),
|
|
201
|
+
screen.getByRole("menuitemradio", { name: "Three" }),
|
|
202
|
+
];
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
expect(row).toHaveClass("focus:bg-accent-subtle", "focus:text-foreground");
|
|
205
|
+
expect(row).not.toHaveClass("focus:bg-accent");
|
|
206
|
+
expect(row).not.toHaveClass("focus:text-accent-foreground");
|
|
207
|
+
}
|
|
208
|
+
expect(rows[1]?.querySelector("svg")).toHaveClass("text-accent");
|
|
209
|
+
expect(rows[2]?.querySelector("svg")).toHaveClass("text-accent");
|
|
210
|
+
});
|
|
180
211
|
});
|
|
@@ -67,7 +67,7 @@ function DropdownMenuItem({
|
|
|
67
67
|
data-inset={inset}
|
|
68
68
|
data-variant={variant}
|
|
69
69
|
className={cn(
|
|
70
|
-
"focus:bg-accent focus:text-
|
|
70
|
+
"focus:bg-accent-subtle focus:text-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
71
71
|
className
|
|
72
72
|
)}
|
|
73
73
|
{...props}
|
|
@@ -89,7 +89,7 @@ function DropdownMenuCheckboxItem({
|
|
|
89
89
|
<DropdownMenuPrimitive.CheckboxItem
|
|
90
90
|
data-slot="dropdown-menu-checkbox-item"
|
|
91
91
|
className={cn(
|
|
92
|
-
"focus:bg-accent focus:text-
|
|
92
|
+
"focus:bg-accent-subtle focus:text-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
93
93
|
className
|
|
94
94
|
)}
|
|
95
95
|
checked={checked}
|
|
@@ -111,7 +111,7 @@ function DropdownMenuCheckboxItem({
|
|
|
111
111
|
</span>
|
|
112
112
|
) : (
|
|
113
113
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
114
|
-
<CheckIcon className="size-4" />
|
|
114
|
+
<CheckIcon className="size-4 text-accent" />
|
|
115
115
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
116
116
|
)}
|
|
117
117
|
</span>
|
|
@@ -135,14 +135,14 @@ function DropdownMenuRadioItem({
|
|
|
135
135
|
<DropdownMenuPrimitive.RadioItem
|
|
136
136
|
data-slot="dropdown-menu-radio-item"
|
|
137
137
|
className={cn(
|
|
138
|
-
"focus:bg-accent focus:text-
|
|
138
|
+
"focus:bg-accent-subtle focus:text-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
139
139
|
className
|
|
140
140
|
)}
|
|
141
141
|
{...props}
|
|
142
142
|
>
|
|
143
143
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
144
144
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
145
|
-
<CircleIcon className="size-2 fill-current" />
|
|
145
|
+
<CircleIcon className="size-2 fill-current text-accent" />
|
|
146
146
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
147
147
|
</span>
|
|
148
148
|
{children}
|
|
@@ -207,7 +207,7 @@ function DropdownMenuSubTrigger({
|
|
|
207
207
|
data-slot="dropdown-menu-sub-trigger"
|
|
208
208
|
data-inset={inset}
|
|
209
209
|
className={cn(
|
|
210
|
-
"focus:bg-accent focus:text-
|
|
210
|
+
"focus:bg-accent-subtle focus:text-foreground data-[state=open]:bg-accent-subtle data-[state=open]:text-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
|
211
211
|
className
|
|
212
212
|
)}
|
|
213
213
|
{...props}
|
|
@@ -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-semibold text-muted-foreground transition-colors hover:bg-accent hover:text-
|
|
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-subtle hover:text-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}
|
|
@@ -93,7 +93,13 @@ export function MultiSelectCombobox({
|
|
|
93
93
|
align="start"
|
|
94
94
|
>
|
|
95
95
|
<Command shouldFilter={shouldFilter}>
|
|
96
|
-
|
|
96
|
+
{/* Spread rather than `onValueChange={onSearchChange}`: under a
|
|
97
|
+
consumer's `exactOptionalPropertyTypes`, passing a possibly
|
|
98
|
+
`undefined` value to cmdk's optional prop does not typecheck. */}
|
|
99
|
+
<CommandInput
|
|
100
|
+
placeholder={searchPlaceholder}
|
|
101
|
+
{...(onSearchChange ? { onValueChange: onSearchChange } : {})}
|
|
102
|
+
/>
|
|
97
103
|
<CommandList>
|
|
98
104
|
<CommandEmpty>{loading ? "Loading…" : emptyText}</CommandEmpty>
|
|
99
105
|
<CommandGroup>
|
|
@@ -81,7 +81,7 @@ export function MultiSelectMenu({
|
|
|
81
81
|
onCheckedChange={() => handleToggle(option.value)}
|
|
82
82
|
className={cn(
|
|
83
83
|
"relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
|
|
84
|
-
"data-[highlighted]:bg-accent data-[highlighted]:text-
|
|
84
|
+
"data-[highlighted]:bg-accent-subtle data-[highlighted]:text-foreground",
|
|
85
85
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
|
86
86
|
)}
|
|
87
87
|
>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { PanZoomViewport, type PanZoomViewportHandle } from "./pan-zoom-viewport";
|
|
5
|
+
|
|
6
|
+
const labels = {
|
|
7
|
+
viewport: "Floor plan, Inside",
|
|
8
|
+
instructions: "Arrow keys move the plan. Plus and minus zoom; 0 fits it.",
|
|
9
|
+
zoomIn: "Zoom in",
|
|
10
|
+
zoomOut: "Zoom out",
|
|
11
|
+
fit: "Fit",
|
|
12
|
+
controls: "Zoom",
|
|
13
|
+
zoomLevel: (percent: number) => `${percent}%`,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** A stand-in plan, painted with tokens only: the real canvas is the app's. */
|
|
17
|
+
const TABLES = [
|
|
18
|
+
{ id: "T1", x: 120, y: 240, w: 90, h: 90, round: true },
|
|
19
|
+
{ id: "T2", x: 270, y: 240, w: 90, h: 90, round: true },
|
|
20
|
+
{ id: "T3", x: 420, y: 240, w: 90, h: 90, round: false },
|
|
21
|
+
{ id: "T4", x: 120, y: 420, w: 120, h: 120, round: false },
|
|
22
|
+
{ id: "T5", x: 300, y: 420, w: 220, h: 90, round: false },
|
|
23
|
+
{ id: "T6", x: 300, y: 600, w: 180, h: 90, round: false },
|
|
24
|
+
{ id: "T7", x: 900, y: 220, w: 180, h: 120, round: false },
|
|
25
|
+
{ id: "T8", x: 700, y: 240, w: 120, h: 120, round: false },
|
|
26
|
+
{ id: "T9", x: 700, y: 420, w: 120, h: 120, round: false },
|
|
27
|
+
{ id: "T12", x: 900, y: 600, w: 180, h: 90, round: false },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function DemoPlan({ onPick }: { onPick?: (id: string) => void }) {
|
|
31
|
+
return (
|
|
32
|
+
<div className="relative size-full rounded-sm border border-border bg-[length:24px_24px] bg-[linear-gradient(to_right,var(--line-soft)_1px,transparent_1px),linear-gradient(to_bottom,var(--line-soft)_1px,transparent_1px)]">
|
|
33
|
+
<div className="absolute left-16 top-16 grid h-20 w-96 place-items-center border border-border text-xs text-muted-foreground">
|
|
34
|
+
Bar counter
|
|
35
|
+
</div>
|
|
36
|
+
{TABLES.map((table) => (
|
|
37
|
+
<button
|
|
38
|
+
key={table.id}
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={() => onPick?.(table.id)}
|
|
41
|
+
aria-label={`Table ${table.id}`}
|
|
42
|
+
className={
|
|
43
|
+
"absolute grid place-items-center border border-border bg-card text-sm font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " +
|
|
44
|
+
(table.round ? "rounded-full" : "rounded-md")
|
|
45
|
+
}
|
|
46
|
+
style={{ left: table.x, top: table.y, width: table.w, height: table.h }}
|
|
47
|
+
>
|
|
48
|
+
{table.id}
|
|
49
|
+
</button>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const meta: Meta<typeof PanZoomViewport> = {
|
|
56
|
+
title: "Booking/PanZoomViewport",
|
|
57
|
+
component: PanZoomViewport,
|
|
58
|
+
tags: ["autodocs"],
|
|
59
|
+
parameters: {
|
|
60
|
+
docs: {
|
|
61
|
+
description: {
|
|
62
|
+
component:
|
|
63
|
+
"A window onto a plan that pans and zooms: drag, pinch, trackpad pinch or ctrl-wheel, arrows / + / − / 0 on the focused viewport, and the + / − / Fit controls. Fit never goes below `minScale`; past the floor the plan anchors top-left and the edges it runs past are shaded. Every string is injected through `labels`.",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default meta;
|
|
70
|
+
type Story = StoryObj<typeof PanZoomViewport>;
|
|
71
|
+
|
|
72
|
+
/** floor/07-f0 desktop: the plan fits the viewport. Tap a table: the click still lands. */
|
|
73
|
+
export const Desktop: Story = {
|
|
74
|
+
render: () => {
|
|
75
|
+
const [picked, setPicked] = React.useState<string | null>(null);
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex flex-col gap-2">
|
|
78
|
+
<PanZoomViewport
|
|
79
|
+
contentWidth={1200}
|
|
80
|
+
contentHeight={800}
|
|
81
|
+
labels={labels}
|
|
82
|
+
className="h-[560px] w-[900px] rounded-xl border border-border bg-background"
|
|
83
|
+
>
|
|
84
|
+
<DemoPlan onPick={setPicked} />
|
|
85
|
+
</PanZoomViewport>
|
|
86
|
+
<p className="text-sm text-muted-foreground">Picked: {picked ?? "none"}</p>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** floor/07-f0 phone: the fit would be unreadable, so it stops at the floor and pans. */
|
|
93
|
+
export const PhoneAtTheFloor: Story = {
|
|
94
|
+
render: () => (
|
|
95
|
+
<PanZoomViewport
|
|
96
|
+
contentWidth={1200}
|
|
97
|
+
contentHeight={800}
|
|
98
|
+
minScale={0.55}
|
|
99
|
+
labels={labels}
|
|
100
|
+
className="h-[480px] w-[390px] border-y border-border bg-background"
|
|
101
|
+
>
|
|
102
|
+
<DemoPlan />
|
|
103
|
+
</PanZoomViewport>
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Driving it from outside: a "Show T12" link, a reset. */
|
|
108
|
+
export const Imperative: Story = {
|
|
109
|
+
render: () => {
|
|
110
|
+
const api = React.useRef<PanZoomViewportHandle>(null);
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex flex-col gap-2">
|
|
113
|
+
<div className="flex gap-2">
|
|
114
|
+
<Button variant="outline" size="sm" onClick={() => api.current?.centerOn(990, 645)}>
|
|
115
|
+
Show T12
|
|
116
|
+
</Button>
|
|
117
|
+
<Button variant="outline" size="sm" onClick={() => api.current?.fit()}>
|
|
118
|
+
Reset
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
<PanZoomViewport
|
|
122
|
+
apiRef={api}
|
|
123
|
+
contentWidth={1200}
|
|
124
|
+
contentHeight={800}
|
|
125
|
+
minScale={0.8}
|
|
126
|
+
labels={labels}
|
|
127
|
+
className="h-[420px] w-[640px] rounded-xl border border-border bg-background"
|
|
128
|
+
>
|
|
129
|
+
<DemoPlan />
|
|
130
|
+
</PanZoomViewport>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
};
|