@bearmenu/ui 0.8.35 → 0.8.36
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-6W25S5CF.js → chunk-KGXTBTCJ.js} +1 -1
- package/dist/chunk-LAEVMNJN.js +135 -0
- 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/button.d.ts +2 -2
- package/dist/components/command.d.ts +3 -3
- package/dist/components/concept-convergence.js +1 -1
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/multi-select-combobox.js +8 -3
- 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 +45 -6
- 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/spinner.d.ts +2 -2
- package/dist/components/tabs.d.ts +16 -1
- package/dist/components/tabs.js +21 -3
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +2 -2
- 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/multi-select-combobox.tsx +7 -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/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
|
@@ -23,6 +23,32 @@ type ResponsiveOverlayProps = {
|
|
|
23
23
|
closeLabel?: string;
|
|
24
24
|
/** Widths for the desktop dialog. Mobile is always the full-width drawer. */
|
|
25
25
|
size?: React.ComponentPropsWithoutRef<typeof DialogContent>["size"];
|
|
26
|
+
/**
|
|
27
|
+
* The container on a wide screen. `dialog` (the default) is the centred box.
|
|
28
|
+
* `sheet` is a right rail: an inset panel held 12px off the top, right and
|
|
29
|
+
* bottom, 420px wide — the booking pages' table sheet, booking details,
|
|
30
|
+
* guest panel and create forms (floor/14-w1, calendar/07-n1, guests/07-g6).
|
|
31
|
+
* Narrow screens get the bottom drawer either way.
|
|
32
|
+
*/
|
|
33
|
+
desktop?: "dialog" | "sheet";
|
|
34
|
+
/**
|
|
35
|
+
* Whether the wide `sheet` dims and locks the page. `true` (the default)
|
|
36
|
+
* is a form rail: a scrim, focus held inside, the page inert. `false` is a
|
|
37
|
+
* working rail: no scrim, no focus trap, and the page behind stays live —
|
|
38
|
+
* clicking another table on the floor does NOT close it, so the caller can
|
|
39
|
+
* swap what it shows. Escape and the close button still close it.
|
|
40
|
+
*
|
|
41
|
+
* Applies to `desktop="sheet"` only. The dialog is always modal, and so is
|
|
42
|
+
* the phone drawer: at 390 the rail covers most of the page, and a page
|
|
43
|
+
* that stays live under a sheet the thumb cannot see past is a trap.
|
|
44
|
+
*/
|
|
45
|
+
modal?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Where the wide container takes over from the drawer. `md` (768, the
|
|
48
|
+
* default) for most overlays; `lg` (1024) where the page beside the rail
|
|
49
|
+
* needs the room — the floor plan's rails switch there.
|
|
50
|
+
*/
|
|
51
|
+
wideFrom?: "md" | "lg";
|
|
26
52
|
/** Classes for the panel itself — the dialog box or the drawer sheet. */
|
|
27
53
|
className?: string;
|
|
28
54
|
/** Classes for the scrolling body. */
|
|
@@ -30,7 +56,9 @@ type ResponsiveOverlayProps = {
|
|
|
30
56
|
};
|
|
31
57
|
/**
|
|
32
58
|
* ResponsiveOverlay — one overlay authored once: a `Drawer` under md, a
|
|
33
|
-
* `Dialog` from md
|
|
59
|
+
* `Dialog` from md — or, with `desktop="sheet"`, an inset right rail that can
|
|
60
|
+
* leave the page live (`modal={false}`), switching at md or at lg
|
|
61
|
+
* (`wideFrom`).
|
|
34
62
|
*
|
|
35
63
|
* The shared-table board draws the same chrome on T1, T3, T5, T6 and T8
|
|
36
64
|
* (16-shared-table.html) and the two widths never disagree about it: the
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { useMediaQuery } from '../chunk-OPVKL4VS.js';
|
|
1
|
+
import { useMediaQuery, DESKTOP_MEDIA_QUERY } from '../chunk-OPVKL4VS.js';
|
|
2
|
+
import { Sheet, SheetContent, SheetTitle, SheetDescription } from '../chunk-LAEVMNJN.js';
|
|
2
3
|
import { Drawer, DrawerContent, DrawerTitle, DrawerDescription } from '../chunk-XLHMF6F5.js';
|
|
3
4
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '../chunk-TVEIUTBQ.js';
|
|
4
5
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
5
|
-
import '../chunk-MMUBH76A.js';
|
|
6
|
+
import { __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
6
7
|
import * as React from 'react';
|
|
7
8
|
import { X } from 'lucide-react';
|
|
8
9
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
9
10
|
|
|
10
11
|
var MD_MEDIA_QUERY = "(min-width: 768px)";
|
|
12
|
+
var CONTAINER_TITLE = { drawer: DrawerTitle, dialog: DialogTitle, sheet: SheetTitle };
|
|
13
|
+
var CONTAINER_DESCRIPTION = {
|
|
14
|
+
drawer: DrawerDescription,
|
|
15
|
+
dialog: DialogDescription,
|
|
16
|
+
sheet: SheetDescription
|
|
17
|
+
};
|
|
11
18
|
function useFocusReturn(open) {
|
|
12
19
|
const openerRef = React.useRef(null);
|
|
13
20
|
const wasOpen = React.useRef(false);
|
|
@@ -44,17 +51,23 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
44
51
|
footer,
|
|
45
52
|
closeLabel = "Close",
|
|
46
53
|
size = "lg",
|
|
54
|
+
desktop = "dialog",
|
|
55
|
+
modal = true,
|
|
56
|
+
wideFrom = "md",
|
|
47
57
|
className,
|
|
48
58
|
bodyClassName
|
|
49
59
|
}, ref) => {
|
|
50
|
-
const isWide = useMediaQuery(MD_MEDIA_QUERY);
|
|
60
|
+
const isWide = useMediaQuery(wideFrom === "lg" ? DESKTOP_MEDIA_QUERY : MD_MEDIA_QUERY);
|
|
61
|
+
const container = isWide ? desktop : "drawer";
|
|
51
62
|
const titleId = React.useId();
|
|
52
63
|
const subtitleId = React.useId();
|
|
53
64
|
useFocusReturn(open);
|
|
65
|
+
const Title = CONTAINER_TITLE[container];
|
|
66
|
+
const Description = CONTAINER_DESCRIPTION[container];
|
|
54
67
|
const header = /* @__PURE__ */ jsxs("div", { "data-slot": "responsive-overlay-header", className: "flex items-start gap-3 px-5 pt-5", children: [
|
|
55
68
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
56
|
-
|
|
57
|
-
subtitle ?
|
|
69
|
+
/* @__PURE__ */ jsx(Title, { id: titleId, className: "text-[19px] font-bold leading-tight", children: title }),
|
|
70
|
+
subtitle ? /* @__PURE__ */ jsx(Description, { id: subtitleId, className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : null
|
|
58
71
|
] }),
|
|
59
72
|
/* @__PURE__ */ jsxs(
|
|
60
73
|
"button",
|
|
@@ -86,7 +99,33 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
86
99
|
children: footer
|
|
87
100
|
}
|
|
88
101
|
) : null;
|
|
89
|
-
if (
|
|
102
|
+
if (container === "sheet") {
|
|
103
|
+
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, modal, children: /* @__PURE__ */ jsxs(
|
|
104
|
+
SheetContent,
|
|
105
|
+
__spreadProps(__spreadValues({
|
|
106
|
+
ref,
|
|
107
|
+
side: "right",
|
|
108
|
+
attachment: "inset",
|
|
109
|
+
hideClose: true,
|
|
110
|
+
overlayClassName: "bg-scrim-overlay backdrop-blur-none",
|
|
111
|
+
"aria-labelledby": titleId,
|
|
112
|
+
"aria-describedby": subtitle ? subtitleId : void 0,
|
|
113
|
+
"data-slot": "responsive-overlay-sheet",
|
|
114
|
+
"data-modal": modal ? "true" : "false"
|
|
115
|
+
}, modal ? {} : { onInteractOutside: (event) => event.preventDefault() }), {
|
|
116
|
+
className: cn(
|
|
117
|
+
"flex w-[420px] max-w-[calc(100vw-1.5rem)] flex-col gap-0 bg-card p-0",
|
|
118
|
+
className
|
|
119
|
+
),
|
|
120
|
+
children: [
|
|
121
|
+
header,
|
|
122
|
+
body,
|
|
123
|
+
foot
|
|
124
|
+
]
|
|
125
|
+
})
|
|
126
|
+
) });
|
|
127
|
+
}
|
|
128
|
+
if (container === "dialog") {
|
|
90
129
|
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
91
130
|
DialogContent,
|
|
92
131
|
{
|
|
@@ -36,6 +36,12 @@ interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof Dialog
|
|
|
36
36
|
* buttons in the same corner.
|
|
37
37
|
*/
|
|
38
38
|
hideClose?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Classes for the scrim. The default `bg-black/50` is kept for every
|
|
41
|
+
* existing caller; pass `bg-scrim-overlay` for the themed dim, as `Drawer`
|
|
42
|
+
* and `Dialog` already allow. A non-modal sheet renders no scrim at all.
|
|
43
|
+
*/
|
|
44
|
+
overlayClassName?: string;
|
|
39
45
|
}
|
|
40
46
|
declare const SheetContent: React.ForwardRefExoticComponent<SheetContentProps & React.RefAttributes<HTMLDivElement>>;
|
|
41
47
|
declare const SheetHeader: {
|
package/dist/components/sheet.js
CHANGED
|
@@ -1,133 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
|
5
|
-
import { cva } from 'class-variance-authority';
|
|
6
|
-
import { X } from 'lucide-react';
|
|
7
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
8
|
-
|
|
9
|
-
var Sheet = SheetPrimitive.Root;
|
|
10
|
-
var SheetTrigger = SheetPrimitive.Trigger;
|
|
11
|
-
var SheetClose = SheetPrimitive.Close;
|
|
12
|
-
var SheetPortal = SheetPrimitive.Portal;
|
|
13
|
-
var SheetOverlay = React.forwardRef((_a, ref) => {
|
|
14
|
-
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
15
|
-
return /* @__PURE__ */ jsx(
|
|
16
|
-
SheetPrimitive.Overlay,
|
|
17
|
-
__spreadProps(__spreadValues({
|
|
18
|
-
className: cn(
|
|
19
|
-
"fixed inset-0 z-[1000] bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
20
|
-
className
|
|
21
|
-
)
|
|
22
|
-
}, props), {
|
|
23
|
-
ref
|
|
24
|
-
})
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
|
28
|
-
var sheetVariants = cva(
|
|
29
|
-
"fixed z-[1000] gap-4 bg-background border-border shadow-[var(--shadow-overlay)] 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)]",
|
|
30
|
-
{
|
|
31
|
-
variants: {
|
|
32
|
-
side: {
|
|
33
|
-
top: "inset-x-0 top-0 data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
34
|
-
bottom: "inset-x-0 bottom-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
35
|
-
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",
|
|
36
|
-
right: "inset-y-0 right-0 h-full w-3/4 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right"
|
|
37
|
-
},
|
|
38
|
-
attachment: {
|
|
39
|
-
edge: "p-6",
|
|
40
|
-
// Padding is the caller's business here: a detail surface runs a gallery
|
|
41
|
-
// and a sticky action bar to the panel edge, and a fixed p-6 would inset
|
|
42
|
-
// both. `edge` keeps its p-6 so nothing already shipped shifts.
|
|
43
|
-
inset: "rounded-2xl overflow-hidden"
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
compoundVariants: [
|
|
47
|
-
// `edge` restates exactly what each side carried before `attachment`
|
|
48
|
-
// existed — the seam border and the two inward-facing corners. Kept as
|
|
49
|
-
// compounds rather than left on `side` so `inset` is not fighting them
|
|
50
|
-
// through class-merge order.
|
|
51
|
-
// `sm:max-w-sm` lives here, not on `side`: an inset panel is sized by its
|
|
52
|
-
// caller, and a responsive cap left on `side` cannot be overridden from
|
|
53
|
-
// className — tailwind-merge scopes `sm:max-w-*` and `max-w-*` to
|
|
54
|
-
// different groups, so `max-w-none` loses to it at every width ≥ sm.
|
|
55
|
-
{ side: "top", attachment: "edge", class: "border-b" },
|
|
56
|
-
{ side: "bottom", attachment: "edge", class: "border-t rounded-t-2xl" },
|
|
57
|
-
{ side: "left", attachment: "edge", class: "border-r rounded-r-2xl sm:max-w-sm" },
|
|
58
|
-
{ side: "right", attachment: "edge", class: "border-l rounded-l-2xl sm:max-w-sm" },
|
|
59
|
-
// The inset offsets live here rather than on `attachment` because each
|
|
60
|
-
// side has to give up its own edge: a right-side panel clears top, right
|
|
61
|
-
// and bottom but is still anchored to the right.
|
|
62
|
-
{ side: "right", attachment: "inset", class: "inset-y-3 right-3 h-auto" },
|
|
63
|
-
{ side: "left", attachment: "inset", class: "inset-y-3 left-3 h-auto" },
|
|
64
|
-
{ side: "top", attachment: "inset", class: "inset-x-3 top-3" },
|
|
65
|
-
{ side: "bottom", attachment: "inset", class: "inset-x-3 bottom-3" }
|
|
66
|
-
],
|
|
67
|
-
defaultVariants: {
|
|
68
|
-
side: "right",
|
|
69
|
-
attachment: "edge"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
);
|
|
73
|
-
var SheetContent = React.forwardRef((_a, ref) => {
|
|
74
|
-
var _b = _a, { side = "right", attachment = "edge", className, children, hideClose } = _b, props = __objRest(_b, ["side", "attachment", "className", "children", "hideClose"]);
|
|
75
|
-
return /* @__PURE__ */ jsxs(SheetPortal, { children: [
|
|
76
|
-
/* @__PURE__ */ jsx(SheetOverlay, {}),
|
|
77
|
-
/* @__PURE__ */ jsxs(
|
|
78
|
-
SheetPrimitive.Content,
|
|
79
|
-
__spreadProps(__spreadValues({
|
|
80
|
-
ref,
|
|
81
|
-
className: cn(sheetVariants({ side, attachment }), className)
|
|
82
|
-
}, props), {
|
|
83
|
-
children: [
|
|
84
|
-
children,
|
|
85
|
-
!hideClose && /* @__PURE__ */ jsxs(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", children: [
|
|
86
|
-
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
87
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
|
|
88
|
-
] })
|
|
89
|
-
]
|
|
90
|
-
})
|
|
91
|
-
)
|
|
92
|
-
] });
|
|
93
|
-
});
|
|
94
|
-
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
|
95
|
-
var SheetHeader = (_a) => {
|
|
96
|
-
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
97
|
-
return /* @__PURE__ */ jsx("div", __spreadValues({ className: cn("flex flex-col space-y-2 text-center sm:text-left", className) }, props));
|
|
98
|
-
};
|
|
99
|
-
SheetHeader.displayName = "SheetHeader";
|
|
100
|
-
var SheetFooter = (_a) => {
|
|
101
|
-
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
102
|
-
return /* @__PURE__ */ jsx(
|
|
103
|
-
"div",
|
|
104
|
-
__spreadValues({
|
|
105
|
-
className: cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)
|
|
106
|
-
}, props)
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
SheetFooter.displayName = "SheetFooter";
|
|
110
|
-
var SheetTitle = React.forwardRef((_a, ref) => {
|
|
111
|
-
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
112
|
-
return /* @__PURE__ */ jsx(
|
|
113
|
-
SheetPrimitive.Title,
|
|
114
|
-
__spreadValues({
|
|
115
|
-
ref,
|
|
116
|
-
className: cn("text-lg font-semibold text-foreground", className)
|
|
117
|
-
}, props)
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
|
121
|
-
var SheetDescription = React.forwardRef((_a, ref) => {
|
|
122
|
-
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
123
|
-
return /* @__PURE__ */ jsx(
|
|
124
|
-
SheetPrimitive.Description,
|
|
125
|
-
__spreadValues({
|
|
126
|
-
ref,
|
|
127
|
-
className: cn("text-sm text-muted-foreground", className)
|
|
128
|
-
}, props)
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
|
132
|
-
|
|
133
|
-
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger };
|
|
1
|
+
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger } from '../chunk-LAEVMNJN.js';
|
|
2
|
+
import '../chunk-FEK5XGZW.js';
|
|
3
|
+
import '../chunk-MMUBH76A.js';
|
|
@@ -4,11 +4,11 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
|
|
5
5
|
declare const spinnerVariants: (props?: ({
|
|
6
6
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
7
|
-
tone?: "
|
|
7
|
+
tone?: "default" | "inherit" | "primary" | "destructive" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Spinner: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
10
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
11
|
-
tone?: "
|
|
11
|
+
tone?: "default" | "inherit" | "primary" | "destructive" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
/** Visually-hidden label for screen readers. Defaults to "Loading". */
|
|
14
14
|
label?: string;
|
|
@@ -8,7 +8,22 @@ declare const TabsList: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsL
|
|
|
8
8
|
/** Make horizontal lists scrollable when overflowing. Default true for underline, false for pill. */
|
|
9
9
|
scrollable?: boolean;
|
|
10
10
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
11
|
-
declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> &
|
|
11
|
+
declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & {
|
|
12
|
+
/**
|
|
13
|
+
* A count after the label — "Today 3", "Queue 2" (today/03-d0). `null`
|
|
14
|
+
* means the count is unknown (still loading, or its read failed) and draws
|
|
15
|
+
* an em dash, "Queue —" (today/04-d3); `undefined` draws nothing.
|
|
16
|
+
* Tabular figures, so the strip does not shift as a count ticks.
|
|
17
|
+
*/
|
|
18
|
+
count?: number | null;
|
|
19
|
+
/**
|
|
20
|
+
* What a screen reader hears for the count, in place of the bare figure —
|
|
21
|
+
* "3 waiting", "count unavailable". Injected, like every label here: the
|
|
22
|
+
* design system holds no copy. Without it the figure is read as is and the
|
|
23
|
+
* dash is silent.
|
|
24
|
+
*/
|
|
25
|
+
countLabel?: string;
|
|
26
|
+
} & React.RefAttributes<HTMLButtonElement>>;
|
|
12
27
|
declare const TabsContent: React.ForwardRefExoticComponent<Omit<TabsPrimitive.TabsContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
13
28
|
|
|
14
29
|
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
package/dist/components/tabs.js
CHANGED
|
@@ -4,7 +4,7 @@ import { __objRest, __spreadValues, __spreadProps } from '../chunk-MMUBH76A.js';
|
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
6
6
|
import { motion } from 'framer-motion';
|
|
7
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
|
|
9
9
|
var TabsContext = React.createContext({
|
|
10
10
|
variant: "pill",
|
|
@@ -50,7 +50,7 @@ var TabsList = React.forwardRef((_a, ref) => {
|
|
|
50
50
|
});
|
|
51
51
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
52
52
|
var TabsTrigger = React.forwardRef((_a, ref) => {
|
|
53
|
-
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
53
|
+
var _b = _a, { className, children, count, countLabel } = _b, props = __objRest(_b, ["className", "children", "count", "countLabel"]);
|
|
54
54
|
const { variant, orientation, indicatorId } = React.useContext(TabsContext);
|
|
55
55
|
const triggerRef = React.useRef(null);
|
|
56
56
|
const [isActive, setIsActive] = React.useState(false);
|
|
@@ -111,7 +111,25 @@ var TabsTrigger = React.forwardRef((_a, ref) => {
|
|
|
111
111
|
)
|
|
112
112
|
}
|
|
113
113
|
),
|
|
114
|
-
/* @__PURE__ */
|
|
114
|
+
/* @__PURE__ */ jsxs("span", { className: "relative z-10 flex items-center gap-2", children: [
|
|
115
|
+
children,
|
|
116
|
+
count !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
117
|
+
" ",
|
|
118
|
+
/* @__PURE__ */ jsx(
|
|
119
|
+
"span",
|
|
120
|
+
{
|
|
121
|
+
"data-slot": "tabs-trigger-count",
|
|
122
|
+
"aria-hidden": countLabel !== void 0 || count === null ? true : void 0,
|
|
123
|
+
className: "tabular-nums",
|
|
124
|
+
children: count === null ? "\u2014" : count
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
countLabel !== void 0 ? /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
128
|
+
" ",
|
|
129
|
+
countLabel
|
|
130
|
+
] }) : null
|
|
131
|
+
] }) : null
|
|
132
|
+
] })
|
|
115
133
|
]
|
|
116
134
|
})
|
|
117
135
|
);
|
package/dist/components/tag.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const tagVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "primary" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Tag: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
|
-
variant?: "
|
|
10
|
+
variant?: "default" | "primary" | "success" | "warning" | "destructive" | "outline" | "quiet" | null | undefined;
|
|
11
11
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
onRemove?: () => void;
|
|
@@ -22,8 +22,8 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
22
22
|
* Weights are 400 / 600 / 700. 500 is not in the system.
|
|
23
23
|
*/
|
|
24
24
|
declare const textVariants: (props?: ({
|
|
25
|
-
variant?: "
|
|
26
|
-
color?: "
|
|
25
|
+
variant?: "body" | "small" | "display" | "h2" | "h3" | "label" | "caption" | "h1" | "h4" | "body-lg" | "body-sm" | "overline" | null | undefined;
|
|
26
|
+
color?: "default" | "inherit" | "primary" | "destructive" | "muted" | "primary-foreground" | null | undefined;
|
|
27
27
|
weight?: "bold" | "medium" | "normal" | "semibold" | null | undefined;
|
|
28
28
|
leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
|
|
29
29
|
tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { MenuItem } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
import { MenuItem } from '../chunk-ZUBBMLBD.js';
|
|
2
|
+
import '../chunk-KGXTBTCJ.js';
|
|
3
3
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
4
4
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
5
|
-
import '../chunk-
|
|
5
|
+
import '../chunk-FQAIYKAS.js';
|
|
6
6
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
7
7
|
import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
8
8
|
import { MapPin, Star, MessageSquareQuote } from 'lucide-react';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { ChevronDown, Users } from "lucide-react";
|
|
2
|
+
import { Armchair, Check, ChevronDown, CircleHelp, Clock, Users } from "lucide-react";
|
|
3
3
|
import { Avatar, AvatarFallback, AvatarGroup } from "./avatar";
|
|
4
4
|
import { Badge } from "./badge";
|
|
5
5
|
import { Button } from "./button";
|
|
@@ -22,6 +22,10 @@ const meta = {
|
|
|
22
22
|
"warning",
|
|
23
23
|
"brown",
|
|
24
24
|
"onPrimary",
|
|
25
|
+
"softSuccess",
|
|
26
|
+
"softWarning",
|
|
27
|
+
"softDestructive",
|
|
28
|
+
"softNeutral",
|
|
25
29
|
],
|
|
26
30
|
},
|
|
27
31
|
},
|
|
@@ -126,6 +130,33 @@ export const Assignee: Story = {
|
|
|
126
130
|
),
|
|
127
131
|
};
|
|
128
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Soft tones with an icon — a booking's live status in a table row
|
|
135
|
+
* (calendar/11-dp): a wash of the status colour, its ink on top, a 14px glyph
|
|
136
|
+
* so the status never rests on colour alone.
|
|
137
|
+
*/
|
|
138
|
+
export const SoftTones: Story = {
|
|
139
|
+
render: () => (
|
|
140
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
141
|
+
<Badge as="span" variant="softSuccess" icon={<Armchair />}>
|
|
142
|
+
Seated
|
|
143
|
+
</Badge>
|
|
144
|
+
<Badge as="span" variant="softDestructive" icon={<Clock />}>
|
|
145
|
+
25 min late
|
|
146
|
+
</Badge>
|
|
147
|
+
<Badge as="span" variant="softWarning" icon={<CircleHelp />}>
|
|
148
|
+
Needs an answer
|
|
149
|
+
</Badge>
|
|
150
|
+
<Badge as="span" variant="softNeutral" icon={<Check />}>
|
|
151
|
+
Confirmed
|
|
152
|
+
</Badge>
|
|
153
|
+
<Badge as="span" variant="softNeutral">
|
|
154
|
+
Now
|
|
155
|
+
</Badge>
|
|
156
|
+
</div>
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
|
|
129
160
|
export const AllVariants: Story = {
|
|
130
161
|
render: () => (
|
|
131
162
|
<div className="flex flex-wrap items-center gap-3">
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Armchair } from "lucide-react";
|
|
4
|
+
import { Badge } from "./badge";
|
|
5
|
+
|
|
6
|
+
describe("Badge", () => {
|
|
7
|
+
it("renders the icon before the label and hides it from assistive tech", () => {
|
|
8
|
+
render(
|
|
9
|
+
<Badge variant="softSuccess" icon={<Armchair data-testid="glyph" />}>
|
|
10
|
+
Seated
|
|
11
|
+
</Badge>
|
|
12
|
+
);
|
|
13
|
+
const badge = screen.getByText("Seated");
|
|
14
|
+
const icon = badge.querySelector('[data-slot="badge-icon"]');
|
|
15
|
+
expect(icon).toHaveAttribute("aria-hidden", "true");
|
|
16
|
+
expect(icon).toContainElement(screen.getByTestId("glyph"));
|
|
17
|
+
expect(badge.firstElementChild).toBe(icon);
|
|
18
|
+
expect(badge).toHaveClass("gap-1");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("renders no icon slot and no gap when there is no icon", () => {
|
|
22
|
+
render(<Badge variant="softNeutral">Confirmed</Badge>);
|
|
23
|
+
const badge = screen.getByText("Confirmed");
|
|
24
|
+
expect(badge.querySelector('[data-slot="badge-icon"]')).toBeNull();
|
|
25
|
+
expect(badge).not.toHaveClass("gap-1");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.each([
|
|
29
|
+
["softSuccess", "bg-success/15", "text-success-ink"],
|
|
30
|
+
["softWarning", "bg-warning/20", "text-warning-ink"],
|
|
31
|
+
["softDestructive", "bg-destructive/12", "text-destructive-ink"],
|
|
32
|
+
["softNeutral", "bg-surface-2", "text-ink-secondary"],
|
|
33
|
+
] as const)("%s paints a wash with the tone's ink token", (variant, wash, ink) => {
|
|
34
|
+
render(<Badge variant={variant}>Status</Badge>);
|
|
35
|
+
const badge = screen.getByText("Status");
|
|
36
|
+
expect(badge).toHaveClass(wash, ink);
|
|
37
|
+
// The solid fill tokens fall short of AA as text on their own wash.
|
|
38
|
+
expect(badge.className).not.toMatch(/\btext-(success|warning|destructive)\b(?!-)/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps an existing variant unchanged", () => {
|
|
42
|
+
render(<Badge variant="success">Solid</Badge>);
|
|
43
|
+
expect(screen.getByText("Solid")).toHaveClass("bg-success", "text-success-foreground");
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/components/badge.tsx
CHANGED
|
@@ -58,6 +58,22 @@ const badgeVariants = cva(
|
|
|
58
58
|
*/
|
|
59
59
|
assignee:
|
|
60
60
|
"rounded-full border-0 bg-surface-2 py-1 pl-1 pr-2 gap-1.5 text-[12px] leading-[1.2] font-semibold text-foreground",
|
|
61
|
+
/**
|
|
62
|
+
* Soft tones — a wash of the status colour with its INK on top, for a
|
|
63
|
+
* status that sits in a list and should not shout: the booking
|
|
64
|
+
* design's "Seated" (success), "25 min late" (destructive), "Needs an
|
|
65
|
+
* answer" (warning) and "Confirmed" (neutral) pills in a table row
|
|
66
|
+
* (calendar/11-dp, floor/14-w1, queue/02-q0).
|
|
67
|
+
*
|
|
68
|
+
* The ink tokens, not the solid ones: `--success` / `--warning` /
|
|
69
|
+
* `--destructive` are tuned as fills and fall short of AA as text on
|
|
70
|
+
* their own wash; `--*-ink` exists for exactly that. Usually paired
|
|
71
|
+
* with an `icon`, so the status does not rest on colour alone.
|
|
72
|
+
*/
|
|
73
|
+
softSuccess: "rounded-md border-0 bg-success/15 px-2 py-1 text-success-ink",
|
|
74
|
+
softWarning: "rounded-md border-0 bg-warning/20 px-2 py-1 text-warning-ink",
|
|
75
|
+
softDestructive: "rounded-md border-0 bg-destructive/12 px-2 py-1 text-destructive-ink",
|
|
76
|
+
softNeutral: "rounded-md border-0 bg-surface-2 px-2 py-1 text-ink-secondary",
|
|
61
77
|
},
|
|
62
78
|
/**
|
|
63
79
|
* `status` is the taller pill the booking design draws beside a
|
|
@@ -94,15 +110,37 @@ export interface BadgeProps
|
|
|
94
110
|
* clickable row.
|
|
95
111
|
*/
|
|
96
112
|
as?: "div" | "span";
|
|
113
|
+
/**
|
|
114
|
+
* A lucide glyph drawn at 14px before the label — the armchair on
|
|
115
|
+
* "Seated", the clock on "25 min late". Decorative: the label carries the
|
|
116
|
+
* meaning, so the slot is `aria-hidden`.
|
|
117
|
+
*/
|
|
118
|
+
icon?: React.ReactNode;
|
|
97
119
|
}
|
|
98
120
|
|
|
99
|
-
function Badge({
|
|
121
|
+
function Badge({
|
|
122
|
+
className,
|
|
123
|
+
variant,
|
|
124
|
+
size,
|
|
125
|
+
live,
|
|
126
|
+
as: Tag = "div",
|
|
127
|
+
icon,
|
|
128
|
+
children,
|
|
129
|
+
...props
|
|
130
|
+
}: BadgeProps) {
|
|
100
131
|
return (
|
|
101
132
|
<Tag
|
|
102
133
|
data-slot="badge"
|
|
103
|
-
className={cn(badgeVariants({ variant, size, live }), className)}
|
|
134
|
+
className={cn(badgeVariants({ variant, size, live }), icon ? "gap-1" : undefined, className)}
|
|
104
135
|
{...props}
|
|
105
|
-
|
|
136
|
+
>
|
|
137
|
+
{icon ? (
|
|
138
|
+
<span data-slot="badge-icon" aria-hidden className="inline-flex shrink-0 [&>svg]:size-3.5">
|
|
139
|
+
{icon}
|
|
140
|
+
</span>
|
|
141
|
+
) : null}
|
|
142
|
+
{children}
|
|
143
|
+
</Tag>
|
|
106
144
|
);
|
|
107
145
|
}
|
|
108
146
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { TriangleAlert } from "lucide-react";
|
|
3
3
|
import { Banner } from "./banner";
|
|
4
|
+
import { Button } from "./button";
|
|
4
5
|
|
|
5
6
|
const meta: Meta<typeof Banner> = {
|
|
6
7
|
title: "Booking/Banner",
|
|
@@ -30,6 +31,37 @@ export const Warning: Story = {
|
|
|
30
31
|
},
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
/** today/04-d3: a read that failed, on a page that is otherwise live. */
|
|
35
|
+
export const Destructive: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
tone: "destructive",
|
|
38
|
+
children: (
|
|
39
|
+
<>
|
|
40
|
+
<b className="font-bold">The queue didn't load.</b> This is on our side; everything
|
|
41
|
+
else is live.
|
|
42
|
+
</>
|
|
43
|
+
),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** reports/03-p9: the failure carries its own way out. */
|
|
48
|
+
export const DestructiveWithAction: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
tone: "destructive",
|
|
51
|
+
children: (
|
|
52
|
+
<>
|
|
53
|
+
<b className="font-bold">Walk-ins didn't load.</b> Covers and walk-in figures read —
|
|
54
|
+
until they do.
|
|
55
|
+
</>
|
|
56
|
+
),
|
|
57
|
+
action: (
|
|
58
|
+
<Button variant="link" className="h-auto p-0 font-bold text-destructive-ink">
|
|
59
|
+
Retry
|
|
60
|
+
</Button>
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
33
65
|
/** B0c: a venue that takes no bookings. */
|
|
34
66
|
export const Info: Story = {
|
|
35
67
|
args: { tone: "info", children: "This venue does not take bookings. Walk-ins only." },
|