@medalsocial/meda 2.8.1 → 3.2.0
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/README.md +130 -1
- package/dist/booking/add-child-sheet.d.ts +38 -0
- package/dist/booking/add-child-sheet.js +88 -0
- package/dist/booking/booking-skeleton.d.ts +24 -0
- package/dist/booking/booking-skeleton.js +17 -0
- package/dist/booking/confirmation.d.ts +85 -0
- package/dist/booking/confirmation.js +102 -0
- package/dist/booking/details-screen.d.ts +118 -0
- package/dist/booking/details-screen.js +314 -0
- package/dist/booking/format.d.ts +64 -0
- package/dist/booking/format.js +1 -0
- package/dist/booking/index.d.ts +35 -0
- package/dist/booking/index.js +40 -0
- package/dist/booking/internal/label-parts.d.ts +8 -0
- package/dist/booking/internal/label-parts.js +19 -0
- package/dist/booking/internal/ui.d.ts +33 -0
- package/dist/booking/internal/ui.js +35 -0
- package/dist/booking/label-keys.d.ts +34 -0
- package/dist/booking/label-keys.js +56 -0
- package/dist/booking/labels.d.ts +15 -0
- package/dist/booking/labels.js +17 -0
- package/dist/booking/live-status.d.ts +14 -0
- package/dist/booking/live-status.js +17 -0
- package/dist/booking/login-panel.d.ts +108 -0
- package/dist/booking/login-panel.js +239 -0
- package/dist/booking/login-sheet.d.ts +41 -0
- package/dist/booking/login-sheet.js +49 -0
- package/dist/booking/manage-screen.d.ts +113 -0
- package/dist/booking/manage-screen.js +369 -0
- package/dist/booking/otp-slots.d.ts +42 -0
- package/dist/booking/otp-slots.js +109 -0
- package/dist/booking/portal/account-card.d.ts +24 -0
- package/dist/booking/portal/account-card.js +18 -0
- package/dist/booking/portal/action-result.d.ts +30 -0
- package/dist/booking/portal/action-result.js +18 -0
- package/dist/booking/portal/age-confirm-card.d.ts +39 -0
- package/dist/booking/portal/age-confirm-card.js +42 -0
- package/dist/booking/portal/child-cards.d.ts +65 -0
- package/dist/booking/portal/child-cards.js +59 -0
- package/dist/booking/portal/data-controls.d.ts +48 -0
- package/dist/booking/portal/data-controls.js +103 -0
- package/dist/booking/portal/family-editor.d.ts +141 -0
- package/dist/booking/portal/family-editor.js +422 -0
- package/dist/booking/portal/logout-button.d.ts +21 -0
- package/dist/booking/portal/logout-button.js +27 -0
- package/dist/booking/portal/portal-shell.d.ts +71 -0
- package/dist/booking/portal/portal-shell.js +38 -0
- package/dist/booking/portal/portal-unreachable.d.ts +31 -0
- package/dist/booking/portal/portal-unreachable.js +19 -0
- package/dist/booking/portal/profile-form.d.ts +75 -0
- package/dist/booking/portal/profile-form.js +144 -0
- package/dist/booking/portal/rebook-cards.d.ts +44 -0
- package/dist/booking/portal/rebook-cards.js +35 -0
- package/dist/booking/portal/upcoming-bookings.d.ts +65 -0
- package/dist/booking/portal/upcoming-bookings.js +88 -0
- package/dist/booking/portal/vipps-link-row.d.ts +54 -0
- package/dist/booking/portal/vipps-link-row.js +80 -0
- package/dist/booking/portal/visit-history.d.ts +63 -0
- package/dist/booking/portal/visit-history.js +73 -0
- package/dist/booking/service-screen.d.ts +137 -0
- package/dist/booking/service-screen.js +233 -0
- package/dist/booking/slots.d.ts +12 -0
- package/dist/booking/slots.js +5 -0
- package/dist/booking/stylist-screen.d.ts +117 -0
- package/dist/booking/stylist-screen.js +226 -0
- package/dist/booking/summary-bar.d.ts +26 -0
- package/dist/booking/summary-bar.js +32 -0
- package/dist/booking/time-screen.d.ts +191 -0
- package/dist/booking/time-screen.js +418 -0
- package/dist/booking/types.d.ts +284 -0
- package/dist/booking/types.js +15 -0
- package/dist/booking/vipps-button.d.ts +49 -0
- package/dist/booking/vipps-button.js +33 -0
- package/dist/booking/who-screen.d.ts +121 -0
- package/dist/booking/who-screen.js +164 -0
- package/dist/primitives/avatar.d.ts +24 -0
- package/dist/primitives/avatar.js +44 -0
- package/dist/primitives/button.d.ts +21 -0
- package/dist/primitives/button.js +51 -0
- package/dist/primitives/checkbox.d.ts +23 -0
- package/dist/primitives/checkbox.js +20 -0
- package/dist/primitives/field.d.ts +47 -0
- package/dist/primitives/field.js +48 -0
- package/dist/primitives/index.d.ts +16 -0
- package/dist/primitives/index.js +8 -0
- package/dist/primitives/input.d.ts +10 -0
- package/dist/primitives/input.js +9 -0
- package/dist/primitives/sheet.d.ts +68 -0
- package/dist/primitives/sheet.js +303 -0
- package/dist/primitives/textarea.d.ts +14 -0
- package/dist/primitives/textarea.js +13 -0
- package/dist/primitives/toggle-group.d.ts +46 -0
- package/dist/primitives/toggle-group.js +148 -0
- package/dist/styles/base.css +40 -0
- package/dist/styles/booking.css +10 -0
- package/dist/styles/bridge.css +159 -0
- package/dist/styles/calendar.css +8 -0
- package/dist/styles/primitives.css +10 -0
- package/dist/styles/theme.css +12 -162
- package/dist/styles/vendor/xyflow.css +626 -0
- package/dist/styles/workflow-builder.css +17 -0
- package/package.json +45 -9
- package/skills/components/SKILL.md +53 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, isValidElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { renderElement } from '../lib/render-element.js';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
const SheetContext = createContext(null);
|
|
7
|
+
function useSheetContext(part) {
|
|
8
|
+
const context = useContext(SheetContext);
|
|
9
|
+
if (!context)
|
|
10
|
+
throw new Error(`Sheet.${part} must be rendered inside <Sheet>.`);
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
const FOCUSABLE = 'a[href],area[href],button:not([disabled]),input:not([disabled]):not([type="hidden"]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
|
|
14
|
+
// One page-wide scroll lock, reference-counted, so a second sheet closing
|
|
15
|
+
// cannot unlock the page under a first that is still open.
|
|
16
|
+
let scrollLocks = 0;
|
|
17
|
+
let scrollOverflowBefore = '';
|
|
18
|
+
function lockScroll() {
|
|
19
|
+
const root = document.documentElement;
|
|
20
|
+
if (scrollLocks === 0) {
|
|
21
|
+
scrollOverflowBefore = root.style.overflow;
|
|
22
|
+
root.style.overflow = 'hidden';
|
|
23
|
+
}
|
|
24
|
+
scrollLocks += 1;
|
|
25
|
+
}
|
|
26
|
+
function unlockScroll() {
|
|
27
|
+
scrollLocks = Math.max(0, scrollLocks - 1);
|
|
28
|
+
if (scrollLocks === 0)
|
|
29
|
+
document.documentElement.style.overflow = scrollOverflowBefore;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A rendered host that is a bare `<button>` defaults to `type="submit"` inside
|
|
33
|
+
* a form; give it `type="button"` unless the caller chose a type.
|
|
34
|
+
*/
|
|
35
|
+
function hostDefaults(render) {
|
|
36
|
+
if (!isValidElement(render) || render.type !== 'button')
|
|
37
|
+
return {};
|
|
38
|
+
const own = render.props.type;
|
|
39
|
+
return own === undefined ? { type: 'button' } : {};
|
|
40
|
+
}
|
|
41
|
+
/** A rendered `<a href>` must not navigate away from the sheet it opens or closes. */
|
|
42
|
+
function stopAnchorNavigation(event) {
|
|
43
|
+
if (event.currentTarget instanceof HTMLAnchorElement)
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
}
|
|
46
|
+
function SheetRoot({ open: openProp, defaultOpen = false, onOpenChange, dismissible = true, children, }) {
|
|
47
|
+
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
|
|
48
|
+
const isControlled = openProp !== undefined;
|
|
49
|
+
const open = isControlled ? openProp : uncontrolled;
|
|
50
|
+
const [titleId, registerTitle] = useState(undefined);
|
|
51
|
+
const [descriptionId, registerDescription] = useState(undefined);
|
|
52
|
+
const triggerRef = useRef(null);
|
|
53
|
+
const defaultTitleId = useId();
|
|
54
|
+
const defaultDescriptionId = useId();
|
|
55
|
+
const requestOpenChange = useCallback((next) => {
|
|
56
|
+
if (!next && !dismissible)
|
|
57
|
+
return;
|
|
58
|
+
if (!isControlled)
|
|
59
|
+
setUncontrolled(next);
|
|
60
|
+
onOpenChange?.(next);
|
|
61
|
+
}, [dismissible, isControlled, onOpenChange]);
|
|
62
|
+
return (_jsx(SheetContext.Provider, { value: {
|
|
63
|
+
open,
|
|
64
|
+
dismissible,
|
|
65
|
+
requestOpenChange,
|
|
66
|
+
titleId,
|
|
67
|
+
descriptionId,
|
|
68
|
+
defaultTitleId,
|
|
69
|
+
defaultDescriptionId,
|
|
70
|
+
registerTitle,
|
|
71
|
+
registerDescription,
|
|
72
|
+
triggerRef,
|
|
73
|
+
}, children: children }));
|
|
74
|
+
}
|
|
75
|
+
function SheetTrigger({ render, onClick, className, children, ...props }) {
|
|
76
|
+
const sheet = useSheetContext('Trigger');
|
|
77
|
+
const handleClick = (event) => {
|
|
78
|
+
onClick?.(event);
|
|
79
|
+
if (event.defaultPrevented)
|
|
80
|
+
return;
|
|
81
|
+
stopAnchorNavigation(event);
|
|
82
|
+
sheet.triggerRef.current = event.currentTarget;
|
|
83
|
+
sheet.requestOpenChange(true);
|
|
84
|
+
};
|
|
85
|
+
const triggerProps = {
|
|
86
|
+
...props,
|
|
87
|
+
'data-slot': 'sheet-trigger',
|
|
88
|
+
'aria-haspopup': 'dialog',
|
|
89
|
+
'aria-expanded': sheet.open,
|
|
90
|
+
'data-state': sheet.open ? 'open' : 'closed',
|
|
91
|
+
className,
|
|
92
|
+
onClick: handleClick,
|
|
93
|
+
};
|
|
94
|
+
if (render) {
|
|
95
|
+
// Children given to the trigger win; otherwise the rendered element keeps its own.
|
|
96
|
+
return renderElement(render, {
|
|
97
|
+
...hostDefaults(render),
|
|
98
|
+
...triggerProps,
|
|
99
|
+
...(children == null ? {} : { children }),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return (_jsx("button", { type: "button", ...triggerProps, children: children }));
|
|
103
|
+
}
|
|
104
|
+
const SIDE_CLASSES = {
|
|
105
|
+
responsive: cn('inset-x-0 top-auto bottom-0 w-full max-w-none rounded-t-2xl', 'md:inset-x-auto md:top-1/2 md:bottom-auto md:left-1/2 md:w-full md:max-w-md md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-xl'),
|
|
106
|
+
bottom: 'inset-x-0 top-auto bottom-0 w-full max-w-none rounded-t-2xl',
|
|
107
|
+
};
|
|
108
|
+
// Padding lives on the inner wrapper, never on the <dialog>: a click that
|
|
109
|
+
// lands on the <dialog> element itself is then always a backdrop click.
|
|
110
|
+
const BODY_CLASSES = {
|
|
111
|
+
responsive: 'pb-[max(1.5rem,env(safe-area-inset-bottom))] md:pb-6',
|
|
112
|
+
bottom: 'pb-[max(1.5rem,env(safe-area-inset-bottom))]',
|
|
113
|
+
};
|
|
114
|
+
function SheetContent({ side = 'responsive', className, bodyClassName, children, onCancel, onClick, onClose, onKeyDown, onPointerDown, ...props }) {
|
|
115
|
+
const sheet = useSheetContext('Content');
|
|
116
|
+
const dialogRef = useRef(null);
|
|
117
|
+
const restoreRef = useRef(null);
|
|
118
|
+
const closingRef = useRef(false);
|
|
119
|
+
const pressStartedOnBackdrop = useRef(false);
|
|
120
|
+
const { open, dismissible, requestOpenChange } = sheet;
|
|
121
|
+
const showDialog = useCallback((dialog) => {
|
|
122
|
+
if (typeof dialog.showModal === 'function') {
|
|
123
|
+
dialog.showModal();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
dialog.setAttribute('open', '');
|
|
127
|
+
// React focuses `autoFocus` children itself (it never renders the
|
|
128
|
+
// attribute), so only pick a target when focus is still outside.
|
|
129
|
+
if (dialog.contains(document.activeElement))
|
|
130
|
+
return;
|
|
131
|
+
const target = dialog.querySelector('[autofocus]') ??
|
|
132
|
+
dialog.querySelector(FOCUSABLE);
|
|
133
|
+
target?.focus();
|
|
134
|
+
}, []);
|
|
135
|
+
// Open with showModal(): the browser puts the sheet in the top layer, makes
|
|
136
|
+
// everything behind it inert and traps Tab inside. Environments without it
|
|
137
|
+
// (jsdom, very old engines) fall back to the `open` attribute and a manual
|
|
138
|
+
// first-focus, so the sheet still renders and is still operable.
|
|
139
|
+
useLayoutEffect(() => {
|
|
140
|
+
const dialog = dialogRef.current;
|
|
141
|
+
if (!dialog)
|
|
142
|
+
return;
|
|
143
|
+
if (open) {
|
|
144
|
+
if (dialog.hasAttribute('open'))
|
|
145
|
+
return;
|
|
146
|
+
const active = document.activeElement;
|
|
147
|
+
restoreRef.current =
|
|
148
|
+
sheet.triggerRef.current ?? (active instanceof HTMLElement ? active : null);
|
|
149
|
+
showDialog(dialog);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!dialog.hasAttribute('open'))
|
|
153
|
+
return;
|
|
154
|
+
closingRef.current = true;
|
|
155
|
+
if (typeof dialog.close === 'function')
|
|
156
|
+
dialog.close();
|
|
157
|
+
else
|
|
158
|
+
dialog.removeAttribute('open');
|
|
159
|
+
closingRef.current = false;
|
|
160
|
+
const restore = restoreRef.current;
|
|
161
|
+
restoreRef.current = null;
|
|
162
|
+
if (restore?.isConnected)
|
|
163
|
+
restore.focus();
|
|
164
|
+
}, [open, sheet.triggerRef, showDialog]);
|
|
165
|
+
// The top layer does not stop the page behind from scrolling on touch
|
|
166
|
+
// devices, so hold the root's overflow while the sheet is open.
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!open)
|
|
169
|
+
return;
|
|
170
|
+
lockScroll();
|
|
171
|
+
return unlockScroll;
|
|
172
|
+
}, [open]);
|
|
173
|
+
// Unmounting while open (route change) must still hand focus back.
|
|
174
|
+
useEffect(() => () => {
|
|
175
|
+
const restore = restoreRef.current;
|
|
176
|
+
if (restore?.isConnected)
|
|
177
|
+
restore.focus();
|
|
178
|
+
}, []);
|
|
179
|
+
const handleCancel = (event) => {
|
|
180
|
+
// A platform close request (Android back, Escape where keydown did not
|
|
181
|
+
// run): keep React in charge of the open state.
|
|
182
|
+
onCancel?.(event);
|
|
183
|
+
const consumerPrevented = event.defaultPrevented;
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
if (!consumerPrevented)
|
|
186
|
+
requestOpenChange(false);
|
|
187
|
+
};
|
|
188
|
+
// A close nobody asked React for — `<form method="dialog">`, a direct
|
|
189
|
+
// `dialog.close()` — must not leave the state (and the scroll lock) open.
|
|
190
|
+
const handleClose = (event) => {
|
|
191
|
+
onClose?.(event);
|
|
192
|
+
if (closingRef.current || !open)
|
|
193
|
+
return;
|
|
194
|
+
const dialog = event.currentTarget;
|
|
195
|
+
if (!dismissible) {
|
|
196
|
+
showDialog(dialog);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
requestOpenChange(false);
|
|
200
|
+
};
|
|
201
|
+
const handlePointerDown = (event) => {
|
|
202
|
+
onPointerDown?.(event);
|
|
203
|
+
pressStartedOnBackdrop.current = event.target === event.currentTarget;
|
|
204
|
+
};
|
|
205
|
+
const handleClick = (event) => {
|
|
206
|
+
onClick?.(event);
|
|
207
|
+
const startedOnBackdrop = pressStartedOnBackdrop.current;
|
|
208
|
+
pressStartedOnBackdrop.current = false;
|
|
209
|
+
if (event.defaultPrevented || event.target !== event.currentTarget)
|
|
210
|
+
return;
|
|
211
|
+
// A press that began inside (selecting text) and ended over the
|
|
212
|
+
// backdrop is not a dismissal. `detail === 0` is a keyboard/synthetic
|
|
213
|
+
// click, which has no pointerdown to compare against.
|
|
214
|
+
if (!startedOnBackdrop && event.detail !== 0)
|
|
215
|
+
return;
|
|
216
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
217
|
+
const inside = rect.width > 0 &&
|
|
218
|
+
event.clientX >= rect.left &&
|
|
219
|
+
event.clientX <= rect.right &&
|
|
220
|
+
event.clientY >= rect.top &&
|
|
221
|
+
event.clientY <= rect.bottom;
|
|
222
|
+
if (!inside)
|
|
223
|
+
requestOpenChange(false);
|
|
224
|
+
};
|
|
225
|
+
return (_jsx("dialog", { ref: dialogRef, "data-slot": "sheet", "data-side": side, "data-state": open ? 'open' : 'closed', "aria-modal": "true", "aria-labelledby": sheet.titleId, "aria-describedby": sheet.descriptionId, onCancel: handleCancel, onClose: handleClose, onClick: handleClick, onPointerDown: handlePointerDown, onKeyDown: (event) => {
|
|
226
|
+
onKeyDown?.(event);
|
|
227
|
+
// Handle Escape here (preventing it also stops the native `cancel`),
|
|
228
|
+
// so it behaves the same in every engine, jsdom included.
|
|
229
|
+
if (!event.defaultPrevented && event.key === 'Escape') {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
requestOpenChange(false);
|
|
232
|
+
}
|
|
233
|
+
}, className: cn(
|
|
234
|
+
// Reset the UA dialog box, then place it ourselves.
|
|
235
|
+
'fixed m-0 max-h-[90dvh] overflow-y-auto border-0 bg-background p-0 text-sm text-foreground shadow-lg outline-none',
|
|
236
|
+
// A scrim, not a theme colour: it must darken in light AND dark mode.
|
|
237
|
+
'backdrop:bg-black/10', 'opacity-100 transition-opacity duration-200 starting:opacity-0 motion-reduce:transition-none', SIDE_CLASSES[side], className), ...props, children: open ? (_jsx("div", { "data-slot": "sheet-body", className: cn('relative p-6', BODY_CLASSES[side], bodyClassName), children: children })) : null }));
|
|
238
|
+
}
|
|
239
|
+
function SheetTitle({ className, id, ...props }) {
|
|
240
|
+
const sheet = useSheetContext('Title');
|
|
241
|
+
const { registerTitle } = sheet;
|
|
242
|
+
const resolvedId = id ?? sheet.defaultTitleId;
|
|
243
|
+
useLayoutEffect(() => {
|
|
244
|
+
registerTitle(resolvedId);
|
|
245
|
+
return () => registerTitle(undefined);
|
|
246
|
+
}, [registerTitle, resolvedId]);
|
|
247
|
+
return (_jsx("h2", { "data-slot": "sheet-title", id: resolvedId, className: cn('text-lg font-semibold', className), ...props }));
|
|
248
|
+
}
|
|
249
|
+
function SheetDescription({ className, id, ...props }) {
|
|
250
|
+
const sheet = useSheetContext('Description');
|
|
251
|
+
const { registerDescription } = sheet;
|
|
252
|
+
const resolvedId = id ?? sheet.defaultDescriptionId;
|
|
253
|
+
useLayoutEffect(() => {
|
|
254
|
+
registerDescription(resolvedId);
|
|
255
|
+
return () => registerDescription(undefined);
|
|
256
|
+
}, [registerDescription, resolvedId]);
|
|
257
|
+
return (_jsx("p", { "data-slot": "sheet-description", id: resolvedId, className: cn('text-muted-foreground', className), ...props }));
|
|
258
|
+
}
|
|
259
|
+
function SheetClose({ render, onClick, children, ...props }) {
|
|
260
|
+
const sheet = useSheetContext('Close');
|
|
261
|
+
const handleClick = (event) => {
|
|
262
|
+
onClick?.(event);
|
|
263
|
+
if (event.defaultPrevented)
|
|
264
|
+
return;
|
|
265
|
+
stopAnchorNavigation(event);
|
|
266
|
+
sheet.requestOpenChange(false);
|
|
267
|
+
};
|
|
268
|
+
const closeProps = { ...props, 'data-slot': 'sheet-close', onClick: handleClick };
|
|
269
|
+
if (render) {
|
|
270
|
+
return renderElement(render, {
|
|
271
|
+
...hostDefaults(render),
|
|
272
|
+
...closeProps,
|
|
273
|
+
...(children == null ? {} : { children }),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return (_jsx("button", { type: "button", ...closeProps, children: children }));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* A modal sheet on the native `<dialog>` element: a bottom sheet on phones and
|
|
280
|
+
* a centred dialog from `md` up. `showModal()` gives the top layer, an inert
|
|
281
|
+
* background and a Tab trap for free; the component adds controlled state,
|
|
282
|
+
* Escape / backdrop dismissal, scroll lock and focus return to the trigger.
|
|
283
|
+
* The entry fade is skipped under `prefers-reduced-motion`.
|
|
284
|
+
*
|
|
285
|
+
* ```tsx
|
|
286
|
+
* <Sheet>
|
|
287
|
+
* <Sheet.Trigger>Add child</Sheet.Trigger>
|
|
288
|
+
* <Sheet.Content>
|
|
289
|
+
* <Sheet.Title>Add child</Sheet.Title>
|
|
290
|
+
* <Sheet.Description>Only for this booking.</Sheet.Description>
|
|
291
|
+
* …
|
|
292
|
+
* <Sheet.Close aria-label="Close">×</Sheet.Close>
|
|
293
|
+
* </Sheet.Content>
|
|
294
|
+
* </Sheet>
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export const Sheet = Object.assign(SheetRoot, {
|
|
298
|
+
Trigger: SheetTrigger,
|
|
299
|
+
Content: SheetContent,
|
|
300
|
+
Title: SheetTitle,
|
|
301
|
+
Description: SheetDescription,
|
|
302
|
+
Close: SheetClose,
|
|
303
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
export interface TextareaProps extends ComponentProps<'textarea'> {
|
|
3
|
+
/**
|
|
4
|
+
* Marks the field invalid: sets `aria-invalid="true"` and the destructive
|
|
5
|
+
* border/ring. Pair it with `aria-describedby` pointing at the error text.
|
|
6
|
+
* An explicit `aria-invalid` prop still wins.
|
|
7
|
+
*/
|
|
8
|
+
invalid?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Multi-line text input matching `Input`. Grows with its content where the
|
|
12
|
+
* browser supports `field-sizing: content`, starting at `min-h-16`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function Textarea({ invalid, className, 'aria-invalid': ariaInvalid, ...props }: TextareaProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Multi-line text input matching `Input`. Grows with its content where the
|
|
6
|
+
* browser supports `field-sizing: content`, starting at `min-h-16`.
|
|
7
|
+
*/
|
|
8
|
+
export function Textarea({ invalid = false, className, 'aria-invalid': ariaInvalid, ...props }) {
|
|
9
|
+
const resolvedInvalid = ariaInvalid ?? (invalid || undefined);
|
|
10
|
+
return (_jsx("textarea", { "data-slot": "textarea", "data-invalid": resolvedInvalid === true || resolvedInvalid === 'true' ? '' : undefined, "aria-invalid": resolvedInvalid, className: cn(
|
|
11
|
+
// text-base below `sm` keeps iOS Safari from zooming on focus.
|
|
12
|
+
'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/30 sm:text-sm', className), ...props }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react';
|
|
2
|
+
export type ToggleGroupSize = 'sm' | 'md';
|
|
3
|
+
export type ToggleGroupOrientation = 'horizontal' | 'vertical';
|
|
4
|
+
interface ToggleGroupBaseProps extends Omit<ComponentProps<'div'>, 'defaultValue' | 'onChange' | 'role'> {
|
|
5
|
+
size?: ToggleGroupSize;
|
|
6
|
+
orientation?: ToggleGroupOrientation;
|
|
7
|
+
/** Disables every item. */
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ToggleGroupSingleProps extends ToggleGroupBaseProps {
|
|
11
|
+
/**
|
|
12
|
+
* `single` (default) behaves like a radio group: exactly one chip can be
|
|
13
|
+
* chosen, arrow keys move focus AND selection (WAI-ARIA radio pattern), and
|
|
14
|
+
* the chosen chip cannot be un-chosen by clicking it again.
|
|
15
|
+
*/
|
|
16
|
+
type?: 'single';
|
|
17
|
+
value?: string | null;
|
|
18
|
+
defaultValue?: string | null;
|
|
19
|
+
onValueChange?: (value: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface ToggleGroupMultipleProps extends ToggleGroupBaseProps {
|
|
22
|
+
/**
|
|
23
|
+
* `multiple` is a group of independent toggle buttons (`aria-pressed`);
|
|
24
|
+
* arrow keys only move focus, Space / Enter toggles.
|
|
25
|
+
*/
|
|
26
|
+
type: 'multiple';
|
|
27
|
+
value?: string[];
|
|
28
|
+
defaultValue?: string[];
|
|
29
|
+
onValueChange?: (value: string[]) => void;
|
|
30
|
+
}
|
|
31
|
+
export type ToggleGroupProps = ToggleGroupSingleProps | ToggleGroupMultipleProps;
|
|
32
|
+
export interface ToggleGroupItemProps extends Omit<ComponentProps<'button'>, 'value'> {
|
|
33
|
+
value: string;
|
|
34
|
+
}
|
|
35
|
+
declare function ToggleGroupRoot(props: ToggleGroupProps): import("react").JSX.Element;
|
|
36
|
+
declare function ToggleGroupItem({ value, disabled: itemDisabled, className, children, onClick, onKeyDown, onFocus, ...props }: ToggleGroupItemProps): import("react").JSX.Element;
|
|
37
|
+
/**
|
|
38
|
+
* Chip-style single- or multi-select. `type="single"` (default) is a
|
|
39
|
+
* `radiogroup`; `type="multiple"` is a `group` of `aria-pressed` toggles.
|
|
40
|
+
* Both use a roving tabindex (one Tab stop) with Arrow / Home / End keys.
|
|
41
|
+
* Give the group an accessible name via `aria-label` or `aria-labelledby`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ToggleGroup: typeof ToggleGroupRoot & {
|
|
44
|
+
Item: typeof ToggleGroupItem;
|
|
45
|
+
};
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
const ToggleGroupContext = createContext(null);
|
|
6
|
+
function useToggleGroupContext() {
|
|
7
|
+
const context = useContext(ToggleGroupContext);
|
|
8
|
+
if (!context)
|
|
9
|
+
throw new Error('ToggleGroup.Item must be rendered inside <ToggleGroup>.');
|
|
10
|
+
return context;
|
|
11
|
+
}
|
|
12
|
+
const ITEM_SELECTOR = '[data-slot="toggle-group-item"]';
|
|
13
|
+
const NEXT_KEYS = {
|
|
14
|
+
horizontal: ['ArrowRight', 'ArrowDown'],
|
|
15
|
+
vertical: ['ArrowDown', 'ArrowRight'],
|
|
16
|
+
};
|
|
17
|
+
const PREV_KEYS = {
|
|
18
|
+
horizontal: ['ArrowLeft', 'ArrowUp'],
|
|
19
|
+
vertical: ['ArrowUp', 'ArrowLeft'],
|
|
20
|
+
};
|
|
21
|
+
const SIZE_CLASSES = {
|
|
22
|
+
sm: 'h-8 px-3 text-xs [&_svg]:size-3.5',
|
|
23
|
+
md: 'h-10 px-4 text-sm [&_svg]:size-4',
|
|
24
|
+
};
|
|
25
|
+
function toArray(value) {
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value;
|
|
28
|
+
return value == null ? [] : [value];
|
|
29
|
+
}
|
|
30
|
+
function ToggleGroupRoot(props) {
|
|
31
|
+
const { type = 'single', value, defaultValue, onValueChange, size = 'md', orientation = 'horizontal', disabled = false, className, children, ...rest } = props;
|
|
32
|
+
const rootRef = useRef(null);
|
|
33
|
+
const [uncontrolled, setUncontrolled] = useState(() => toArray(defaultValue));
|
|
34
|
+
const isControlled = value !== undefined;
|
|
35
|
+
const selected = isControlled ? toArray(value) : uncontrolled;
|
|
36
|
+
const [focused, setFocused] = useState(null);
|
|
37
|
+
const [tabbable, setTabbable] = useState(undefined);
|
|
38
|
+
const enabledItems = useCallback(() => Array.from(rootRef.current?.querySelectorAll(ITEM_SELECTOR) ?? []).filter((item) => !item.disabled), []);
|
|
39
|
+
// Roving tabindex: exactly one enabled item is in the tab order — the last
|
|
40
|
+
// focused one, else the (first) selected one, else the first enabled one.
|
|
41
|
+
// Measured from the DOM after each render so nested / conditional items
|
|
42
|
+
// just work; the state update bails out when nothing changed.
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
const values = enabledItems().map((item) => item.dataset.value ?? '');
|
|
45
|
+
const next = [focused, ...selected].find((v) => v != null && values.includes(v)) ?? values[0];
|
|
46
|
+
setTabbable(next ?? null);
|
|
47
|
+
});
|
|
48
|
+
const commit = (next) => {
|
|
49
|
+
if (!isControlled)
|
|
50
|
+
setUncontrolled(next);
|
|
51
|
+
if (type === 'multiple') {
|
|
52
|
+
onValueChange?.(next);
|
|
53
|
+
}
|
|
54
|
+
else if (next[0] !== undefined) {
|
|
55
|
+
onValueChange?.(next[0]);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const toggle = (itemValue) => {
|
|
59
|
+
if (type === 'multiple') {
|
|
60
|
+
commit(selected.includes(itemValue)
|
|
61
|
+
? selected.filter((v) => v !== itemValue)
|
|
62
|
+
: [...selected, itemValue]);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Radio semantics: re-choosing the current value is a no-op.
|
|
66
|
+
if (selected[0] !== itemValue)
|
|
67
|
+
commit([itemValue]);
|
|
68
|
+
};
|
|
69
|
+
const onItemKeyDown = (event) => {
|
|
70
|
+
const items = enabledItems();
|
|
71
|
+
const index = items.indexOf(event.currentTarget);
|
|
72
|
+
if (index === -1 || items.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
let target;
|
|
75
|
+
if (NEXT_KEYS[orientation].includes(event.key))
|
|
76
|
+
target = items[(index + 1) % items.length];
|
|
77
|
+
else if (PREV_KEYS[orientation].includes(event.key))
|
|
78
|
+
target = items[(index - 1 + items.length) % items.length];
|
|
79
|
+
else if (event.key === 'Home')
|
|
80
|
+
target = items[0];
|
|
81
|
+
else if (event.key === 'End')
|
|
82
|
+
target = items[items.length - 1];
|
|
83
|
+
if (!target)
|
|
84
|
+
return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
target.focus();
|
|
87
|
+
const targetValue = target.dataset.value ?? '';
|
|
88
|
+
setFocused(targetValue);
|
|
89
|
+
if (type === 'single')
|
|
90
|
+
toggle(targetValue);
|
|
91
|
+
};
|
|
92
|
+
// `aria-orientation` is only defined for radiogroup, not for group.
|
|
93
|
+
const groupSemantics = type === 'single'
|
|
94
|
+
? {
|
|
95
|
+
role: 'radiogroup',
|
|
96
|
+
'aria-orientation': orientation,
|
|
97
|
+
'aria-disabled': disabled || undefined,
|
|
98
|
+
}
|
|
99
|
+
: { role: 'group', 'aria-disabled': disabled || undefined };
|
|
100
|
+
const context = {
|
|
101
|
+
type,
|
|
102
|
+
size,
|
|
103
|
+
orientation,
|
|
104
|
+
disabled,
|
|
105
|
+
selected,
|
|
106
|
+
tabbable,
|
|
107
|
+
toggle,
|
|
108
|
+
focusItem: setFocused,
|
|
109
|
+
onItemKeyDown,
|
|
110
|
+
};
|
|
111
|
+
return (_jsx(ToggleGroupContext.Provider, { value: context, children: _jsx("div", { ref: rootRef, ...groupSemantics, "data-slot": "toggle-group", "data-type": type, "data-orientation": orientation, className: cn('flex flex-wrap gap-2', orientation === 'vertical' && 'flex-col items-stretch', className), ...rest, children: children }) }));
|
|
112
|
+
}
|
|
113
|
+
function ToggleGroupItem({ value, disabled: itemDisabled, className, children, onClick, onKeyDown, onFocus, ...props }) {
|
|
114
|
+
const group = useToggleGroupContext();
|
|
115
|
+
const isOn = group.selected.includes(value);
|
|
116
|
+
const disabled = group.disabled || Boolean(itemDisabled);
|
|
117
|
+
// Before the group has measured (SSR / first paint) keep the selected item
|
|
118
|
+
// — or every item when nothing is selected — reachable by Tab.
|
|
119
|
+
const inTabOrder = group.tabbable === undefined ? group.selected.length === 0 || isOn : group.tabbable === value;
|
|
120
|
+
const semantics = group.type === 'single'
|
|
121
|
+
? { role: 'radio', 'aria-checked': isOn }
|
|
122
|
+
: { 'aria-pressed': isOn };
|
|
123
|
+
return (_jsx("button", { type: "button", "data-slot": "toggle-group-item", "data-value": value, "data-state": isOn ? 'on' : 'off', disabled: disabled, tabIndex: inTabOrder ? 0 : -1, ...semantics, className: cn('inline-flex shrink-0 select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-full border font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', isOn
|
|
124
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
125
|
+
: 'border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground', SIZE_CLASSES[group.size], className), onClick: (event) => {
|
|
126
|
+
onClick?.(event);
|
|
127
|
+
if (event.defaultPrevented)
|
|
128
|
+
return;
|
|
129
|
+
group.focusItem(value);
|
|
130
|
+
group.toggle(value);
|
|
131
|
+
}, onKeyDown: (event) => {
|
|
132
|
+
onKeyDown?.(event);
|
|
133
|
+
if (!event.defaultPrevented)
|
|
134
|
+
group.onItemKeyDown(event);
|
|
135
|
+
}, onFocus: (event) => {
|
|
136
|
+
onFocus?.(event);
|
|
137
|
+
group.focusItem(value);
|
|
138
|
+
}, ...props, children: children }));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Chip-style single- or multi-select. `type="single"` (default) is a
|
|
142
|
+
* `radiogroup`; `type="multiple"` is a `group` of `aria-pressed` toggles.
|
|
143
|
+
* Both use a roving tabindex (one Tab stop) with Arrow / Home / End keys.
|
|
144
|
+
* Give the group an accessible name via `aria-label` or `aria-labelledby`.
|
|
145
|
+
*/
|
|
146
|
+
export const ToggleGroup = Object.assign(ToggleGroupRoot, {
|
|
147
|
+
Item: ToggleGroupItem,
|
|
148
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* @medalsocial/meda/styles/base.css
|
|
2
|
+
*
|
|
3
|
+
* Lean foundation for consumers that render only a few meda surfaces:
|
|
4
|
+
* canonical design tokens + the Tailwind theme bridge + the base layer.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately does NOT `@source` meda's component output and does NOT pull
|
|
7
|
+
* in `@xyflow/react`'s stylesheet. Pair it with the per-feature stylesheets
|
|
8
|
+
* for the surfaces you render, e.g.
|
|
9
|
+
*
|
|
10
|
+
* @import "tailwindcss";
|
|
11
|
+
* @import "@medalsocial/meda/styles/base.css";
|
|
12
|
+
* @import "@medalsocial/meda/primitives/styles.css";
|
|
13
|
+
* @import "@medalsocial/meda/calendar/styles.css";
|
|
14
|
+
*
|
|
15
|
+
* `@medalsocial/meda/styles.css` (theme.css) remains the batteries-included
|
|
16
|
+
* entry: base + every feature, including workflow-builder.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@import "./tokens.css";
|
|
20
|
+
@import "./bridge.css";
|
|
21
|
+
|
|
22
|
+
@theme inline {
|
|
23
|
+
/* Font families — Geist + Geist Mono only (no Inter) */
|
|
24
|
+
--font-sans: var(--font-family-sans);
|
|
25
|
+
--font-mono: var(--font-family-mono);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@layer base {
|
|
29
|
+
:root {
|
|
30
|
+
color-scheme: light;
|
|
31
|
+
--auth-gradient-primary: var(--color-brand-500);
|
|
32
|
+
--auth-gradient-secondary: var(--color-brand-700);
|
|
33
|
+
--auth-gradient-base: var(--color-brand-800);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.dark,
|
|
37
|
+
[data-theme="dark"] {
|
|
38
|
+
color-scheme: dark;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* @medalsocial/meda/booking/styles.css
|
|
2
|
+
*
|
|
3
|
+
* Generates only the Tailwind utilities used by `@medalsocial/meda/booking`
|
|
4
|
+
* (the booking screens). The screens are built on meda primitives, so import
|
|
5
|
+
* `@medalsocial/meda/primitives/styles.css` too. Requires the theme bridge:
|
|
6
|
+
* import `@medalsocial/meda/styles/bridge.css` (your own shadcn tokens) or
|
|
7
|
+
* `@medalsocial/meda/styles/base.css` (meda tokens) once, before this file.
|
|
8
|
+
* Paths resolve relative to dist/styles/.
|
|
9
|
+
*/
|
|
10
|
+
@source "../booking/**/*.js";
|