@bearmenu/ui 0.8.38 → 0.8.39
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-XLHMF6F5.js → chunk-NF5UMLSL.js} +17 -6
- package/dist/components/drawer.d.ts +8 -1
- package/dist/components/drawer.js +1 -1
- package/dist/components/empty-state.d.ts +26 -1
- package/dist/components/empty-state.js +41 -17
- package/dist/components/responsive-overlay.js +16 -20
- package/package.json +1 -1
- package/src/components/drawer.test.tsx +55 -0
- package/src/components/drawer.tsx +33 -11
- package/src/components/empty-state.stories.tsx +14 -0
- package/src/components/empty-state.test.tsx +60 -0
- package/src/components/empty-state.tsx +66 -21
- package/src/components/floor-tokens.stories.tsx +223 -0
- package/src/components/responsive-overlay.test.tsx +40 -0
- package/src/components/responsive-overlay.tsx +13 -10
- package/src/globals.css +161 -0
|
@@ -4,20 +4,30 @@ import * as React from 'react';
|
|
|
4
4
|
import { Drawer as Drawer$1 } from 'vaul';
|
|
5
5
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var DrawerContext = React.createContext({
|
|
8
|
+
direction: "bottom",
|
|
9
|
+
modal: true
|
|
10
|
+
});
|
|
8
11
|
var Drawer = (_a) => {
|
|
9
12
|
var _b = _a, {
|
|
10
13
|
shouldScaleBackground = false,
|
|
11
|
-
direction = "bottom"
|
|
14
|
+
direction = "bottom",
|
|
15
|
+
modal = true,
|
|
16
|
+
autoFocus = true
|
|
12
17
|
} = _b, props = __objRest(_b, [
|
|
13
18
|
"shouldScaleBackground",
|
|
14
|
-
"direction"
|
|
19
|
+
"direction",
|
|
20
|
+
"modal",
|
|
21
|
+
"autoFocus"
|
|
15
22
|
]);
|
|
16
|
-
|
|
23
|
+
const context = React.useMemo(() => ({ direction, modal }), [direction, modal]);
|
|
24
|
+
return /* @__PURE__ */ jsx(DrawerContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
|
|
17
25
|
Drawer$1.Root,
|
|
18
26
|
__spreadValues({
|
|
19
27
|
shouldScaleBackground,
|
|
20
|
-
direction
|
|
28
|
+
direction,
|
|
29
|
+
modal,
|
|
30
|
+
autoFocus
|
|
21
31
|
}, props)
|
|
22
32
|
) });
|
|
23
33
|
};
|
|
@@ -39,7 +49,7 @@ var DrawerOverlay = React.forwardRef((_a, ref) => {
|
|
|
39
49
|
DrawerOverlay.displayName = Drawer$1.Overlay.displayName;
|
|
40
50
|
var DrawerContent = React.forwardRef((_a, ref) => {
|
|
41
51
|
var _b = _a, { className, variant = "flush", showHandle, overlayClassName, children } = _b, props = __objRest(_b, ["className", "variant", "showHandle", "overlayClassName", "children"]);
|
|
42
|
-
const direction = React.useContext(
|
|
52
|
+
const { direction, modal } = React.useContext(DrawerContext);
|
|
43
53
|
const isVertical = direction === "top" || direction === "bottom";
|
|
44
54
|
const handleEnabled = showHandle != null ? showHandle : direction === "bottom";
|
|
45
55
|
const positionClasses = direction === "bottom" ? "inset-x-0 bottom-0 flex-col rounded-t-2xl border-t [padding-bottom:max(env(safe-area-inset-bottom),0px)]" : direction === "top" ? "inset-x-0 top-0 flex-col rounded-b-2xl border-b [padding-top:max(env(safe-area-inset-top),0px)]" : direction === "left" ? "inset-y-0 left-0 h-full w-3/4 max-w-sm flex-col rounded-r-2xl border-r [padding-left:max(env(safe-area-inset-left),0px)]" : "inset-y-0 right-0 h-full w-3/4 max-w-sm flex-col rounded-l-2xl border-l [padding-right:max(env(safe-area-inset-right),0px)]";
|
|
@@ -50,6 +60,7 @@ var DrawerContent = React.forwardRef((_a, ref) => {
|
|
|
50
60
|
__spreadProps(__spreadValues({
|
|
51
61
|
ref,
|
|
52
62
|
"data-slot": "drawer-content",
|
|
63
|
+
"aria-modal": modal ? true : void 0,
|
|
53
64
|
className: cn(
|
|
54
65
|
"fixed z-[1000] flex border-border bg-card shadow-[var(--shadow-overlay)]",
|
|
55
66
|
positionClasses,
|
|
@@ -4,8 +4,15 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
|
4
4
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
5
5
|
import * as React from 'react';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* `autoFocus` defaults to `true` here, where vaul defaults it to `false`.
|
|
9
|
+
* vaul's default cancels Radix's open auto-focus, so focus never enters the
|
|
10
|
+
* sheet, Radix's FocusScope never engages, and Tab walks the page behind a
|
|
11
|
+
* modal drawer. A caller that must not move focus on open passes
|
|
12
|
+
* `autoFocus={false}`.
|
|
13
|
+
*/
|
|
7
14
|
declare const Drawer: {
|
|
8
|
-
({ shouldScaleBackground, direction, ...props }: React.ComponentProps<typeof Drawer$1.Root>): react_jsx_runtime.JSX.Element;
|
|
15
|
+
({ shouldScaleBackground, direction, modal, autoFocus, ...props }: React.ComponentProps<typeof Drawer$1.Root>): react_jsx_runtime.JSX.Element;
|
|
9
16
|
displayName: string;
|
|
10
17
|
};
|
|
11
18
|
declare const DrawerTrigger: React.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger } from '../chunk-
|
|
1
|
+
export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger } from '../chunk-NF5UMLSL.js';
|
|
2
2
|
import '../chunk-FEK5XGZW.js';
|
|
3
3
|
import '../chunk-MMUBH76A.js';
|
|
@@ -4,6 +4,7 @@ type EmptyStateAction = {
|
|
|
4
4
|
label: string;
|
|
5
5
|
onClick: () => void;
|
|
6
6
|
};
|
|
7
|
+
type EmptyStateVariant = "default" | "search" | "error";
|
|
7
8
|
interface EmptyStateProps {
|
|
8
9
|
title: string;
|
|
9
10
|
description: string;
|
|
@@ -11,10 +12,34 @@ interface EmptyStateProps {
|
|
|
11
12
|
illustration?: React.ReactNode;
|
|
12
13
|
action?: EmptyStateAction;
|
|
13
14
|
secondaryAction?: EmptyStateAction;
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The primary action's style. Defaults by variant: `outline` for `error`
|
|
17
|
+
* (try again) and `search` (clear filters) — a recovery step, not the page's
|
|
18
|
+
* main call to action — and a filled `default` for the plain empty state,
|
|
19
|
+
* where the action is usually "create the first one".
|
|
20
|
+
*/
|
|
21
|
+
actionVariant?: "default" | "outline";
|
|
22
|
+
variant?: EmptyStateVariant;
|
|
15
23
|
size?: "sm" | "md" | "lg";
|
|
24
|
+
/**
|
|
25
|
+
* `card` draws the booking pages' whole-body state card (reports/02-p4,
|
|
26
|
+
* calendar/02-c2): a 16px-radius card surface with the resting card shadow.
|
|
27
|
+
* `none` (the default) leaves the surface to the container it sits in.
|
|
28
|
+
*/
|
|
29
|
+
surface?: "none" | "card";
|
|
16
30
|
className?: string;
|
|
17
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* EmptyState — an icon tile, a title, one line of body and an optional action,
|
|
34
|
+
* spaced as the booking frames draw them (reports/02-p4, calendar/02-c2): a
|
|
35
|
+
* 64px tile (72px from `sm`) with a 28px glyph, 16px to the title, 12px from
|
|
36
|
+
* title to body, 16px from body to the action. The action is full-width on a
|
|
37
|
+
* phone and hugs its label from `sm`.
|
|
38
|
+
*
|
|
39
|
+
* The error variant says "error" with its glyph and its tile's wash, not by
|
|
40
|
+
* painting the title and body red: red body copy is both harder to read and
|
|
41
|
+
* louder than a load failure the user can retry.
|
|
42
|
+
*/
|
|
18
43
|
declare const EmptyState: React.ForwardRefExoticComponent<EmptyStateProps & React.RefAttributes<HTMLDivElement>>;
|
|
19
44
|
declare const SearchEmptyState: React.ForwardRefExoticComponent<Omit<EmptyStateProps, "icon" | "variant"> & React.RefAttributes<HTMLDivElement>>;
|
|
20
45
|
declare const ErrorEmptyState: React.ForwardRefExoticComponent<Omit<EmptyStateProps, "icon" | "variant"> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -5,7 +5,7 @@ import { Text } from '../chunk-ZLMRVBD7.js';
|
|
|
5
5
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
6
6
|
import { __objRest, __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
7
7
|
import * as React from 'react';
|
|
8
|
-
import {
|
|
8
|
+
import { AlertCircle, Search, FileX } from 'lucide-react';
|
|
9
9
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
10
10
|
|
|
11
11
|
var sizeStyles = {
|
|
@@ -13,6 +13,16 @@ var sizeStyles = {
|
|
|
13
13
|
md: "py-12 px-6",
|
|
14
14
|
lg: "py-16 px-8"
|
|
15
15
|
};
|
|
16
|
+
var DEFAULT_ACTION_VARIANT = {
|
|
17
|
+
default: "default",
|
|
18
|
+
search: "outline",
|
|
19
|
+
error: "outline"
|
|
20
|
+
};
|
|
21
|
+
var DEFAULT_ICON = {
|
|
22
|
+
default: /* @__PURE__ */ jsx(FileX, { className: "text-muted-foreground", "aria-hidden": true }),
|
|
23
|
+
search: /* @__PURE__ */ jsx(Search, { className: "text-muted-foreground", "aria-hidden": true }),
|
|
24
|
+
error: /* @__PURE__ */ jsx(AlertCircle, { className: "text-destructive", "aria-hidden": true })
|
|
25
|
+
};
|
|
16
26
|
var EmptyState = React.forwardRef(
|
|
17
27
|
(_a, ref) => {
|
|
18
28
|
var _b = _a, {
|
|
@@ -22,8 +32,10 @@ var EmptyState = React.forwardRef(
|
|
|
22
32
|
illustration,
|
|
23
33
|
action,
|
|
24
34
|
secondaryAction,
|
|
35
|
+
actionVariant,
|
|
25
36
|
variant = "default",
|
|
26
37
|
size = "md",
|
|
38
|
+
surface = "none",
|
|
27
39
|
className
|
|
28
40
|
} = _b, props = __objRest(_b, [
|
|
29
41
|
"title",
|
|
@@ -32,21 +44,12 @@ var EmptyState = React.forwardRef(
|
|
|
32
44
|
"illustration",
|
|
33
45
|
"action",
|
|
34
46
|
"secondaryAction",
|
|
47
|
+
"actionVariant",
|
|
35
48
|
"variant",
|
|
36
49
|
"size",
|
|
50
|
+
"surface",
|
|
37
51
|
"className"
|
|
38
52
|
]);
|
|
39
|
-
const getDefaultIcon = () => {
|
|
40
|
-
switch (variant) {
|
|
41
|
-
case "search":
|
|
42
|
-
return /* @__PURE__ */ jsx(Search, { className: "h-12 w-12 text-muted-foreground/50" });
|
|
43
|
-
case "error":
|
|
44
|
-
return /* @__PURE__ */ jsx(AlertCircle, { className: "h-12 w-12 text-destructive/50" });
|
|
45
|
-
default:
|
|
46
|
-
return /* @__PURE__ */ jsx(FileX, { className: "h-12 w-12 text-muted-foreground/50" });
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const textColor = variant === "error" ? "destructive" : "muted";
|
|
50
53
|
return /* @__PURE__ */ jsxs(
|
|
51
54
|
"div",
|
|
52
55
|
__spreadProps(__spreadValues({
|
|
@@ -55,15 +58,36 @@ var EmptyState = React.forwardRef(
|
|
|
55
58
|
className: cn(
|
|
56
59
|
"flex flex-col items-center justify-center text-center",
|
|
57
60
|
sizeStyles[size],
|
|
61
|
+
surface === "card" && "rounded-2xl border border-border bg-card shadow-[var(--shadow-card)]",
|
|
58
62
|
className
|
|
59
63
|
)
|
|
60
64
|
}, props), {
|
|
61
65
|
children: [
|
|
62
|
-
illustration ? /* @__PURE__ */ jsx("div", { className: "mb-6", children: illustration }) : /* @__PURE__ */ jsx(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
illustration ? /* @__PURE__ */ jsx("div", { className: "mb-6", children: illustration }) : /* @__PURE__ */ jsx(
|
|
67
|
+
"span",
|
|
68
|
+
{
|
|
69
|
+
"data-slot": "empty-state-icon",
|
|
70
|
+
className: cn(
|
|
71
|
+
// The glyph size is the tile's, whatever size the caller's icon
|
|
72
|
+
// node asked for — a 40px icon in a 64px tile has no margin.
|
|
73
|
+
"mb-4 flex size-16 shrink-0 items-center justify-center rounded-2xl border border-border shadow-sm sm:size-18 [&_svg]:size-7",
|
|
74
|
+
variant === "error" ? "bg-destructive/5" : "bg-card"
|
|
75
|
+
),
|
|
76
|
+
children: icon != null ? icon : DEFAULT_ICON[variant]
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsx(Text, { as: "h3", weight: "semibold", className: "text-lg", children: title }),
|
|
80
|
+
description ? /* @__PURE__ */ jsx(Text, { as: "p", color: "muted", className: "mt-3 max-w-md text-sm", children: description }) : null,
|
|
81
|
+
(action || secondaryAction) && /* @__PURE__ */ jsxs("div", { className: "mt-4 flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center", children: [
|
|
82
|
+
action && /* @__PURE__ */ jsx(
|
|
83
|
+
Button,
|
|
84
|
+
{
|
|
85
|
+
onClick: action.onClick,
|
|
86
|
+
variant: actionVariant != null ? actionVariant : DEFAULT_ACTION_VARIANT[variant],
|
|
87
|
+
className: "glass-hover",
|
|
88
|
+
children: action.label
|
|
89
|
+
}
|
|
90
|
+
),
|
|
67
91
|
secondaryAction && /* @__PURE__ */ jsx(Button, { onClick: secondaryAction.onClick, variant: "outline", className: "glass-hover", children: secondaryAction.label })
|
|
68
92
|
] })
|
|
69
93
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMediaQuery, DESKTOP_MEDIA_QUERY } from '../chunk-OPVKL4VS.js';
|
|
2
2
|
import { Sheet, SheetContent, SheetTitle, SheetDescription } from '../chunk-LAEVMNJN.js';
|
|
3
|
-
import { Drawer, DrawerContent, DrawerTitle, DrawerDescription } from '../chunk-
|
|
3
|
+
import { Drawer, DrawerContent, DrawerTitle, DrawerDescription } from '../chunk-NF5UMLSL.js';
|
|
4
4
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '../chunk-IQIJCMG2.js';
|
|
5
5
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
6
6
|
import { __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
@@ -59,15 +59,14 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
59
59
|
}, ref) => {
|
|
60
60
|
const isWide = useMediaQuery(wideFrom === "lg" ? DESKTOP_MEDIA_QUERY : MD_MEDIA_QUERY);
|
|
61
61
|
const container = isWide ? desktop : "drawer";
|
|
62
|
-
const titleId = React.useId();
|
|
63
|
-
const subtitleId = React.useId();
|
|
64
62
|
useFocusReturn(open);
|
|
63
|
+
const describedBy = subtitle ? {} : { "aria-describedby": void 0 };
|
|
65
64
|
const Title = CONTAINER_TITLE[container];
|
|
66
65
|
const Description = CONTAINER_DESCRIPTION[container];
|
|
67
66
|
const header = /* @__PURE__ */ jsxs("div", { "data-slot": "responsive-overlay-header", className: "flex items-start gap-3 px-5 pt-5", children: [
|
|
68
67
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
69
|
-
/* @__PURE__ */ jsx(Title, {
|
|
70
|
-
subtitle ? /* @__PURE__ */ jsx(Description, {
|
|
68
|
+
/* @__PURE__ */ jsx(Title, { className: "text-[19px] font-bold leading-tight", children: title }),
|
|
69
|
+
subtitle ? /* @__PURE__ */ jsx(Description, { className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : null
|
|
71
70
|
] }),
|
|
72
71
|
/* @__PURE__ */ jsxs(
|
|
73
72
|
"button",
|
|
@@ -102,17 +101,16 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
102
101
|
if (container === "sheet") {
|
|
103
102
|
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, modal, children: /* @__PURE__ */ jsxs(
|
|
104
103
|
SheetContent,
|
|
105
|
-
__spreadProps(__spreadValues({
|
|
104
|
+
__spreadProps(__spreadValues(__spreadProps(__spreadValues({
|
|
106
105
|
ref,
|
|
107
106
|
side: "right",
|
|
108
107
|
attachment: "inset",
|
|
109
108
|
hideClose: true,
|
|
110
|
-
overlayClassName: "bg-scrim-overlay backdrop-blur-none"
|
|
111
|
-
|
|
112
|
-
"aria-describedby": subtitle ? subtitleId : void 0,
|
|
109
|
+
overlayClassName: "bg-scrim-overlay backdrop-blur-none"
|
|
110
|
+
}, describedBy), {
|
|
113
111
|
"data-slot": "responsive-overlay-sheet",
|
|
114
112
|
"data-modal": modal ? "true" : "false"
|
|
115
|
-
}, modal ? {} : { onInteractOutside: (event) => event.preventDefault() }), {
|
|
113
|
+
}), modal ? {} : { onInteractOutside: (event) => event.preventDefault() }), {
|
|
116
114
|
className: cn(
|
|
117
115
|
"flex w-[420px] max-w-[calc(100vw-1.5rem)] flex-col gap-0 bg-card p-0",
|
|
118
116
|
className
|
|
@@ -128,13 +126,12 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
128
126
|
if (container === "dialog") {
|
|
129
127
|
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
130
128
|
DialogContent,
|
|
131
|
-
{
|
|
129
|
+
__spreadProps(__spreadValues({
|
|
132
130
|
ref,
|
|
133
131
|
size,
|
|
134
132
|
showCloseButton: false,
|
|
135
|
-
overlayClassName: "bg-scrim-overlay backdrop-blur-none"
|
|
136
|
-
|
|
137
|
-
"aria-describedby": subtitle ? subtitleId : void 0,
|
|
133
|
+
overlayClassName: "bg-scrim-overlay backdrop-blur-none"
|
|
134
|
+
}, describedBy), {
|
|
138
135
|
className: cn(
|
|
139
136
|
// The dialog's own `grid gap-4 p-6` would put air between the
|
|
140
137
|
// header, the body and the band the frames draw flush.
|
|
@@ -146,23 +143,22 @@ var ResponsiveOverlay = React.forwardRef(
|
|
|
146
143
|
body,
|
|
147
144
|
foot
|
|
148
145
|
]
|
|
149
|
-
}
|
|
146
|
+
})
|
|
150
147
|
) });
|
|
151
148
|
}
|
|
152
149
|
return /* @__PURE__ */ jsx(Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
153
150
|
DrawerContent,
|
|
154
|
-
{
|
|
151
|
+
__spreadProps(__spreadValues({
|
|
155
152
|
ref,
|
|
156
|
-
overlayClassName: "bg-scrim-overlay"
|
|
157
|
-
|
|
158
|
-
"aria-describedby": subtitle ? subtitleId : void 0,
|
|
153
|
+
overlayClassName: "bg-scrim-overlay"
|
|
154
|
+
}, describedBy), {
|
|
159
155
|
className: cn("flex max-h-[85vh] flex-col p-0", className),
|
|
160
156
|
children: [
|
|
161
157
|
header,
|
|
162
158
|
body,
|
|
163
159
|
foot
|
|
164
160
|
]
|
|
165
|
-
}
|
|
161
|
+
})
|
|
166
162
|
) });
|
|
167
163
|
}
|
|
168
164
|
);
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Drawer, DrawerContent, DrawerDescription, DrawerTitle } from "./drawer";
|
|
5
|
+
|
|
6
|
+
function Harness(props: { modal?: boolean; autoFocus?: boolean }) {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<button type="button">Behind</button>
|
|
10
|
+
<Drawer open onOpenChange={() => {}} {...props}>
|
|
11
|
+
<DrawerContent>
|
|
12
|
+
<DrawerTitle>Pick a range</DrawerTitle>
|
|
13
|
+
<DrawerDescription>Reports</DrawerDescription>
|
|
14
|
+
<button type="button">Inside</button>
|
|
15
|
+
</DrawerContent>
|
|
16
|
+
</Drawer>
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Drawer", () => {
|
|
22
|
+
it("moves focus into the sheet on open", async () => {
|
|
23
|
+
render(<Harness />);
|
|
24
|
+
const dialog = screen.getByRole("dialog");
|
|
25
|
+
await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("keeps Tab inside a modal sheet", async () => {
|
|
29
|
+
render(<Harness />);
|
|
30
|
+
const dialog = screen.getByRole("dialog");
|
|
31
|
+
await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true));
|
|
32
|
+
for (let i = 0; i < 4; i += 1) {
|
|
33
|
+
await userEvent.tab();
|
|
34
|
+
expect(dialog.contains(document.activeElement)).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("marks a modal sheet aria-modal", () => {
|
|
39
|
+
render(<Harness />);
|
|
40
|
+
expect(screen.getByRole("dialog")).toHaveAttribute("aria-modal", "true");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not claim aria-modal when the sheet is not modal", () => {
|
|
44
|
+
render(<Harness modal={false} />);
|
|
45
|
+
expect(screen.getByRole("dialog")).not.toHaveAttribute("aria-modal");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("leaves focus where it was with autoFocus={false}", async () => {
|
|
49
|
+
render(<Harness autoFocus={false} />);
|
|
50
|
+
const dialog = screen.getByRole("dialog");
|
|
51
|
+
// Give an open auto-focus the frame it would have taken.
|
|
52
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
53
|
+
expect(dialog.contains(document.activeElement)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -6,21 +6,40 @@ import { cn } from "../lib/utils";
|
|
|
6
6
|
|
|
7
7
|
type DrawerDirection = "top" | "bottom" | "left" | "right";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
type DrawerContextValue = { direction: DrawerDirection; modal: boolean };
|
|
10
10
|
|
|
11
|
+
const DrawerContext = React.createContext<DrawerContextValue>({
|
|
12
|
+
direction: "bottom",
|
|
13
|
+
modal: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `autoFocus` defaults to `true` here, where vaul defaults it to `false`.
|
|
18
|
+
* vaul's default cancels Radix's open auto-focus, so focus never enters the
|
|
19
|
+
* sheet, Radix's FocusScope never engages, and Tab walks the page behind a
|
|
20
|
+
* modal drawer. A caller that must not move focus on open passes
|
|
21
|
+
* `autoFocus={false}`.
|
|
22
|
+
*/
|
|
11
23
|
const Drawer = ({
|
|
12
24
|
shouldScaleBackground = false,
|
|
13
25
|
direction = "bottom",
|
|
26
|
+
modal = true,
|
|
27
|
+
autoFocus = true,
|
|
14
28
|
...props
|
|
15
|
-
}: React.ComponentProps<typeof DrawerPrimitive.Root>) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => {
|
|
30
|
+
const context = React.useMemo(() => ({ direction, modal }), [direction, modal]);
|
|
31
|
+
return (
|
|
32
|
+
<DrawerContext.Provider value={context}>
|
|
33
|
+
<DrawerPrimitive.Root
|
|
34
|
+
shouldScaleBackground={shouldScaleBackground}
|
|
35
|
+
direction={direction}
|
|
36
|
+
modal={modal}
|
|
37
|
+
autoFocus={autoFocus}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</DrawerContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
24
43
|
Drawer.displayName = "Drawer";
|
|
25
44
|
|
|
26
45
|
const DrawerTrigger = DrawerPrimitive.Trigger;
|
|
@@ -55,7 +74,7 @@ const DrawerContent = React.forwardRef<
|
|
|
55
74
|
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
56
75
|
DrawerContentProps
|
|
57
76
|
>(({ className, variant = "flush", showHandle, overlayClassName, children, ...props }, ref) => {
|
|
58
|
-
const direction = React.useContext(
|
|
77
|
+
const { direction, modal } = React.useContext(DrawerContext);
|
|
59
78
|
const isVertical = direction === "top" || direction === "bottom";
|
|
60
79
|
const handleEnabled = showHandle ?? (direction === "bottom");
|
|
61
80
|
|
|
@@ -74,6 +93,9 @@ const DrawerContent = React.forwardRef<
|
|
|
74
93
|
<DrawerPrimitive.Content
|
|
75
94
|
ref={ref}
|
|
76
95
|
data-slot="drawer-content"
|
|
96
|
+
// Radix hides the page from assistive tech but never says so on the
|
|
97
|
+
// dialog itself; `aria-modal` is what tells a screen reader to stay in.
|
|
98
|
+
aria-modal={modal ? true : undefined}
|
|
77
99
|
className={cn(
|
|
78
100
|
"fixed z-[1000] flex border-border bg-card shadow-[var(--shadow-overlay)]",
|
|
79
101
|
positionClasses,
|
|
@@ -49,6 +49,20 @@ export const Error: Story = {
|
|
|
49
49
|
),
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/** The booking pages' whole-body failure (reports/02-p4, calendar/02-c2). */
|
|
53
|
+
export const ErrorCard: Story = {
|
|
54
|
+
render: () => (
|
|
55
|
+
<div className="bg-background p-4">
|
|
56
|
+
<ErrorEmptyState
|
|
57
|
+
surface="card"
|
|
58
|
+
title="Couldn't load this report"
|
|
59
|
+
description="This is on our side, not an empty week. Try again in a moment."
|
|
60
|
+
action={{ label: "Try again", onClick: () => {} }}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
52
66
|
export const Small: Story = {
|
|
53
67
|
args: {
|
|
54
68
|
title: "No data",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { EmptyState, ErrorEmptyState, SearchEmptyState } from "./empty-state";
|
|
5
|
+
|
|
6
|
+
const retry = { label: "Try again", onClick: () => {} };
|
|
7
|
+
|
|
8
|
+
describe("EmptyState", () => {
|
|
9
|
+
it("draws a retry on an error as an outline button", () => {
|
|
10
|
+
render(<ErrorEmptyState title="Couldn't load this report" description="On our side." action={retry} />);
|
|
11
|
+
expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-border", "bg-background");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("draws a clear-filters on a search as an outline button", () => {
|
|
15
|
+
render(<SearchEmptyState title="No results" description="Try another name." action={retry} />);
|
|
16
|
+
expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-border", "bg-background");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("keeps the plain empty state's create action filled", () => {
|
|
20
|
+
render(<EmptyState title="No items yet" description="Add the first one." action={{ label: "Add item", onClick: () => {} }} />);
|
|
21
|
+
expect(screen.getByRole("button", { name: "Add item" })).toHaveClass("bg-primary");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("lets the caller choose the action style", () => {
|
|
25
|
+
render(<EmptyState title="Nothing" description="Here." action={retry} actionVariant="outline" />);
|
|
26
|
+
expect(screen.getByRole("button", { name: "Try again" })).toHaveClass("border-border", "bg-background");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("calls the action", async () => {
|
|
30
|
+
const onClick = vi.fn();
|
|
31
|
+
render(<ErrorEmptyState title="Failed" description="Retry." action={{ label: "Try again", onClick }} />);
|
|
32
|
+
await userEvent.click(screen.getByRole("button", { name: "Try again" }));
|
|
33
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not paint an error's title and body red", () => {
|
|
37
|
+
render(<ErrorEmptyState title="Failed" description="On our side." />);
|
|
38
|
+
expect(screen.getByRole("heading", { name: "Failed" }).className).not.toMatch(/destructive/);
|
|
39
|
+
expect(screen.getByText("On our side.").className).not.toMatch(/destructive/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("sets the icon in a tile, whatever the icon", () => {
|
|
43
|
+
render(<EmptyState title="Empty" description="" icon={<svg data-testid="glyph" className="h-10 w-10" />} />);
|
|
44
|
+
const tile = document.querySelector('[data-slot="empty-state-icon"]');
|
|
45
|
+
expect(tile).toContainElement(screen.getByTestId("glyph"));
|
|
46
|
+
expect(tile).toHaveClass("size-16", "sm:size-18", "[&_svg]:size-7");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders no empty paragraph when there is no body", () => {
|
|
50
|
+
render(<EmptyState title="Empty" description="" />);
|
|
51
|
+
expect(screen.getByRole("status").querySelector("p")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("draws the card surface on request only", () => {
|
|
55
|
+
const { rerender } = render(<EmptyState title="Empty" description="Nothing." />);
|
|
56
|
+
expect(screen.getByRole("status")).not.toHaveClass("bg-card");
|
|
57
|
+
rerender(<EmptyState title="Empty" description="Nothing." surface="card" />);
|
|
58
|
+
expect(screen.getByRole("status")).toHaveClass("rounded-2xl", "bg-card", "border");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -11,6 +11,8 @@ type EmptyStateAction = {
|
|
|
11
11
|
onClick: () => void;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
type EmptyStateVariant = "default" | "search" | "error";
|
|
15
|
+
|
|
14
16
|
interface EmptyStateProps {
|
|
15
17
|
title: string;
|
|
16
18
|
description: string;
|
|
@@ -18,8 +20,21 @@ interface EmptyStateProps {
|
|
|
18
20
|
illustration?: React.ReactNode;
|
|
19
21
|
action?: EmptyStateAction;
|
|
20
22
|
secondaryAction?: EmptyStateAction;
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The primary action's style. Defaults by variant: `outline` for `error`
|
|
25
|
+
* (try again) and `search` (clear filters) — a recovery step, not the page's
|
|
26
|
+
* main call to action — and a filled `default` for the plain empty state,
|
|
27
|
+
* where the action is usually "create the first one".
|
|
28
|
+
*/
|
|
29
|
+
actionVariant?: "default" | "outline";
|
|
30
|
+
variant?: EmptyStateVariant;
|
|
22
31
|
size?: "sm" | "md" | "lg";
|
|
32
|
+
/**
|
|
33
|
+
* `card` draws the booking pages' whole-body state card (reports/02-p4,
|
|
34
|
+
* calendar/02-c2): a 16px-radius card surface with the resting card shadow.
|
|
35
|
+
* `none` (the default) leaves the surface to the container it sits in.
|
|
36
|
+
*/
|
|
37
|
+
surface?: "none" | "card";
|
|
23
38
|
className?: string;
|
|
24
39
|
}
|
|
25
40
|
|
|
@@ -29,6 +44,29 @@ const sizeStyles = {
|
|
|
29
44
|
lg: "py-16 px-8",
|
|
30
45
|
} as const;
|
|
31
46
|
|
|
47
|
+
const DEFAULT_ACTION_VARIANT: Record<EmptyStateVariant, "default" | "outline"> = {
|
|
48
|
+
default: "default",
|
|
49
|
+
search: "outline",
|
|
50
|
+
error: "outline",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DEFAULT_ICON: Record<EmptyStateVariant, React.ReactNode> = {
|
|
54
|
+
default: <FileX className="text-muted-foreground" aria-hidden />,
|
|
55
|
+
search: <Search className="text-muted-foreground" aria-hidden />,
|
|
56
|
+
error: <AlertCircle className="text-destructive" aria-hidden />,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* EmptyState — an icon tile, a title, one line of body and an optional action,
|
|
61
|
+
* spaced as the booking frames draw them (reports/02-p4, calendar/02-c2): a
|
|
62
|
+
* 64px tile (72px from `sm`) with a 28px glyph, 16px to the title, 12px from
|
|
63
|
+
* title to body, 16px from body to the action. The action is full-width on a
|
|
64
|
+
* phone and hugs its label from `sm`.
|
|
65
|
+
*
|
|
66
|
+
* The error variant says "error" with its glyph and its tile's wash, not by
|
|
67
|
+
* painting the title and body red: red body copy is both harder to read and
|
|
68
|
+
* louder than a load failure the user can retry.
|
|
69
|
+
*/
|
|
32
70
|
const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
33
71
|
(
|
|
34
72
|
{
|
|
@@ -38,26 +76,15 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
38
76
|
illustration,
|
|
39
77
|
action,
|
|
40
78
|
secondaryAction,
|
|
79
|
+
actionVariant,
|
|
41
80
|
variant = "default",
|
|
42
81
|
size = "md",
|
|
82
|
+
surface = "none",
|
|
43
83
|
className,
|
|
44
84
|
...props
|
|
45
85
|
},
|
|
46
86
|
ref
|
|
47
87
|
) => {
|
|
48
|
-
const getDefaultIcon = () => {
|
|
49
|
-
switch (variant) {
|
|
50
|
-
case "search":
|
|
51
|
-
return <Search className="h-12 w-12 text-muted-foreground/50" />;
|
|
52
|
-
case "error":
|
|
53
|
-
return <AlertCircle className="h-12 w-12 text-destructive/50" />;
|
|
54
|
-
default:
|
|
55
|
-
return <FileX className="h-12 w-12 text-muted-foreground/50" />;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const textColor = variant === "error" ? "destructive" : "muted";
|
|
60
|
-
|
|
61
88
|
return (
|
|
62
89
|
<div
|
|
63
90
|
ref={ref}
|
|
@@ -65,6 +92,8 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
65
92
|
className={cn(
|
|
66
93
|
"flex flex-col items-center justify-center text-center",
|
|
67
94
|
sizeStyles[size],
|
|
95
|
+
surface === "card" &&
|
|
96
|
+
"rounded-2xl border border-border bg-card shadow-[var(--shadow-card)]",
|
|
68
97
|
className
|
|
69
98
|
)}
|
|
70
99
|
{...props}
|
|
@@ -72,21 +101,37 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
|
72
101
|
{illustration ? (
|
|
73
102
|
<div className="mb-6">{illustration}</div>
|
|
74
103
|
) : (
|
|
75
|
-
<
|
|
104
|
+
<span
|
|
105
|
+
data-slot="empty-state-icon"
|
|
106
|
+
className={cn(
|
|
107
|
+
// The glyph size is the tile's, whatever size the caller's icon
|
|
108
|
+
// node asked for — a 40px icon in a 64px tile has no margin.
|
|
109
|
+
"mb-4 flex size-16 shrink-0 items-center justify-center rounded-2xl border border-border shadow-sm sm:size-18 [&_svg]:size-7",
|
|
110
|
+
variant === "error" ? "bg-destructive/5" : "bg-card"
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
{icon ?? DEFAULT_ICON[variant]}
|
|
114
|
+
</span>
|
|
76
115
|
)}
|
|
77
116
|
|
|
78
|
-
<Text as="h3" weight="semibold"
|
|
117
|
+
<Text as="h3" weight="semibold" className="text-lg">
|
|
79
118
|
{title}
|
|
80
119
|
</Text>
|
|
81
120
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
{description ? (
|
|
122
|
+
<Text as="p" color="muted" className="mt-3 max-w-md text-sm">
|
|
123
|
+
{description}
|
|
124
|
+
</Text>
|
|
125
|
+
) : null}
|
|
85
126
|
|
|
86
127
|
{(action || secondaryAction) && (
|
|
87
|
-
<div className="flex
|
|
128
|
+
<div className="mt-4 flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center">
|
|
88
129
|
{action && (
|
|
89
|
-
<Button
|
|
130
|
+
<Button
|
|
131
|
+
onClick={action.onClick}
|
|
132
|
+
variant={actionVariant ?? DEFAULT_ACTION_VARIANT[variant]}
|
|
133
|
+
className="glass-hover"
|
|
134
|
+
>
|
|
90
135
|
{action.label}
|
|
91
136
|
</Button>
|
|
92
137
|
)}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tokens / Floor — the floor plan's `--floor-*` palette (5a "Jewel",
|
|
5
|
+
* contrast-corrected). Every status fill gradient, its chair-ring colour and
|
|
6
|
+
* its foreground, in light and dark side by side. The dark column is a `.dark`
|
|
7
|
+
* scope, so keep the toolbar on Light to compare the two.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Status = {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
time?: string;
|
|
14
|
+
/** Token stem, e.g. `booked` → `--floor-booked-from`. */
|
|
15
|
+
stem: string;
|
|
16
|
+
/** The status overlay on the chair ring; statuses without one show the neutral track only. */
|
|
17
|
+
seat?: string;
|
|
18
|
+
foreground: string;
|
|
19
|
+
halo?: string;
|
|
20
|
+
ring?: { color: string; dashed: boolean };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const STATUSES: Status[] = [
|
|
24
|
+
{ key: "free", label: "T1", time: "Free", stem: "free", foreground: "--floor-free-foreground" },
|
|
25
|
+
{ key: "booked", label: "T2", time: "19:30", stem: "booked", seat: "--floor-booked-seat", foreground: "--floor-booked-foreground" },
|
|
26
|
+
{ key: "seated", label: "T3", time: "45m", stem: "seated", seat: "--floor-seated-seat", foreground: "--floor-seated-foreground" },
|
|
27
|
+
{ key: "over", label: "T4", time: "+12m", stem: "over", seat: "--floor-over-seat", foreground: "--floor-over-foreground", halo: "--floor-over-halo" },
|
|
28
|
+
{ key: "late", label: "T5", time: "Late 8m", stem: "late", seat: "--floor-late-seat", foreground: "--floor-late-foreground", halo: "--floor-late-halo" },
|
|
29
|
+
{ key: "blocked", label: "T6", time: "Held", stem: "blocked", seat: "--floor-blocked-seat", foreground: "--floor-blocked-foreground" },
|
|
30
|
+
{ key: "out-of-service", label: "T7", time: "Out", stem: "out-of-service", foreground: "--floor-out-of-service-foreground" },
|
|
31
|
+
{ key: "offline", label: "T8", time: "Offline", stem: "offline", seat: "--floor-offline-seat", foreground: "--floor-offline-foreground" },
|
|
32
|
+
{ key: "offer", label: "T9", time: "Offer", stem: "free", foreground: "--floor-free-foreground", ring: { color: "--floor-offer", dashed: true } },
|
|
33
|
+
{ key: "selected", label: "T10", time: "19:30", stem: "booked", seat: "--floor-booked-seat", foreground: "--floor-booked-foreground", ring: { color: "--floor-selected", dashed: false } },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const SINGLES = [
|
|
37
|
+
"--floor-seat",
|
|
38
|
+
"--floor-muted-foreground",
|
|
39
|
+
"--floor-on-fill-foreground",
|
|
40
|
+
"--floor-late-halo",
|
|
41
|
+
"--floor-over-halo",
|
|
42
|
+
"--floor-busy",
|
|
43
|
+
"--floor-error",
|
|
44
|
+
"--floor-error-foreground",
|
|
45
|
+
"--floor-offer",
|
|
46
|
+
"--floor-selected",
|
|
47
|
+
"--floor-room",
|
|
48
|
+
"--floor-wall",
|
|
49
|
+
"--floor-fixture",
|
|
50
|
+
"--floor-shadow",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Short side 60: the geometry in the token notes is in units of it. */
|
|
54
|
+
const S = 60;
|
|
55
|
+
const W = 96;
|
|
56
|
+
const RX = 0.1 * S;
|
|
57
|
+
const OFFSET = 0.0867 * S;
|
|
58
|
+
const HALO = 0.1756 * S;
|
|
59
|
+
|
|
60
|
+
function Table({ status, theme }: { status: Status; theme: string }) {
|
|
61
|
+
const id = `floor-${status.key}-${theme}`;
|
|
62
|
+
const pad = 14;
|
|
63
|
+
const ring = { x: pad - OFFSET, y: pad - OFFSET, w: W + 2 * OFFSET, h: S + 2 * OFFSET };
|
|
64
|
+
return (
|
|
65
|
+
<svg
|
|
66
|
+
width={W + 2 * pad}
|
|
67
|
+
height={S + 2 * pad}
|
|
68
|
+
viewBox={`0 0 ${W + 2 * pad} ${S + 2 * pad}`}
|
|
69
|
+
role="img"
|
|
70
|
+
aria-label={`${status.key} table`}
|
|
71
|
+
>
|
|
72
|
+
<defs>
|
|
73
|
+
<linearGradient id={id} x1="0" x2="1" y1="0" y2="0">
|
|
74
|
+
<stop offset="0" style={{ stopColor: `var(--floor-${status.stem}-from)` }} />
|
|
75
|
+
<stop offset="1" style={{ stopColor: `var(--floor-${status.stem}-to)` }} />
|
|
76
|
+
</linearGradient>
|
|
77
|
+
<filter id={`${id}-shadow`} x="-30%" y="-30%" width="160%" height="180%">
|
|
78
|
+
<feDropShadow dx="0" dy={0.047 * S} stdDeviation={0.053 * S} floodColor="var(--floor-shadow)" floodOpacity="0.16" />
|
|
79
|
+
</filter>
|
|
80
|
+
</defs>
|
|
81
|
+
{status.halo ? (
|
|
82
|
+
<rect
|
|
83
|
+
x={pad - HALO}
|
|
84
|
+
y={pad - HALO}
|
|
85
|
+
width={W + 2 * HALO}
|
|
86
|
+
height={S + 2 * HALO}
|
|
87
|
+
rx={RX + HALO}
|
|
88
|
+
fill="none"
|
|
89
|
+
strokeWidth={0.037 * S}
|
|
90
|
+
style={{ stroke: `var(${status.halo})` }}
|
|
91
|
+
/>
|
|
92
|
+
) : null}
|
|
93
|
+
<rect
|
|
94
|
+
x={ring.x}
|
|
95
|
+
y={ring.y}
|
|
96
|
+
width={ring.w}
|
|
97
|
+
height={ring.h}
|
|
98
|
+
rx={RX + OFFSET}
|
|
99
|
+
fill="none"
|
|
100
|
+
strokeWidth={0.0667 * S}
|
|
101
|
+
strokeLinecap="round"
|
|
102
|
+
style={{ stroke: "var(--floor-seat)" }}
|
|
103
|
+
/>
|
|
104
|
+
{status.seat ? (
|
|
105
|
+
<rect
|
|
106
|
+
x={ring.x}
|
|
107
|
+
y={ring.y}
|
|
108
|
+
width={ring.w}
|
|
109
|
+
height={ring.h}
|
|
110
|
+
rx={RX + OFFSET}
|
|
111
|
+
fill="none"
|
|
112
|
+
pathLength={100}
|
|
113
|
+
strokeDasharray="60 40"
|
|
114
|
+
strokeWidth={0.0667 * S}
|
|
115
|
+
strokeLinecap="round"
|
|
116
|
+
style={{ stroke: `var(${status.seat})` }}
|
|
117
|
+
/>
|
|
118
|
+
) : null}
|
|
119
|
+
{status.ring ? (
|
|
120
|
+
<rect
|
|
121
|
+
x={pad - HALO}
|
|
122
|
+
y={pad - HALO}
|
|
123
|
+
width={W + 2 * HALO}
|
|
124
|
+
height={S + 2 * HALO}
|
|
125
|
+
rx={RX + HALO}
|
|
126
|
+
fill="none"
|
|
127
|
+
strokeWidth={0.037 * S}
|
|
128
|
+
strokeDasharray={status.ring.dashed ? "4 3" : undefined}
|
|
129
|
+
style={{ stroke: `var(${status.ring.color})` }}
|
|
130
|
+
/>
|
|
131
|
+
) : null}
|
|
132
|
+
<rect x={pad} y={pad} width={W} height={S} rx={RX} fill={`url(#${id})`} filter={`url(#${id}-shadow)`} />
|
|
133
|
+
<text
|
|
134
|
+
x={pad + W / 2}
|
|
135
|
+
y={pad + S / 2 - 2}
|
|
136
|
+
textAnchor="middle"
|
|
137
|
+
fontSize={0.2067 * S}
|
|
138
|
+
fontWeight={700}
|
|
139
|
+
style={{ fill: `var(${status.foreground})` }}
|
|
140
|
+
>
|
|
141
|
+
{status.label}
|
|
142
|
+
</text>
|
|
143
|
+
{status.time ? (
|
|
144
|
+
<text
|
|
145
|
+
x={pad + W / 2}
|
|
146
|
+
y={pad + S / 2 + 0.1333 * S + 2}
|
|
147
|
+
textAnchor="middle"
|
|
148
|
+
fontSize={Math.max(12, 0.1333 * S)}
|
|
149
|
+
fontWeight={600}
|
|
150
|
+
style={{ fill: `var(${status.foreground})`, fontVariantNumeric: "tabular-nums" }}
|
|
151
|
+
>
|
|
152
|
+
{status.time}
|
|
153
|
+
</text>
|
|
154
|
+
) : null}
|
|
155
|
+
</svg>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Chip({ token }: { token: string }) {
|
|
160
|
+
return (
|
|
161
|
+
<div className="flex items-center gap-2">
|
|
162
|
+
<span
|
|
163
|
+
className="size-5 shrink-0 rounded-md border border-border"
|
|
164
|
+
style={{ background: `var(${token})` }}
|
|
165
|
+
/>
|
|
166
|
+
<code className="text-2xs text-muted-foreground">{token}</code>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function Panel({ theme }: { theme: "light" | "dark" }) {
|
|
172
|
+
return (
|
|
173
|
+
<section
|
|
174
|
+
className={`${theme === "dark" ? "dark" : ""} flex flex-col gap-6 rounded-2xl p-6 text-foreground`}
|
|
175
|
+
style={{ background: "var(--floor-room)" }}
|
|
176
|
+
>
|
|
177
|
+
<h2 className="text-lg font-semibold">{theme === "dark" ? "Dark" : "Light"}</h2>
|
|
178
|
+
<div className="flex flex-col gap-4">
|
|
179
|
+
{STATUSES.map((status) => (
|
|
180
|
+
<div key={status.key} className="flex items-center gap-4">
|
|
181
|
+
<Table status={status} theme={theme} />
|
|
182
|
+
<div className="flex flex-col gap-1">
|
|
183
|
+
<span className="text-sm font-semibold">{status.key}</span>
|
|
184
|
+
{status.ring ? (
|
|
185
|
+
<Chip token={status.ring.color} />
|
|
186
|
+
) : (
|
|
187
|
+
<>
|
|
188
|
+
<Chip token={`--floor-${status.stem}-from`} />
|
|
189
|
+
<Chip token={`--floor-${status.stem}-to`} />
|
|
190
|
+
{status.seat ? <Chip token={status.seat} /> : null}
|
|
191
|
+
<Chip token={status.foreground} />
|
|
192
|
+
{status.halo ? <Chip token={status.halo} /> : null}
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
<div className="grid grid-cols-2 gap-2 border-t border-border pt-4">
|
|
200
|
+
{SINGLES.map((token) => (
|
|
201
|
+
<Chip key={token} token={token} />
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
</section>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const meta = {
|
|
209
|
+
title: "Tokens/Floor",
|
|
210
|
+
parameters: { layout: "padded" },
|
|
211
|
+
} satisfies Meta;
|
|
212
|
+
|
|
213
|
+
export default meta;
|
|
214
|
+
type Story = StoryObj<typeof meta>;
|
|
215
|
+
|
|
216
|
+
export const BothThemes: Story = {
|
|
217
|
+
render: () => (
|
|
218
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
219
|
+
<Panel theme="light" />
|
|
220
|
+
<Panel theme="dark" />
|
|
221
|
+
</div>
|
|
222
|
+
),
|
|
223
|
+
};
|
|
@@ -78,6 +78,46 @@ describe("ResponsiveOverlay", () => {
|
|
|
78
78
|
expect(dialog).toHaveAccessibleDescription("Table 18 · from the menu");
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
describe("wires its title as the Radix title in every mode", () => {
|
|
82
|
+
const MODES: Array<{
|
|
83
|
+
name: string;
|
|
84
|
+
wide: boolean;
|
|
85
|
+
props: Partial<React.ComponentProps<typeof ResponsiveOverlay>>;
|
|
86
|
+
}> = [
|
|
87
|
+
{ name: "drawer", wide: false, props: {} },
|
|
88
|
+
{ name: "dialog", wide: true, props: {} },
|
|
89
|
+
{ name: "modal sheet", wide: true, props: { desktop: "sheet" } },
|
|
90
|
+
{ name: "live sheet", wide: true, props: { desktop: "sheet", modal: false } },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const logged = (...spies: Array<ReturnType<typeof vi.spyOn>>) =>
|
|
94
|
+
spies.flatMap((spy) => spy.mock.calls.flat()).join(" ");
|
|
95
|
+
|
|
96
|
+
for (const mode of MODES) {
|
|
97
|
+
it(`${mode.name}: named, described, and no Radix title warning`, () => {
|
|
98
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
99
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
100
|
+
setWidth(mode.wide);
|
|
101
|
+
open(mode.props);
|
|
102
|
+
const dialog = screen.getByRole("dialog");
|
|
103
|
+
expect(dialog).toHaveAccessibleName("Add an item");
|
|
104
|
+
expect(dialog).toHaveAccessibleDescription("Table 18 · from the menu");
|
|
105
|
+
expect(logged(error, warn)).not.toMatch(/DialogTitle|Description/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it(`${mode.name}: no description warning without a subtitle`, () => {
|
|
109
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
110
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
111
|
+
setWidth(mode.wide);
|
|
112
|
+
open({ ...mode.props, subtitle: undefined });
|
|
113
|
+
const dialog = screen.getByRole("dialog");
|
|
114
|
+
expect(dialog).toHaveAccessibleName("Add an item");
|
|
115
|
+
expect(dialog).not.toHaveAttribute("aria-describedby");
|
|
116
|
+
expect(logged(error, warn)).not.toMatch(/DialogTitle|Description/);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
81
121
|
it("closes through the round button", async () => {
|
|
82
122
|
const onOpenChange = vi.fn();
|
|
83
123
|
open({ onOpenChange });
|
|
@@ -164,21 +164,27 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
164
164
|
) => {
|
|
165
165
|
const isWide = useMediaQuery(wideFrom === "lg" ? DESKTOP_MEDIA_QUERY : MD_MEDIA_QUERY);
|
|
166
166
|
const container: "drawer" | "dialog" | "sheet" = isWide ? desktop : "drawer";
|
|
167
|
-
const titleId = React.useId();
|
|
168
|
-
const subtitleId = React.useId();
|
|
169
167
|
useFocusReturn(open);
|
|
170
168
|
|
|
169
|
+
// The name and the description are Radix's own: its `Title` and
|
|
170
|
+
// `Description` register ids in the dialog context and `Content` points
|
|
171
|
+
// `aria-labelledby` / `aria-describedby` at them. Passing our own ids
|
|
172
|
+
// overrode the registered one, so Radix found no title under its id and
|
|
173
|
+
// logged "DialogContent requires a DialogTitle" in every mode. Without a subtitle, `aria-describedby={undefined}` is Radix's
|
|
174
|
+
// documented opt-out, rather than a pointer at an element that never renders.
|
|
175
|
+
const describedBy = subtitle ? {} : { "aria-describedby": undefined };
|
|
176
|
+
|
|
171
177
|
const Title = CONTAINER_TITLE[container];
|
|
172
178
|
const Description = CONTAINER_DESCRIPTION[container];
|
|
173
179
|
|
|
174
180
|
const header = (
|
|
175
181
|
<div data-slot="responsive-overlay-header" className="flex items-start gap-3 px-5 pt-5">
|
|
176
182
|
<div className="min-w-0 flex-1">
|
|
177
|
-
<Title
|
|
183
|
+
<Title className="text-[19px] font-bold leading-tight">
|
|
178
184
|
{title}
|
|
179
185
|
</Title>
|
|
180
186
|
{subtitle ? (
|
|
181
|
-
<Description
|
|
187
|
+
<Description className="mt-1 text-[13px] text-muted-foreground">
|
|
182
188
|
{subtitle}
|
|
183
189
|
</Description>
|
|
184
190
|
) : null}
|
|
@@ -226,8 +232,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
226
232
|
attachment="inset"
|
|
227
233
|
hideClose
|
|
228
234
|
overlayClassName="bg-scrim-overlay backdrop-blur-none"
|
|
229
|
-
|
|
230
|
-
aria-describedby={subtitle ? subtitleId : undefined}
|
|
235
|
+
{...describedBy}
|
|
231
236
|
data-slot="responsive-overlay-sheet"
|
|
232
237
|
data-modal={modal ? "true" : "false"}
|
|
233
238
|
// A working rail must survive the page being used: a click on the
|
|
@@ -258,8 +263,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
258
263
|
size={size}
|
|
259
264
|
showCloseButton={false}
|
|
260
265
|
overlayClassName="bg-scrim-overlay backdrop-blur-none"
|
|
261
|
-
|
|
262
|
-
aria-describedby={subtitle ? subtitleId : undefined}
|
|
266
|
+
{...describedBy}
|
|
263
267
|
className={cn(
|
|
264
268
|
// The dialog's own `grid gap-4 p-6` would put air between the
|
|
265
269
|
// header, the body and the band the frames draw flush.
|
|
@@ -280,8 +284,7 @@ const ResponsiveOverlay = React.forwardRef<HTMLDivElement, ResponsiveOverlayProp
|
|
|
280
284
|
<DrawerContent
|
|
281
285
|
ref={ref}
|
|
282
286
|
overlayClassName="bg-scrim-overlay"
|
|
283
|
-
|
|
284
|
-
aria-describedby={subtitle ? subtitleId : undefined}
|
|
287
|
+
{...describedBy}
|
|
285
288
|
className={cn("flex max-h-[85vh] flex-col p-0", className)}
|
|
286
289
|
>
|
|
287
290
|
{header}
|
package/src/globals.css
CHANGED
|
@@ -312,6 +312,70 @@ h6 {
|
|
|
312
312
|
than a black one before it looks dirty. */
|
|
313
313
|
--icon-depth-neutral: drop-shadow(0 1px 1px oklch(0.38 0 0 / 0.29))
|
|
314
314
|
drop-shadow(0 2px 4px oklch(0.38 0 0 / 0.22));
|
|
315
|
+
|
|
316
|
+
/* ─── Floor plan (5a "Jewel", contrast-corrected) ─────────────────────
|
|
317
|
+
The table map's status palette. 5a as drawn failed contrast (white on
|
|
318
|
+
booked/seated/late/blocked 2.4–4.6:1, the empty seat track 1.49:1 on the
|
|
319
|
+
card); these keep its hue, chroma and gradient direction and correct
|
|
320
|
+
lightness only. Worst point over 15–85% of each gradient: text >= 4.79:1
|
|
321
|
+
light / 6.29:1 dark, seats >= 3.03:1 / 3.11:1 against card and room.
|
|
322
|
+
|
|
323
|
+
Per status: `-from` / `-to` are the table fill, a horizontal gradient
|
|
324
|
+
inside the table's rotated group (SVG `stop-color: var(--floor-…-from)`);
|
|
325
|
+
`-seat` is the status overlay on the chair ring, drawn over the neutral
|
|
326
|
+
`--floor-seat` track; `-foreground` is the label and time on the fill.
|
|
327
|
+
Booked, seated, late and blocked share `--floor-on-fill-foreground`.
|
|
328
|
+
Halos (late, over time) pulse outside the ring; `--floor-offer` is the
|
|
329
|
+
dashed offer ring and `--floor-selected` the solid selection ring.
|
|
330
|
+
`--floor-shadow` is the flood colour of the table's three drop shadows,
|
|
331
|
+
their alphas multiplied by `--floor-shadow-strength`.
|
|
332
|
+
Tailwind: `fill-floor-*`, `stroke-floor-*`, `bg-floor-*`, `text-floor-*`. */
|
|
333
|
+
--floor-free-from: oklch(1 0 0);
|
|
334
|
+
--floor-free-to: oklch(0.955 0.012 65);
|
|
335
|
+
--floor-free-foreground: oklch(0.22 0.025 45);
|
|
336
|
+
--floor-muted-foreground: oklch(0.48 0.02 45);
|
|
337
|
+
--floor-seat: oklch(0.66 0.012 60);
|
|
338
|
+
--floor-booked-from: oklch(0.47 0.13 258);
|
|
339
|
+
--floor-booked-to: oklch(0.555 0.115 236);
|
|
340
|
+
--floor-booked-seat: oklch(0.6 0.12 248);
|
|
341
|
+
--floor-seated-from: oklch(0.45 0.095 163);
|
|
342
|
+
--floor-seated-to: oklch(0.545 0.145 150);
|
|
343
|
+
--floor-seated-seat: oklch(0.53 0.12 158);
|
|
344
|
+
--floor-over-from: oklch(0.74 0.15 62);
|
|
345
|
+
--floor-over-to: oklch(0.85 0.15 92);
|
|
346
|
+
--floor-over-seat: oklch(0.64 0.145 62);
|
|
347
|
+
--floor-over-foreground: oklch(0.22 0.025 45);
|
|
348
|
+
--floor-late-from: oklch(0.5 0.19 18);
|
|
349
|
+
--floor-late-to: oklch(0.57 0.18 32);
|
|
350
|
+
--floor-late-seat: oklch(0.56 0.19 22);
|
|
351
|
+
--floor-blocked-from: oklch(0.48 0.015 60);
|
|
352
|
+
--floor-blocked-to: oklch(0.555 0.01 70);
|
|
353
|
+
--floor-blocked-seat: oklch(0.6 0.012 60);
|
|
354
|
+
--floor-out-of-service-from: oklch(0.93 0.006 60);
|
|
355
|
+
--floor-out-of-service-to: oklch(0.96 0.004 70);
|
|
356
|
+
--floor-offline-from: oklch(0.86 0.004 60);
|
|
357
|
+
--floor-offline-to: oklch(0.9 0.004 70);
|
|
358
|
+
--floor-offline-seat: oklch(0.64 0.006 60);
|
|
359
|
+
--floor-offline-foreground: oklch(0.3 0.012 50);
|
|
360
|
+
--floor-on-fill-foreground: oklch(0.99 0.005 70);
|
|
361
|
+
--floor-late-halo: oklch(0.56 0.2 27);
|
|
362
|
+
--floor-over-halo: oklch(0.64 0.145 62);
|
|
363
|
+
--floor-busy: oklch(0.42 0.02 45);
|
|
364
|
+
--floor-error: oklch(0.56 0.2 27);
|
|
365
|
+
--floor-error-foreground: oklch(0.99 0 0);
|
|
366
|
+
--floor-offer: var(--primary);
|
|
367
|
+
--floor-selected: oklch(0.25 0.05 35);
|
|
368
|
+
--floor-room: oklch(0.99 0.005 75);
|
|
369
|
+
--floor-wall: oklch(0.33 0.02 45);
|
|
370
|
+
--floor-fixture: oklch(0.62 0.018 50);
|
|
371
|
+
--floor-shadow: oklch(0.222 0.036 53.5);
|
|
372
|
+
--floor-shadow-strength: 1;
|
|
373
|
+
/* Per-status foreground aliases, so a status maps to one token family. */
|
|
374
|
+
--floor-booked-foreground: var(--floor-on-fill-foreground);
|
|
375
|
+
--floor-seated-foreground: var(--floor-on-fill-foreground);
|
|
376
|
+
--floor-late-foreground: var(--floor-on-fill-foreground);
|
|
377
|
+
--floor-blocked-foreground: var(--floor-on-fill-foreground);
|
|
378
|
+
--floor-out-of-service-foreground: var(--floor-muted-foreground);
|
|
315
379
|
}
|
|
316
380
|
|
|
317
381
|
.dark {
|
|
@@ -424,6 +488,57 @@ h6 {
|
|
|
424
488
|
pushed harder since the panel absorbs most of the shadow. */
|
|
425
489
|
--icon-depth-neutral: drop-shadow(0 1px 1px oklch(0 0 0 / 0.47))
|
|
426
490
|
drop-shadow(0 2px 4px oklch(0 0 0 / 0.34));
|
|
491
|
+
|
|
492
|
+
/* Floor plan, dark — derived from the light set (see :root). Fills sit
|
|
493
|
+
deeper, seats and halos lift off the dark card, the over-time amber keeps
|
|
494
|
+
dark type. The shadow goes black at twice the alpha: a warm shadow has
|
|
495
|
+
nothing to darken on a 0.225 room. Aliases are restated, as the sidebar's
|
|
496
|
+
are, so a `.dark` below the root does not inherit the light values. */
|
|
497
|
+
--floor-free-from: oklch(0.32 0.012 60);
|
|
498
|
+
--floor-free-to: oklch(0.285 0.01 60);
|
|
499
|
+
--floor-free-foreground: oklch(0.96 0.005 60);
|
|
500
|
+
--floor-muted-foreground: oklch(0.76 0.01 60);
|
|
501
|
+
--floor-seat: oklch(0.54 0.012 60);
|
|
502
|
+
--floor-booked-from: oklch(0.4 0.12 258);
|
|
503
|
+
--floor-booked-to: oklch(0.48 0.1 236);
|
|
504
|
+
--floor-booked-seat: oklch(0.64 0.12 245);
|
|
505
|
+
--floor-seated-from: oklch(0.39 0.082 163);
|
|
506
|
+
--floor-seated-to: oklch(0.475 0.125 150);
|
|
507
|
+
--floor-seated-seat: oklch(0.66 0.15 155);
|
|
508
|
+
--floor-over-from: oklch(0.7 0.14 62);
|
|
509
|
+
--floor-over-to: oklch(0.8 0.14 88);
|
|
510
|
+
--floor-over-seat: oklch(0.78 0.15 78);
|
|
511
|
+
--floor-over-foreground: oklch(0.2 0.03 40);
|
|
512
|
+
--floor-late-from: oklch(0.42 0.16 18);
|
|
513
|
+
--floor-late-to: oklch(0.5 0.165 30);
|
|
514
|
+
--floor-late-seat: oklch(0.64 0.19 25);
|
|
515
|
+
--floor-blocked-from: oklch(0.38 0.012 60);
|
|
516
|
+
--floor-blocked-to: oklch(0.44 0.01 70);
|
|
517
|
+
--floor-blocked-seat: oklch(0.58 0.012 60);
|
|
518
|
+
--floor-out-of-service-from: oklch(0.26 0.006 60);
|
|
519
|
+
--floor-out-of-service-to: oklch(0.24 0.005 60);
|
|
520
|
+
--floor-offline-from: oklch(0.3 0.004 60);
|
|
521
|
+
--floor-offline-to: oklch(0.27 0.004 60);
|
|
522
|
+
--floor-offline-seat: oklch(0.52 0.004 60);
|
|
523
|
+
--floor-offline-foreground: oklch(0.82 0.004 60);
|
|
524
|
+
--floor-on-fill-foreground: oklch(0.98 0.005 70);
|
|
525
|
+
--floor-late-halo: oklch(0.66 0.2 25);
|
|
526
|
+
--floor-over-halo: oklch(0.82 0.16 82);
|
|
527
|
+
--floor-busy: oklch(0.82 0.01 60);
|
|
528
|
+
--floor-error: oklch(0.58 0.19 25);
|
|
529
|
+
--floor-error-foreground: oklch(0.99 0 0);
|
|
530
|
+
--floor-offer: var(--primary);
|
|
531
|
+
--floor-selected: oklch(0.96 0.005 60);
|
|
532
|
+
--floor-room: oklch(0.225 0.012 45);
|
|
533
|
+
--floor-wall: oklch(0.78 0.01 60);
|
|
534
|
+
--floor-fixture: oklch(0.55 0.012 60);
|
|
535
|
+
--floor-shadow: oklch(0 0 0);
|
|
536
|
+
--floor-shadow-strength: 2;
|
|
537
|
+
--floor-booked-foreground: var(--floor-on-fill-foreground);
|
|
538
|
+
--floor-seated-foreground: var(--floor-on-fill-foreground);
|
|
539
|
+
--floor-late-foreground: var(--floor-on-fill-foreground);
|
|
540
|
+
--floor-blocked-foreground: var(--floor-on-fill-foreground);
|
|
541
|
+
--floor-out-of-service-foreground: var(--floor-muted-foreground);
|
|
427
542
|
}
|
|
428
543
|
|
|
429
544
|
/* Named 11px step (`text-2xs`) for micro-labels: overlay pills, hour
|
|
@@ -477,6 +592,52 @@ h6 {
|
|
|
477
592
|
--color-avatar-blue: var(--avatar-blue);
|
|
478
593
|
--color-avatar-violet: var(--avatar-violet);
|
|
479
594
|
--color-avatar-rose: var(--avatar-rose);
|
|
595
|
+
/* `fill-floor-booked-from`, `stroke-floor-seat`, `text-floor-on-fill-foreground` … —
|
|
596
|
+
the floor plan's status palette (see `--floor-*` in :root). */
|
|
597
|
+
--color-floor-free-from: var(--floor-free-from);
|
|
598
|
+
--color-floor-free-to: var(--floor-free-to);
|
|
599
|
+
--color-floor-free-foreground: var(--floor-free-foreground);
|
|
600
|
+
--color-floor-muted-foreground: var(--floor-muted-foreground);
|
|
601
|
+
--color-floor-seat: var(--floor-seat);
|
|
602
|
+
--color-floor-booked-from: var(--floor-booked-from);
|
|
603
|
+
--color-floor-booked-to: var(--floor-booked-to);
|
|
604
|
+
--color-floor-booked-seat: var(--floor-booked-seat);
|
|
605
|
+
--color-floor-booked-foreground: var(--floor-booked-foreground);
|
|
606
|
+
--color-floor-seated-from: var(--floor-seated-from);
|
|
607
|
+
--color-floor-seated-to: var(--floor-seated-to);
|
|
608
|
+
--color-floor-seated-seat: var(--floor-seated-seat);
|
|
609
|
+
--color-floor-seated-foreground: var(--floor-seated-foreground);
|
|
610
|
+
--color-floor-over-from: var(--floor-over-from);
|
|
611
|
+
--color-floor-over-to: var(--floor-over-to);
|
|
612
|
+
--color-floor-over-seat: var(--floor-over-seat);
|
|
613
|
+
--color-floor-over-foreground: var(--floor-over-foreground);
|
|
614
|
+
--color-floor-late-from: var(--floor-late-from);
|
|
615
|
+
--color-floor-late-to: var(--floor-late-to);
|
|
616
|
+
--color-floor-late-seat: var(--floor-late-seat);
|
|
617
|
+
--color-floor-late-foreground: var(--floor-late-foreground);
|
|
618
|
+
--color-floor-blocked-from: var(--floor-blocked-from);
|
|
619
|
+
--color-floor-blocked-to: var(--floor-blocked-to);
|
|
620
|
+
--color-floor-blocked-seat: var(--floor-blocked-seat);
|
|
621
|
+
--color-floor-blocked-foreground: var(--floor-blocked-foreground);
|
|
622
|
+
--color-floor-out-of-service-from: var(--floor-out-of-service-from);
|
|
623
|
+
--color-floor-out-of-service-to: var(--floor-out-of-service-to);
|
|
624
|
+
--color-floor-out-of-service-foreground: var(--floor-out-of-service-foreground);
|
|
625
|
+
--color-floor-offline-from: var(--floor-offline-from);
|
|
626
|
+
--color-floor-offline-to: var(--floor-offline-to);
|
|
627
|
+
--color-floor-offline-seat: var(--floor-offline-seat);
|
|
628
|
+
--color-floor-offline-foreground: var(--floor-offline-foreground);
|
|
629
|
+
--color-floor-on-fill-foreground: var(--floor-on-fill-foreground);
|
|
630
|
+
--color-floor-late-halo: var(--floor-late-halo);
|
|
631
|
+
--color-floor-over-halo: var(--floor-over-halo);
|
|
632
|
+
--color-floor-busy: var(--floor-busy);
|
|
633
|
+
--color-floor-error: var(--floor-error);
|
|
634
|
+
--color-floor-error-foreground: var(--floor-error-foreground);
|
|
635
|
+
--color-floor-offer: var(--floor-offer);
|
|
636
|
+
--color-floor-selected: var(--floor-selected);
|
|
637
|
+
--color-floor-room: var(--floor-room);
|
|
638
|
+
--color-floor-wall: var(--floor-wall);
|
|
639
|
+
--color-floor-fixture: var(--floor-fixture);
|
|
640
|
+
--color-floor-shadow: var(--floor-shadow);
|
|
480
641
|
|
|
481
642
|
/* Radius scale */
|
|
482
643
|
--radius-sm: calc(var(--radius) - 4px);
|