@medalsocial/meda 3.0.0 → 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 +36 -2
- 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/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 +8 -0
- package/dist/primitives/index.js +4 -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/styles/booking.css +10 -0
- package/package.json +8 -1
- package/skills/components/SKILL.md +53 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
export interface CheckboxProps extends Omit<ComponentProps<'input'>, 'type' | 'size'> {
|
|
3
|
+
/**
|
|
4
|
+
* Called with the new checked state. Fires alongside `onChange`, so a form
|
|
5
|
+
* can use whichever it prefers.
|
|
6
|
+
*/
|
|
7
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Marks the box invalid: sets `aria-invalid="true"` and the destructive
|
|
10
|
+
* border. An explicit `aria-invalid` prop still wins.
|
|
11
|
+
*/
|
|
12
|
+
invalid?: boolean;
|
|
13
|
+
/** Class for the wrapping `<span>` (the input itself takes `className`). */
|
|
14
|
+
rootClassName?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A native `<input type="checkbox">` drawn as a 16 px box with a check mark.
|
|
18
|
+
* Native on purpose: it submits with a form, works with `<label htmlFor>`,
|
|
19
|
+
* answers Space without JavaScript, and costs no dialog/portal machinery.
|
|
20
|
+
* The input carries `data-slot="checkbox"` and a `data-invalid` hook; style
|
|
21
|
+
* the checked state with `checked:` / `peer-checked:`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function Checkbox({ className, rootClassName, invalid, onCheckedChange, onChange, checked, defaultChecked, 'aria-invalid': ariaInvalid, ...props }: CheckboxProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* A native `<input type="checkbox">` drawn as a 16 px box with a check mark.
|
|
7
|
+
* Native on purpose: it submits with a form, works with `<label htmlFor>`,
|
|
8
|
+
* answers Space without JavaScript, and costs no dialog/portal machinery.
|
|
9
|
+
* The input carries `data-slot="checkbox"` and a `data-invalid` hook; style
|
|
10
|
+
* the checked state with `checked:` / `peer-checked:`.
|
|
11
|
+
*/
|
|
12
|
+
export function Checkbox({ className, rootClassName, invalid = false, onCheckedChange, onChange, checked, defaultChecked, 'aria-invalid': ariaInvalid, ...props }) {
|
|
13
|
+
const resolvedInvalid = ariaInvalid ?? (invalid || undefined);
|
|
14
|
+
return (_jsxs("span", { "data-slot": "checkbox-root", className: cn('relative inline-flex size-4 shrink-0', rootClassName), children: [_jsx("input", { type: "checkbox", "data-slot": "checkbox", "data-invalid": resolvedInvalid === true || resolvedInvalid === 'true' ? '' : undefined, "aria-invalid": resolvedInvalid, checked: checked, defaultChecked: defaultChecked, onChange: (event) => {
|
|
15
|
+
onChange?.(event);
|
|
16
|
+
onCheckedChange?.(event.currentTarget.checked);
|
|
17
|
+
}, className: cn(
|
|
18
|
+
// Same focus / invalid treatment as Input, so the two sit together.
|
|
19
|
+
'peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-input bg-background transition-colors outline-none checked:border-primary checked:bg-primary focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/30 disabled:cursor-not-allowed disabled:opacity-50', className), ...props }), _jsx(Check, { "aria-hidden": "true", "data-slot": "checkbox-indicator", className: "pointer-events-none absolute inset-0 m-auto size-3.5 text-primary-foreground opacity-0 peer-checked:opacity-100" })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
export type FieldOrientation = 'vertical' | 'horizontal';
|
|
3
|
+
export interface FieldProps extends ComponentProps<'div'> {
|
|
4
|
+
/**
|
|
5
|
+
* `vertical` (default) stacks label, control, description and error.
|
|
6
|
+
* `horizontal` puts a checkbox/switch beside its label.
|
|
7
|
+
*/
|
|
8
|
+
orientation?: FieldOrientation;
|
|
9
|
+
/**
|
|
10
|
+
* Dims the label. A disabled control inside the field does this on its
|
|
11
|
+
* own (`:has(:disabled)`); the prop is for fields whose control cannot be.
|
|
12
|
+
*/
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export type FieldLabelProps = ComponentProps<'label'>;
|
|
16
|
+
export type FieldDescriptionProps = ComponentProps<'p'>;
|
|
17
|
+
export type FieldSetProps = ComponentProps<'fieldset'>;
|
|
18
|
+
export type FieldLegendProps = ComponentProps<'legend'>;
|
|
19
|
+
export interface FieldErrorProps extends ComponentProps<'p'> {
|
|
20
|
+
/**
|
|
21
|
+
* Several messages at once (e.g. from a schema validator). Each renders as
|
|
22
|
+
* its own `<p data-slot="field-error">` inside one wrapper that takes `id`
|
|
23
|
+
* and the other props; duplicates are shown once.
|
|
24
|
+
*/
|
|
25
|
+
errors?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
declare function FieldRoot({ orientation, disabled, className, ...props }: FieldProps): import("react").JSX.Element;
|
|
28
|
+
declare function FieldLabel({ className, ...props }: FieldLabelProps): import("react").JSX.Element;
|
|
29
|
+
declare function FieldDescription({ className, ...props }: FieldDescriptionProps): import("react").JSX.Element;
|
|
30
|
+
declare function FieldError({ className, children, errors, ...props }: FieldErrorProps): import("react").JSX.Element | null;
|
|
31
|
+
declare function FieldSet({ className, ...props }: FieldSetProps): import("react").JSX.Element;
|
|
32
|
+
declare function FieldLegend({ className, ...props }: FieldLegendProps): import("react").JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* Form field layout: `Field` groups a control with `Field.Label`,
|
|
35
|
+
* `Field.Description` and `Field.Error`. It owns no state and no ids — wire
|
|
36
|
+
* `htmlFor` / `aria-describedby` yourself, so the markup stays obvious and
|
|
37
|
+
* works with any form library. `Field.Set` + `Field.Legend` group related
|
|
38
|
+
* fields (radio sets, consent boxes).
|
|
39
|
+
*/
|
|
40
|
+
export declare const Field: typeof FieldRoot & {
|
|
41
|
+
Label: typeof FieldLabel;
|
|
42
|
+
Description: typeof FieldDescription;
|
|
43
|
+
Error: typeof FieldError;
|
|
44
|
+
Set: typeof FieldSet;
|
|
45
|
+
Legend: typeof FieldLegend;
|
|
46
|
+
};
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
const ORIENTATION_CLASSES = {
|
|
5
|
+
vertical: '',
|
|
6
|
+
horizontal: 'grid-cols-[auto_1fr] items-center',
|
|
7
|
+
};
|
|
8
|
+
function FieldRoot({ orientation = 'vertical', disabled, className, ...props }) {
|
|
9
|
+
return (_jsx("div", { "data-slot": "field", "data-orientation": orientation, "data-disabled": disabled ? 'true' : undefined, className: cn('group/field grid gap-2', ORIENTATION_CLASSES[orientation], className), ...props }));
|
|
10
|
+
}
|
|
11
|
+
function FieldLabel({ className, ...props }) {
|
|
12
|
+
return (_jsx("label", { "data-slot": "field-label", className: cn('flex select-none items-center gap-2 text-sm font-medium leading-none group-has-disabled/field:cursor-not-allowed group-has-disabled/field:opacity-50 group-data-[disabled=true]/field:pointer-events-none group-data-[disabled=true]/field:opacity-50', className), ...props }));
|
|
13
|
+
}
|
|
14
|
+
function FieldDescription({ className, ...props }) {
|
|
15
|
+
return (_jsx("p", { "data-slot": "field-description", className: cn('text-sm text-muted-foreground', className), ...props }));
|
|
16
|
+
}
|
|
17
|
+
function FieldError({ className, children, errors, ...props }) {
|
|
18
|
+
const unique = errors ? Array.from(new Set(errors)) : [];
|
|
19
|
+
if (unique.length > 0) {
|
|
20
|
+
// One element owns the id (and every other prop), so `aria-describedby`
|
|
21
|
+
// pointing at it reads out all of the messages.
|
|
22
|
+
const { id, ...rest } = props;
|
|
23
|
+
return (_jsx("div", { id: id, "data-slot": "field-errors", className: "flex flex-col gap-1", ...rest, children: unique.map((error) => (_jsx("p", { "data-slot": "field-error", className: cn('text-sm text-destructive', className), children: error }, error))) }));
|
|
24
|
+
}
|
|
25
|
+
if (children == null || children === false)
|
|
26
|
+
return null;
|
|
27
|
+
return (_jsx("p", { "data-slot": "field-error", className: cn('text-sm text-destructive', className), ...props, children: children }));
|
|
28
|
+
}
|
|
29
|
+
function FieldSet({ className, ...props }) {
|
|
30
|
+
return _jsx("fieldset", { "data-slot": "field-set", className: cn('grid gap-6', className), ...props });
|
|
31
|
+
}
|
|
32
|
+
function FieldLegend({ className, ...props }) {
|
|
33
|
+
return (_jsx("legend", { "data-slot": "field-legend", className: cn('text-base font-medium leading-none text-foreground', className), ...props }));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Form field layout: `Field` groups a control with `Field.Label`,
|
|
37
|
+
* `Field.Description` and `Field.Error`. It owns no state and no ids — wire
|
|
38
|
+
* `htmlFor` / `aria-describedby` yourself, so the markup stays obvious and
|
|
39
|
+
* works with any form library. `Field.Set` + `Field.Legend` group related
|
|
40
|
+
* fields (radio sets, consent boxes).
|
|
41
|
+
*/
|
|
42
|
+
export const Field = Object.assign(FieldRoot, {
|
|
43
|
+
Label: FieldLabel,
|
|
44
|
+
Description: FieldDescription,
|
|
45
|
+
Error: FieldError,
|
|
46
|
+
Set: FieldSet,
|
|
47
|
+
Legend: FieldLegend,
|
|
48
|
+
});
|
|
@@ -4,15 +4,23 @@ export type { ButtonProps, ButtonSize, ButtonVariant } from './button.js';
|
|
|
4
4
|
export { Button } from './button.js';
|
|
5
5
|
export type { CardBodyProps, CardFooterProps, CardHeaderProps, CardProps, } from './card.js';
|
|
6
6
|
export { Card } from './card.js';
|
|
7
|
+
export type { CheckboxProps } from './checkbox.js';
|
|
8
|
+
export { Checkbox } from './checkbox.js';
|
|
7
9
|
export type { EmptyStateProps, EmptyStateVariant } from './empty-state.js';
|
|
8
10
|
export { EmptyState } from './empty-state.js';
|
|
11
|
+
export type { FieldDescriptionProps, FieldErrorProps, FieldLabelProps, FieldLegendProps, FieldOrientation, FieldProps, FieldSetProps, } from './field.js';
|
|
12
|
+
export { Field } from './field.js';
|
|
9
13
|
export type { FilterRailGroupProps, FilterRailProps } from './filter-rail.js';
|
|
10
14
|
export { FilterRail } from './filter-rail.js';
|
|
11
15
|
export type { InputProps } from './input.js';
|
|
12
16
|
export { Input } from './input.js';
|
|
17
|
+
export type { SheetCloseProps, SheetContentProps, SheetProps, SheetSide, SheetTriggerProps, } from './sheet.js';
|
|
18
|
+
export { Sheet } from './sheet.js';
|
|
13
19
|
export type { SkeletonProps } from './skeleton.js';
|
|
14
20
|
export { Skeleton } from './skeleton.js';
|
|
15
21
|
export type { StatusPillProps, StatusPillSize, StatusPillTone } from './status-pill.js';
|
|
16
22
|
export { StatusPill } from './status-pill.js';
|
|
23
|
+
export type { TextareaProps } from './textarea.js';
|
|
24
|
+
export { Textarea } from './textarea.js';
|
|
17
25
|
export type { ToggleGroupItemProps, ToggleGroupMultipleProps, ToggleGroupOrientation, ToggleGroupProps, ToggleGroupSingleProps, ToggleGroupSize, } from './toggle-group.js';
|
|
18
26
|
export { ToggleGroup } from './toggle-group.js';
|
package/dist/primitives/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export { Avatar, getInitials } from './avatar.js';
|
|
2
2
|
export { Button } from './button.js';
|
|
3
3
|
export { Card } from './card.js';
|
|
4
|
+
export { Checkbox } from './checkbox.js';
|
|
4
5
|
export { EmptyState } from './empty-state.js';
|
|
6
|
+
export { Field } from './field.js';
|
|
5
7
|
export { FilterRail } from './filter-rail.js';
|
|
6
8
|
export { Input } from './input.js';
|
|
9
|
+
export { Sheet } from './sheet.js';
|
|
7
10
|
export { Skeleton } from './skeleton.js';
|
|
8
11
|
export { StatusPill } from './status-pill.js';
|
|
12
|
+
export { Textarea } from './textarea.js';
|
|
9
13
|
export { ToggleGroup } from './toggle-group.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import { type RenderElement } from '../lib/render-element.js';
|
|
3
|
+
export type SheetSide = 'responsive' | 'bottom';
|
|
4
|
+
export interface SheetProps {
|
|
5
|
+
/** Controlled open state. */
|
|
6
|
+
open?: boolean;
|
|
7
|
+
/** Initial open state when uncontrolled. */
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
/** Called with the requested state (trigger, close button, Escape, backdrop). */
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
/**
|
|
12
|
+
* `false` ignores every close request (Escape, backdrop, `Sheet.Close`,
|
|
13
|
+
* a native `dialog.close()`) — use it while a submit is in flight so the
|
|
14
|
+
* sheet cannot vanish under the user. Default `true`.
|
|
15
|
+
*/
|
|
16
|
+
dismissible?: boolean;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export interface SheetTriggerProps extends ComponentProps<'button'> {
|
|
20
|
+
/** Render as another element, merging the trigger's props onto it. */
|
|
21
|
+
render?: RenderElement<ButtonHTMLAttributes<HTMLElement>>;
|
|
22
|
+
}
|
|
23
|
+
export interface SheetContentProps extends Omit<ComponentProps<'dialog'>, 'open'> {
|
|
24
|
+
/**
|
|
25
|
+
* `responsive` (default): a bottom sheet on phones, a centred dialog from
|
|
26
|
+
* `md` up. `bottom` keeps the bottom sheet at every width.
|
|
27
|
+
*/
|
|
28
|
+
side?: SheetSide;
|
|
29
|
+
/** Class for the padded inner wrapper that holds the children. */
|
|
30
|
+
bodyClassName?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SheetCloseProps extends ComponentProps<'button'> {
|
|
33
|
+
/** Render as another element (e.g. a meda `Button`), merging close behaviour onto it. */
|
|
34
|
+
render?: RenderElement<ButtonHTMLAttributes<HTMLElement>>;
|
|
35
|
+
}
|
|
36
|
+
declare function SheetRoot({ open: openProp, defaultOpen, onOpenChange, dismissible, children, }: SheetProps): import("react").JSX.Element;
|
|
37
|
+
declare function SheetTrigger({ render, onClick, className, children, ...props }: SheetTriggerProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
38
|
+
declare function SheetContent({ side, className, bodyClassName, children, onCancel, onClick, onClose, onKeyDown, onPointerDown, ...props }: SheetContentProps): import("react").JSX.Element;
|
|
39
|
+
declare function SheetTitle({ className, id, ...props }: ComponentProps<'h2'>): import("react").JSX.Element;
|
|
40
|
+
declare function SheetDescription({ className, id, ...props }: ComponentProps<'p'>): import("react").JSX.Element;
|
|
41
|
+
declare function SheetClose({ render, onClick, children, ...props }: SheetCloseProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* A modal sheet on the native `<dialog>` element: a bottom sheet on phones and
|
|
44
|
+
* a centred dialog from `md` up. `showModal()` gives the top layer, an inert
|
|
45
|
+
* background and a Tab trap for free; the component adds controlled state,
|
|
46
|
+
* Escape / backdrop dismissal, scroll lock and focus return to the trigger.
|
|
47
|
+
* The entry fade is skipped under `prefers-reduced-motion`.
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <Sheet>
|
|
51
|
+
* <Sheet.Trigger>Add child</Sheet.Trigger>
|
|
52
|
+
* <Sheet.Content>
|
|
53
|
+
* <Sheet.Title>Add child</Sheet.Title>
|
|
54
|
+
* <Sheet.Description>Only for this booking.</Sheet.Description>
|
|
55
|
+
* …
|
|
56
|
+
* <Sheet.Close aria-label="Close">×</Sheet.Close>
|
|
57
|
+
* </Sheet.Content>
|
|
58
|
+
* </Sheet>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare const Sheet: typeof SheetRoot & {
|
|
62
|
+
Trigger: typeof SheetTrigger;
|
|
63
|
+
Content: typeof SheetContent;
|
|
64
|
+
Title: typeof SheetTitle;
|
|
65
|
+
Description: typeof SheetDescription;
|
|
66
|
+
Close: typeof SheetClose;
|
|
67
|
+
};
|
|
68
|
+
export {};
|
|
@@ -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,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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medalsocial/meda",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Shared Meda UI shell and runtime package.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
"types": "./dist/calendar/index.d.ts",
|
|
64
64
|
"import": "./dist/calendar/index.js"
|
|
65
65
|
},
|
|
66
|
+
"./booking": {
|
|
67
|
+
"types": "./dist/booking/index.d.ts",
|
|
68
|
+
"default": "./dist/booking/index.js"
|
|
69
|
+
},
|
|
66
70
|
"./workflow-builder": {
|
|
67
71
|
"types": "./dist/workflow-builder/index.d.ts",
|
|
68
72
|
"import": "./dist/workflow-builder/index.js"
|
|
@@ -116,6 +120,9 @@
|
|
|
116
120
|
"./calendar/styles.css": {
|
|
117
121
|
"default": "./dist/styles/calendar.css"
|
|
118
122
|
},
|
|
123
|
+
"./booking/styles.css": {
|
|
124
|
+
"default": "./dist/styles/booking.css"
|
|
125
|
+
},
|
|
119
126
|
"./primitives/styles.css": {
|
|
120
127
|
"default": "./dist/styles/primitives.css"
|
|
121
128
|
},
|