@bearmenu/ui 0.8.33 → 0.8.35
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-GESB7GCF.js → chunk-6W25S5CF.js} +1 -1
- package/dist/chunk-LY67SOFC.js +142 -0
- package/dist/{chunk-ZU4NAYN7.js → chunk-NIAL4ZU2.js} +13 -1
- package/dist/{chunk-JWTN7MNK.js → chunk-TVEIUTBQ.js} +2 -2
- package/dist/{chunk-ZJMCU7MI.js → chunk-VCRXL7H5.js} +1 -1
- package/dist/chunk-XLHMF6F5.js +127 -0
- package/dist/{chunk-VSRGEB36.js → chunk-XNBDACL7.js} +1 -1
- package/dist/components/action-bar.d.ts +67 -0
- package/dist/components/action-bar.js +43 -0
- package/dist/components/activity-row.d.ts +38 -0
- package/dist/components/activity-row.js +43 -0
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/avatar.d.ts +4 -1
- package/dist/components/avatar.js +39 -5
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/booking-row.d.ts +11 -1
- package/dist/components/booking-row.js +4 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/combobox.js +2 -2
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +2 -2
- package/dist/components/concept-convergence.js +2 -2
- package/dist/components/currency-input.js +4 -142
- package/dist/components/dialog.d.ts +6 -0
- package/dist/components/dialog.js +1 -1
- package/dist/components/drawer.d.ts +6 -0
- package/dist/components/drawer.js +3 -127
- package/dist/components/empty-state.d.ts +2 -2
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/menu-pick-row.d.ts +47 -0
- package/dist/components/menu-pick-row.js +49 -0
- package/dist/components/multi-select-combobox.js +3 -3
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +1 -1
- package/dist/components/qr-panel.d.ts +31 -0
- package/dist/components/qr-panel.js +33 -0
- package/dist/components/reconcile-bar.d.ts +47 -0
- package/dist/components/reconcile-bar.js +60 -0
- package/dist/components/responsive-overlay.d.ts +59 -0
- package/dist/components/responsive-overlay.js +132 -0
- package/dist/components/searchable-select.js +2 -2
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/split-share-row.d.ts +77 -0
- package/dist/components/split-share-row.js +101 -0
- package/dist/components/table-ledger-card.d.ts +80 -0
- package/dist/components/table-ledger-card.js +84 -0
- package/dist/components/table-trigger-band.d.ts +56 -0
- package/dist/components/table-trigger-band.js +43 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +2 -2
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/top-ranking-entry.js +3 -3
- package/dist/components/venue-events-block.js +1 -1
- package/package.json +23 -207
- package/src/components/action-bar.stories.tsx +78 -0
- package/src/components/action-bar.tsx +80 -0
- package/src/components/activity-row.stories.tsx +78 -0
- package/src/components/activity-row.tsx +65 -0
- package/src/components/avatar.stories.tsx +55 -0
- package/src/components/avatar.tsx +63 -19
- package/src/components/badge.stories.tsx +41 -0
- package/src/components/badge.tsx +13 -0
- package/src/components/booking-row.tsx +13 -0
- package/src/components/dialog.tsx +8 -2
- package/src/components/drawer.tsx +8 -2
- package/src/components/menu-pick-row.stories.tsx +71 -0
- package/src/components/menu-pick-row.tsx +76 -0
- package/src/components/qr-panel.stories.tsx +39 -0
- package/src/components/qr-panel.tsx +52 -0
- package/src/components/reconcile-bar.stories.tsx +57 -0
- package/src/components/reconcile-bar.tsx +80 -0
- package/src/components/responsive-overlay.stories.tsx +91 -0
- package/src/components/responsive-overlay.test.tsx +162 -0
- package/src/components/responsive-overlay.tsx +232 -0
- package/src/components/shared-table-primitives.test.tsx +482 -0
- package/src/components/split-share-row.stories.tsx +114 -0
- package/src/components/split-share-row.tsx +150 -0
- package/src/components/table-ledger-card.stories.tsx +120 -0
- package/src/components/table-ledger-card.tsx +138 -0
- package/src/components/table-trigger-band.stories.tsx +76 -0
- package/src/components/table-trigger-band.tsx +75 -0
- package/src/globals.css +44 -0
- package/src/test/setup.ts +11 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { useMediaQuery } from "../hooks/use-media-query";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "./dialog";
|
|
8
|
+
import { Drawer, DrawerContent, DrawerDescription, DrawerTitle } from "./drawer";
|
|
9
|
+
|
|
10
|
+
/** The board switches container at Tailwind's `md`, not at the library's `lg`. */
|
|
11
|
+
const MD_MEDIA_QUERY = "(min-width: 768px)";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sends focus back to whatever opened the overlay once it closes.
|
|
15
|
+
*
|
|
16
|
+
* Radix restores focus to its own `DialogTrigger`, and this overlay is
|
|
17
|
+
* controlled — the opener is an ordinary button somewhere else on the page, or
|
|
18
|
+
* a URL that was already carrying `?add=1` on arrival. Left to the primitives,
|
|
19
|
+
* focus lands on `document.body` and a keyboard user starts again from the top
|
|
20
|
+
* of the page. The shared table's brief asks for the opposite in as many words:
|
|
21
|
+
* "focus returns to the control that opened an overlay or a sheet".
|
|
22
|
+
*
|
|
23
|
+
* The opener is tracked while the overlay is CLOSED rather than captured when
|
|
24
|
+
* it opens: by the time an effect could run on the opening render, the dialog
|
|
25
|
+
* has already mounted and pulled focus into itself.
|
|
26
|
+
*/
|
|
27
|
+
function useFocusReturn(open: boolean) {
|
|
28
|
+
const openerRef = React.useRef<HTMLElement | null>(null);
|
|
29
|
+
const wasOpen = React.useRef(false);
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (open) {
|
|
33
|
+
wasOpen.current = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (wasOpen.current) {
|
|
38
|
+
wasOpen.current = false;
|
|
39
|
+
const opener = openerRef.current;
|
|
40
|
+
// The overlay is still unmounting on this tick, and it takes focus with
|
|
41
|
+
// it; restoring on the next frame lands after that.
|
|
42
|
+
if (opener && opener.isConnected) {
|
|
43
|
+
requestAnimationFrame(() => opener.focus());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const track = (event: FocusEvent) => {
|
|
48
|
+
const target = event.target;
|
|
49
|
+
if (target instanceof HTMLElement) openerRef.current = target;
|
|
50
|
+
};
|
|
51
|
+
if (document.activeElement instanceof HTMLElement && document.activeElement !== document.body) {
|
|
52
|
+
openerRef.current = document.activeElement;
|
|
53
|
+
}
|
|
54
|
+
document.addEventListener("focusin", track);
|
|
55
|
+
return () => document.removeEventListener("focusin", track);
|
|
56
|
+
}, [open]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ResponsiveOverlayProps = {
|
|
60
|
+
open: boolean;
|
|
61
|
+
onOpenChange: (open: boolean) => void;
|
|
62
|
+
/** The overlay's name — "Add an item", "Split this item", "Invite people". */
|
|
63
|
+
title: React.ReactNode;
|
|
64
|
+
/** The one line under it — "Table 18 · from the menu". Optional, never two lines. */
|
|
65
|
+
subtitle?: React.ReactNode;
|
|
66
|
+
/** The scrolling body. */
|
|
67
|
+
children: React.ReactNode;
|
|
68
|
+
/** The closing band — an `ActionBar`, usually. */
|
|
69
|
+
footer?: React.ReactNode;
|
|
70
|
+
/**
|
|
71
|
+
* The close button's accessible name. Injected, like every label here: the
|
|
72
|
+
* design system holds no copy and no i18n.
|
|
73
|
+
*/
|
|
74
|
+
closeLabel?: string;
|
|
75
|
+
/** Widths for the desktop dialog. Mobile is always the full-width drawer. */
|
|
76
|
+
size?: React.ComponentPropsWithoutRef<typeof DialogContent>["size"];
|
|
77
|
+
/** Classes for the panel itself — the dialog box or the drawer sheet. */
|
|
78
|
+
className?: string;
|
|
79
|
+
/** Classes for the scrolling body. */
|
|
80
|
+
bodyClassName?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* ResponsiveOverlay — one overlay authored once: a `Drawer` under md, a
|
|
85
|
+
* `Dialog` from md.
|
|
86
|
+
*
|
|
87
|
+
* The shared-table board draws the same chrome on T1, T3, T5, T6 and T8
|
|
88
|
+
* (16-shared-table.html) and the two widths never disagree about it: the
|
|
89
|
+
* title and its one-line subtitle top left, a round close top right, the body,
|
|
90
|
+
* and a footer band carrying the note and the actions. What differs is the
|
|
91
|
+
* container — a sheet that a thumb drags down, or a box in the middle of the
|
|
92
|
+
* screen — and that is the only thing this component switches.
|
|
93
|
+
*
|
|
94
|
+
* The scrim is `--scrim-overlay`. bearmenu-fe's `globals.css` loads after the
|
|
95
|
+
* design system's and defines a `--scrim` of its own — an opaque photo hue —
|
|
96
|
+
* so an overlay that reached for `bg-scrim` would paint that colour over the
|
|
97
|
+
* whole page at full alpha. The two drawer/dialog primitives still carry their
|
|
98
|
+
* older `bg-black/…`; this component states the token explicitly rather than
|
|
99
|
+
* changing theirs underneath every other caller.
|
|
100
|
+
*
|
|
101
|
+
* It resolves the width with `useMediaQuery`, which is mobile-first: the
|
|
102
|
+
* drawer is what the server and the first client frame render. A wide screen
|
|
103
|
+
* that arrives with the overlay already open — an invite link opened on a
|
|
104
|
+
* laptop — therefore shows the sheet for one frame before the dialog replaces
|
|
105
|
+
* it. That is the library's documented convention for every breakpoint-
|
|
106
|
+
* switching component, and a second breakpoint hook with different semantics
|
|
107
|
+
* would be a worse thing to own than one frame.
|
|
108
|
+
*/
|
|
109
|
+
const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProps>(
|
|
110
|
+
(
|
|
111
|
+
{
|
|
112
|
+
open,
|
|
113
|
+
onOpenChange,
|
|
114
|
+
title,
|
|
115
|
+
subtitle,
|
|
116
|
+
children,
|
|
117
|
+
footer,
|
|
118
|
+
closeLabel = "Close",
|
|
119
|
+
size = "lg",
|
|
120
|
+
className,
|
|
121
|
+
bodyClassName,
|
|
122
|
+
},
|
|
123
|
+
ref
|
|
124
|
+
) => {
|
|
125
|
+
const isWide = useMediaQuery(MD_MEDIA_QUERY);
|
|
126
|
+
const titleId = React.useId();
|
|
127
|
+
const subtitleId = React.useId();
|
|
128
|
+
useFocusReturn(open);
|
|
129
|
+
|
|
130
|
+
const header = (
|
|
131
|
+
<div data-slot="responsive-overlay-header" className="flex items-start gap-3 px-5 pt-5">
|
|
132
|
+
<div className="min-w-0 flex-1">
|
|
133
|
+
{isWide ? (
|
|
134
|
+
<DialogTitle id={titleId} className="text-[19px] font-bold leading-tight">
|
|
135
|
+
{title}
|
|
136
|
+
</DialogTitle>
|
|
137
|
+
) : (
|
|
138
|
+
<DrawerTitle id={titleId} className="text-[19px] font-bold leading-tight">
|
|
139
|
+
{title}
|
|
140
|
+
</DrawerTitle>
|
|
141
|
+
)}
|
|
142
|
+
{subtitle ? (
|
|
143
|
+
isWide ? (
|
|
144
|
+
<DialogDescription id={subtitleId} className="mt-1 text-[13px] text-muted-foreground">
|
|
145
|
+
{subtitle}
|
|
146
|
+
</DialogDescription>
|
|
147
|
+
) : (
|
|
148
|
+
<DrawerDescription id={subtitleId} className="mt-1 text-[13px] text-muted-foreground">
|
|
149
|
+
{subtitle}
|
|
150
|
+
</DrawerDescription>
|
|
151
|
+
)
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
data-slot="responsive-overlay-close"
|
|
157
|
+
onClick={() => onOpenChange(false)}
|
|
158
|
+
// 32px disc, 44px of tap target around it — the frames draw the disc,
|
|
159
|
+
// the accessibility floor asks for the target.
|
|
160
|
+
className="-m-1.5 grid size-11 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
161
|
+
>
|
|
162
|
+
<span className="grid size-8 place-items-center rounded-full bg-surface-2 transition-colors hover:bg-muted">
|
|
163
|
+
<X className="size-4" aria-hidden />
|
|
164
|
+
</span>
|
|
165
|
+
<span className="sr-only">{closeLabel}</span>
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const body = (
|
|
171
|
+
<div
|
|
172
|
+
data-slot="responsive-overlay-body"
|
|
173
|
+
className={cn("min-h-0 flex-1 overflow-y-auto px-5 py-4", bodyClassName)}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const foot = footer ? (
|
|
180
|
+
<div
|
|
181
|
+
data-slot="responsive-overlay-footer"
|
|
182
|
+
className="shrink-0 border-t border-border bg-surface-2"
|
|
183
|
+
>
|
|
184
|
+
{footer}
|
|
185
|
+
</div>
|
|
186
|
+
) : null;
|
|
187
|
+
|
|
188
|
+
if (isWide) {
|
|
189
|
+
return (
|
|
190
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
191
|
+
<DialogContent
|
|
192
|
+
ref={ref}
|
|
193
|
+
size={size}
|
|
194
|
+
showCloseButton={false}
|
|
195
|
+
overlayClassName="bg-scrim-overlay backdrop-blur-none"
|
|
196
|
+
aria-labelledby={titleId}
|
|
197
|
+
aria-describedby={subtitle ? subtitleId : undefined}
|
|
198
|
+
className={cn(
|
|
199
|
+
// The dialog's own `grid gap-4 p-6` would put air between the
|
|
200
|
+
// header, the body and the band the frames draw flush.
|
|
201
|
+
"flex max-h-[85vh] flex-col gap-0 overflow-hidden p-0",
|
|
202
|
+
className
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
{header}
|
|
206
|
+
{body}
|
|
207
|
+
{foot}
|
|
208
|
+
</DialogContent>
|
|
209
|
+
</Dialog>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
215
|
+
<DrawerContent
|
|
216
|
+
ref={ref}
|
|
217
|
+
overlayClassName="bg-scrim-overlay"
|
|
218
|
+
aria-labelledby={titleId}
|
|
219
|
+
aria-describedby={subtitle ? subtitleId : undefined}
|
|
220
|
+
className={cn("flex max-h-[85vh] flex-col p-0", className)}
|
|
221
|
+
>
|
|
222
|
+
{header}
|
|
223
|
+
{body}
|
|
224
|
+
{foot}
|
|
225
|
+
</DrawerContent>
|
|
226
|
+
</Drawer>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
ResponsiveOverlay.displayName = "ResponsiveOverlay";
|
|
231
|
+
|
|
232
|
+
export { ResponsiveOverlay };
|