@mediquo/medicore 0.3.0 → 0.3.1
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/components/empty-state/empty-state.d.ts +1 -1
- package/dist/components/empty-state/empty-state.js +10 -1
- package/dist/components/empty-state/empty-state.js.map +1 -1
- package/dist/components/fact-card/fact-card.js +3 -3
- package/dist/components/fact-card/fact-card.js.map +1 -1
- package/dist/components/link/link.js +1 -1
- package/dist/components/link/link.js.map +1 -1
- package/dist/components/menu/menu.d.ts +1 -1
- package/dist/components/menu/menu.js +2 -2
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/modal/modal.js +2 -2
- package/dist/components/modal/modal.js.map +1 -1
- package/dist/components/phone-input/phone-input.js +1 -1
- package/dist/components/phone-input/phone-input.js.map +1 -1
- package/dist/components/select/select.js +2 -2
- package/dist/components/select/select.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ export interface EmptyStateProps extends Omit<ComponentProps<"div">, "title"> {
|
|
|
16
16
|
* no "sube tus informes" if documents can't be uploaded.
|
|
17
17
|
*/
|
|
18
18
|
description?: ReactNode;
|
|
19
|
-
/** A MediCore icon
|
|
19
|
+
/** A MediCore icon, decorative, inside a brand `IconContainer`. `calendar` for appointments. */
|
|
20
20
|
icon?: ReactNode;
|
|
21
21
|
/**
|
|
22
22
|
* The next step, a `Button` with the closed naming ("Pedir cita", never "Nueva
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cva } from "class-variance-authority";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { IconContainer } from "../icon-container/icon-container.js";
|
|
4
5
|
const emptyStateVariants = cva(
|
|
5
6
|
"flex w-full flex-col items-center text-center",
|
|
6
7
|
{
|
|
@@ -33,7 +34,15 @@ function EmptyState({
|
|
|
33
34
|
className: cn(emptyStateVariants({ size }), className),
|
|
34
35
|
...props,
|
|
35
36
|
children: [
|
|
36
|
-
icon && /* @__PURE__ */ jsx(
|
|
37
|
+
icon && /* @__PURE__ */ jsx(
|
|
38
|
+
IconContainer,
|
|
39
|
+
{
|
|
40
|
+
"aria-hidden": true,
|
|
41
|
+
icon,
|
|
42
|
+
tone: "brand",
|
|
43
|
+
size: size === "compact" ? 40 : 48
|
|
44
|
+
}
|
|
45
|
+
),
|
|
37
46
|
/* @__PURE__ */ jsx(
|
|
38
47
|
Title,
|
|
39
48
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"empty-state.js","sources":["../../../src/components/empty-state/empty-state.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport const emptyStateVariants = cva(\n \"flex w-full flex-col items-center text-center\",\n {\n variants: {\n size: {\n default: \"gap-4 p-12\",\n compact: \"gap-2 p-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n },\n);\n\nexport type EmptyStateSize = \"default\" | \"compact\";\n\nexport type EmptyStateHeadingLevel = 2 | 3 | 4 | 5 | 6;\n\nexport interface EmptyStateProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n /**\n * Describes, never blames: \"Todavía no tienes citas\", not \"No has pedido ninguna\n * cita\".\n */\n title: ReactNode;\n /**\n * What will happen and when this fills up (\"Cuando pidas una cita aparecerá aquí\n * con la hora y el profesional que te atiende\"). Never promise what doesn't exist:\n * no \"sube tus informes\" if documents can't be uploaded.\n */\n description?: ReactNode;\n /** A MediCore icon
|
|
1
|
+
{"version":3,"file":"empty-state.js","sources":["../../../src/components/empty-state/empty-state.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { IconContainer } from \"../icon-container\";\n\nexport const emptyStateVariants = cva(\n \"flex w-full flex-col items-center text-center\",\n {\n variants: {\n size: {\n default: \"gap-4 p-12\",\n compact: \"gap-2 p-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n },\n);\n\nexport type EmptyStateSize = \"default\" | \"compact\";\n\nexport type EmptyStateHeadingLevel = 2 | 3 | 4 | 5 | 6;\n\nexport interface EmptyStateProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n /**\n * Describes, never blames: \"Todavía no tienes citas\", not \"No has pedido ninguna\n * cita\".\n */\n title: ReactNode;\n /**\n * What will happen and when this fills up (\"Cuando pidas una cita aparecerá aquí\n * con la hora y el profesional que te atiende\"). Never promise what doesn't exist:\n * no \"sube tus informes\" if documents can't be uploaded.\n */\n description?: ReactNode;\n /** A MediCore icon, decorative, inside a brand `IconContainer`. `calendar` for appointments. */\n icon?: ReactNode;\n /**\n * The next step, a `Button` with the closed naming (\"Pedir cita\", never \"Nueva\n * cita\"). Leave it out only when there really is none (a history that fills up\n * after the first consultation) and let the description say when it will appear.\n */\n action?: ReactNode;\n /**\n * `default` for a whole page. `compact` inside a card or a column: less air and a\n * Body/Default Strong title, because an h4 look inside a card competes with the\n * card's own title.\n */\n size?: EmptyStateSize;\n /** Renders the title as h2–h6. Without it the title is a paragraph. */\n headingLevel?: EmptyStateHeadingLevel;\n}\n\n/**\n * Empty state. It is NOT a sign saying \"there's nothing\": it's where the product\n * explains what will happen and offers the next step — empty space turned into\n * real use or education.\n *\n * Three things it can't do:\n * - Promise what doesn't exist.\n * - Blame the person (\"No has pedido…\" accuses; \"Todavía no tienes…\" describes).\n * - Leave no way out: if there is a sensible action, it goes here.\n *\n * An empty state is NOT an error state. If the list is empty because the request\n * failed, show an error with a retry that says something failed — mixing them makes\n * people think they lost their data.\n *\n * Its text is centred, the only time this system centres text: a short, isolated\n * block with nothing around to align with.\n */\nexport function EmptyState({\n title,\n description,\n icon,\n action,\n size = \"default\",\n headingLevel,\n className,\n ...props\n}: EmptyStateProps): React.JSX.Element {\n const Title = headingLevel ? (`h${headingLevel}` as const) : \"p\";\n\n return (\n <div\n data-slot=\"empty-state\"\n className={cn(emptyStateVariants({ size }), className)}\n {...props}\n >\n {icon && (\n <IconContainer\n aria-hidden\n icon={icon}\n tone=\"brand\"\n size={size === \"compact\" ? 40 : 48}\n />\n )}\n <Title\n className={cn(\n \"max-w-sm text-text-primary\",\n size === \"compact\" ? \"text-body-default-strong\" : \"text-heading-h4\",\n )}\n >\n {title}\n </Title>\n {description && (\n <p className=\"max-w-sm text-body-small text-text-secondary\">\n {description}\n </p>\n )}\n {action && <div className=\"flex justify-center\">{action}</div>}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;AAMO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,SAAS;AAAA,MAAA;AAAA,IACX;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAqDO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuC;AACrC,QAAM,QAAQ,eAAgB,IAAI,YAAY,KAAe;AAE7D,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,mBAAmB,EAAE,KAAA,CAAM,GAAG,SAAS;AAAA,MACpD,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA,QACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAW;AAAA,YACX;AAAA,YACA,MAAK;AAAA,YACL,MAAM,SAAS,YAAY,KAAK;AAAA,UAAA;AAAA,QAAA;AAAA,QAGpC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,SAAS,YAAY,6BAA6B;AAAA,YAAA;AAAA,YAGnD,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAEF,eACC,oBAAC,KAAA,EAAE,WAAU,gDACV,UAAA,aACH;AAAA,QAED,UAAU,oBAAC,OAAA,EAAI,WAAU,uBAAuB,UAAA,OAAA,CAAO;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG9D;"}
|
|
@@ -11,9 +11,9 @@ const factCardVariants = cva(
|
|
|
11
11
|
variants: {
|
|
12
12
|
tone: {
|
|
13
13
|
neutral: "border-border-subtle bg-background",
|
|
14
|
-
success: "border-
|
|
15
|
-
warning: "border-
|
|
16
|
-
error: "border-
|
|
14
|
+
success: "border-transparent bg-success-subtle",
|
|
15
|
+
warning: "border-transparent bg-warning-subtle",
|
|
16
|
+
error: "border-transparent bg-error-subtle"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
defaultVariants: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fact-card.js","sources":["../../../src/components/fact-card/fact-card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport {\n AlertCircleIcon,\n AlertTriangleIcon,\n CheckCircleIcon,\n InfoCircleIcon,\n} from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\n\nexport const factCardVariants = cva(\n \"flex min-w-0 items-start gap-3 rounded-xl border p-4\",\n {\n variants: {\n tone: {\n neutral: \"border-border-subtle bg-background\",\n success: \"border-
|
|
1
|
+
{"version":3,"file":"fact-card.js","sources":["../../../src/components/fact-card/fact-card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport {\n AlertCircleIcon,\n AlertTriangleIcon,\n CheckCircleIcon,\n InfoCircleIcon,\n} from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\n\nexport const factCardVariants = cva(\n \"flex min-w-0 items-start gap-3 rounded-xl border p-4\",\n {\n variants: {\n tone: {\n neutral: \"border-border-subtle bg-background\",\n success: \"border-transparent bg-success-subtle\",\n warning: \"border-transparent bg-warning-subtle\",\n error: \"border-transparent bg-error-subtle\",\n },\n },\n defaultVariants: {\n tone: \"neutral\",\n },\n },\n);\n\nconst iconClassName: Record<FactCardTone, string> = {\n neutral: \"text-text-secondary\",\n success: \"text-text-success\",\n warning: \"text-text-warning\",\n error: \"text-text-error\",\n};\n\nconst defaultIcon: Record<FactCardTone, ReactNode> = {\n neutral: <InfoCircleIcon />,\n success: <CheckCircleIcon />,\n warning: <AlertTriangleIcon />,\n error: <AlertCircleIcon />,\n};\n\nexport type FactCardTone = \"neutral\" | \"success\" | \"warning\" | \"error\";\n\nexport interface FactCardProps\n extends Omit<ComponentProps<\"div\">, \"title\">,\n VariantProps<typeof factCardVariants> {\n /**\n * The fact, in a few words: \"Datos de facturación completos\", \"Falta el NIF\".\n * On `error`, say what it blocks: \"Falta el NIF: no se puede cerrar el mes\".\n */\n title: ReactNode;\n /** The detail: \"Laura Gómez Ruiz · NIF 12345678Z · IBAN ···· 1332\". */\n description?: ReactNode;\n /** One way to act on it, a `Link` or a small `Button`. */\n action?: ReactNode;\n /** Replaces the tone's icon. */\n icon?: ReactNode;\n /**\n * `success` when the fact is settled, `warning` when it limits something,\n * `error` when it blocks the main action, `neutral` for plain information.\n */\n tone?: FactCardTone;\n}\n\n/**\n * A fact about the record that decides what can be done with it, next to its figures\n * in a `DetailHeader`: the invoicing data before a close, a missing consent, a pending\n * document. It stays as long as the fact does.\n *\n * Unlike `Banner`, it is a card among cards, not a notice across the page, and it is\n * never dismissible. Text in text-primary on every tone; the icon carries the tone,\n * so the meaning is in the words. `error` is announced with `role=\"status\"`.\n */\nexport function FactCard({\n title,\n description,\n action,\n icon,\n tone = \"neutral\",\n className,\n ...props\n}: FactCardProps): React.JSX.Element {\n return (\n <div\n data-slot=\"fact-card\"\n role={tone === \"error\" ? \"status\" : undefined}\n className={cn(factCardVariants({ tone }), className)}\n {...props}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n \"mt-0.5 flex shrink-0 [&_svg]:size-icon-md\",\n iconClassName[tone],\n )}\n >\n {icon ?? defaultIcon[tone]}\n </span>\n <div className=\"flex min-w-0 flex-col gap-1\">\n <p className=\"text-label-strong text-text-primary\">{title}</p>\n {description && (\n <p\n className={cn(\n \"text-body-small\",\n tone === \"neutral\" ? \"text-text-secondary\" : \"text-text-primary\",\n )}\n >\n {description}\n </p>\n )}\n {action && <div className=\"pt-1\">{action}</div>}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;AAWO,MAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAEA,MAAM,gBAA8C;AAAA,EAClD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEA,MAAM,cAA+C;AAAA,EACnD,6BAAU,gBAAA,EAAe;AAAA,EACzB,6BAAU,iBAAA,EAAgB;AAAA,EAC1B,6BAAU,mBAAA,EAAkB;AAAA,EAC5B,2BAAQ,iBAAA,CAAA,CAAgB;AAC1B;AAkCO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAqC;AACnC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAM,SAAS,UAAU,WAAW;AAAA,MACpC,WAAW,GAAG,iBAAiB,EAAE,KAAA,CAAM,GAAG,SAAS;AAAA,MAClD,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,cAAc,IAAI;AAAA,YAAA;AAAA,YAGnB,UAAA,QAAQ,YAAY,IAAI;AAAA,UAAA;AAAA,QAAA;AAAA,QAE3B,qBAAC,OAAA,EAAI,WAAU,+BACb,UAAA;AAAA,UAAA,oBAAC,KAAA,EAAE,WAAU,uCAAuC,UAAA,OAAM;AAAA,UACzD,eACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,SAAS,YAAY,wBAAwB;AAAA,cAAA;AAAA,cAG9C,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGJ,UAAU,oBAAC,OAAA,EAAI,WAAU,QAAQ,UAAA,OAAA,CAAO;AAAA,QAAA,EAAA,CAC3C;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -19,7 +19,7 @@ const linkVariants = cva(
|
|
|
19
19
|
},
|
|
20
20
|
disabled: {
|
|
21
21
|
true: "cursor-not-allowed text-text-disabled",
|
|
22
|
-
false: "cursor-pointer text-text-
|
|
22
|
+
false: "cursor-pointer text-text-brand decoration-border hover:decoration-current"
|
|
23
23
|
},
|
|
24
24
|
withIcon: {
|
|
25
25
|
true: "inline-flex items-center gap-1",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.js","sources":["../../../src/components/link/link.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva } from \"class-variance-authority\";\nimport type { MouseEvent, ReactNode } from \"react\";\n\nimport { LinkExternal01Icon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\n\nexport type LinkSize = \"default\" | \"large\";\n\nexport const linkVariants = cva(\n [\n // Always underlined: colour can't be the only cue that it is a link (WCAG 1.4.1).\n \"rounded-xs underline underline-offset-2 focus-ring\",\n \"[&_svg]:shrink-0\",\n ],\n {\n variants: {\n // Follows the body ramp: an inline link measures the same as the text around it.\n size: {\n default: \"text-body-small [&_svg]:size-icon-sm\",\n large: \"text-body-default [&_svg]:size-icon-md\",\n },\n disabled: {\n true: \"cursor-not-allowed text-text-disabled\",\n false
|
|
1
|
+
{"version":3,"file":"link.js","sources":["../../../src/components/link/link.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva } from \"class-variance-authority\";\nimport type { MouseEvent, ReactNode } from \"react\";\n\nimport { LinkExternal01Icon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\n\nexport type LinkSize = \"default\" | \"large\";\n\nexport const linkVariants = cva(\n [\n // Always underlined: colour can't be the only cue that it is a link (WCAG 1.4.1).\n \"rounded-xs underline underline-offset-2 focus-ring\",\n \"[&_svg]:shrink-0\",\n ],\n {\n variants: {\n // Follows the body ramp: an inline link measures the same as the text around it.\n size: {\n default: \"text-body-small [&_svg]:size-icon-sm\",\n large: \"text-body-default [&_svg]:size-icon-md\",\n },\n disabled: {\n true: \"cursor-not-allowed text-text-disabled\",\n false:\n \"cursor-pointer text-text-brand decoration-border hover:decoration-current\",\n },\n withIcon: {\n true: \"inline-flex items-center gap-1\",\n false: \"\",\n },\n standalone: {\n true: \"touch-target\",\n false: \"\",\n },\n },\n defaultVariants: {\n size: \"default\",\n disabled: false,\n withIcon: false,\n standalone: false,\n },\n },\n);\n\nexport interface LinkState extends Record<string, unknown> {\n disabled: boolean;\n}\n\nexport interface LinkProps\n extends Omit<useRender.ComponentProps<\"a\", LinkState>, \"className\"> {\n size?: LinkSize;\n /**\n * Not focusable and not clickable. An `<a>` can't be disabled natively, so the\n * `href` is dropped and it is exposed as `aria-disabled`. With `render`, pass the\n * router's own disabled prop too.\n */\n disabled?: boolean;\n /** Icon after the label, e.g. `<ArrowRightIcon />`. `external` puts `link-external-01` here. */\n trailingIcon?: ReactNode;\n /**\n * Opens in a new tab (`target=\"_blank\"`, `rel=\"noopener noreferrer\"`) and shows the\n * external icon. For documents: privacy policy, terms, a PDF.\n */\n external?: boolean;\n /**\n * Screen-reader text appended when `external`, e.g. \"(se abre en una pestaña nueva)\":\n * the icon alone isn't announced.\n */\n newTabLabel?: string;\n /**\n * On its own row, not inside running text: extends the hit area to 44px. Inside a\n * paragraph it must stay off (it would overlap the lines around it).\n */\n standalone?: boolean;\n className?: string;\n}\n\nconst preventNavigation = (event: MouseEvent<HTMLAnchorElement>): void => {\n event.preventDefault();\n};\n\n/**\n * Text link. A link NAVIGATES; a button ACTS (`Button`). Renders an `<a>`; pass\n * `render` to use the router's link, e.g. `render={<RouterLink to=\"/citas\" />}`.\n *\n * - Always underlined. Only on a row of its own, never inside a paragraph, can a\n * screen drop the underline (`className=\"no-underline\"`): there the position already\n * says it is a link.\n * - `default` (14) and `large` (16) follow the body text; if the paragraph uses\n * another size, it isn't this component but text styled as a link.\n * - A standalone link that is the main action of a block is a `Button` with\n * `variant=\"ghost\"`, not a Link.\n */\nexport function Link({\n size,\n disabled = false,\n trailingIcon,\n external = false,\n newTabLabel,\n standalone = false,\n className,\n render,\n href,\n children,\n ...props\n}: LinkProps): React.JSX.Element {\n const icon = trailingIcon ?? (external ? <LinkExternal01Icon /> : undefined);\n\n const defaultProps: useRender.ElementProps<\"a\"> & { \"data-slot\": string } = {\n \"data-slot\": \"link\",\n className: cn(\n linkVariants({\n size,\n disabled,\n withIcon: icon !== undefined,\n standalone,\n }),\n className,\n ),\n // Only when there is one: an explicit `href: undefined` would reach (and could\n // override) the href a router link computes.\n ...(href !== undefined && !disabled ? { href } : {}),\n ...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {}),\n children: (\n <>\n {children}\n {external && newTabLabel && (\n <span className=\"sr-only\"> {newTabLabel}</span>\n )}\n {icon}\n </>\n ),\n };\n\n // Last, so they win over whatever came in `props`.\n const disabledProps: useRender.ElementProps<\"a\"> = disabled\n ? { role: \"link\", \"aria-disabled\": true, onClick: preventNavigation }\n : {};\n\n return useRender({\n defaultTagName: \"a\",\n render,\n state: { disabled },\n props: mergeProps<\"a\">(defaultProps, props, disabledProps),\n });\n}\n"],"names":[],"mappings":";;;;;;AAUO,MAAM,eAAe;AAAA,EAC1B;AAAA;AAAA,IAEE;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA;AAAA,MAER,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,MAET,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OACE;AAAA,MAAA;AAAA,MAEJ,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,MAET,YAAY;AAAA,QACV,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,MACN,UAAU;AAAA,MACV,UAAU;AAAA,MACV,YAAY;AAAA,IAAA;AAAA,EACd;AAEJ;AAmCA,MAAM,oBAAoB,CAAC,UAA+C;AACxE,QAAM,eAAA;AACR;AAcO,SAAS,KAAK;AAAA,EACnB;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiC;AAC/B,QAAM,OAAO,iBAAiB,WAAW,oBAAC,sBAAmB,IAAK;AAElE,QAAM,eAAsE;AAAA,IAC1E,aAAa;AAAA,IACb,WAAW;AAAA,MACT,aAAa;AAAA,QACX;AAAA,QACA;AAAA,QACA,UAAU,SAAS;AAAA,QACnB;AAAA,MAAA,CACD;AAAA,MACD;AAAA,IAAA;AAAA;AAAA;AAAA,IAIF,GAAI,SAAS,UAAa,CAAC,WAAW,EAAE,KAAA,IAAS,CAAA;AAAA,IACjD,GAAI,WAAW,EAAE,QAAQ,UAAU,KAAK,sBAAA,IAA0B,CAAA;AAAA,IAClE,UACE,qBAAA,UAAA,EACG,UAAA;AAAA,MAAA;AAAA,MACA,YAAY,eACX,qBAAC,QAAA,EAAK,WAAU,WAAU,UAAA;AAAA,QAAA;AAAA,QAAE;AAAA,MAAA,GAAY;AAAA,MAEzC;AAAA,IAAA,EAAA,CACH;AAAA,EAAA;AAKJ,QAAM,gBAA6C,WAC/C,EAAE,MAAM,QAAQ,iBAAiB,MAAM,SAAS,kBAAA,IAChD,CAAA;AAEJ,SAAO,UAAU;AAAA,IACf,gBAAgB;AAAA,IAChB;AAAA,IACA,OAAO,EAAE,SAAA;AAAA,IACT,OAAO,WAAgB,cAAc,OAAO,aAAa;AAAA,EAAA,CAC1D;AACH;"}
|
|
@@ -55,7 +55,7 @@ export interface MenuContentProps {
|
|
|
55
55
|
/** Sheet only: accessible name of the sheet. Defaults to the `moreOptions` label. */
|
|
56
56
|
label?: string;
|
|
57
57
|
}
|
|
58
|
-
/** The list of options. 240px popover with `shadow-
|
|
58
|
+
/** The list of options. 240px popover with `shadow-lg` and a border, or a bottom sheet. */
|
|
59
59
|
export declare function MenuContent({ children, className, side, align, label, }: MenuContentProps): React.JSX.Element;
|
|
60
60
|
export interface MenuItemProps {
|
|
61
61
|
/** The option's label. */
|
|
@@ -117,7 +117,7 @@ function MenuContent({
|
|
|
117
117
|
className: cn(
|
|
118
118
|
// The shadow separates it from the content below; the border, where the
|
|
119
119
|
// shadow doesn't show (dark backgrounds, poor screens).
|
|
120
|
-
"flex w-60 flex-col rounded-
|
|
120
|
+
"flex w-60 flex-col rounded-xl border border-border-subtle bg-background p-1.5 shadow-lg outline-hidden",
|
|
121
121
|
"origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out",
|
|
122
122
|
"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0",
|
|
123
123
|
"motion-reduce:transition-none",
|
|
@@ -131,7 +131,7 @@ function MenuContent({
|
|
|
131
131
|
}
|
|
132
132
|
const menuItemVariants = cva(
|
|
133
133
|
[
|
|
134
|
-
"flex w-full shrink-0 cursor-pointer select-none items-center gap-3 rounded-
|
|
134
|
+
"flex w-full shrink-0 cursor-pointer select-none items-center gap-3 rounded-lg px-3 text-left text-label-default",
|
|
135
135
|
"outline-hidden focus-visible:outline-2 focus-visible:outline-border-focus focus-visible:outline-solid focus-visible:-outline-offset-2",
|
|
136
136
|
"[&_svg]:size-icon-md [&_svg]:shrink-0",
|
|
137
137
|
"data-disabled:cursor-not-allowed data-disabled:text-text-disabled disabled:cursor-not-allowed disabled:text-text-disabled"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.js","sources":["../../../src/components/menu/menu.tsx"],"sourcesContent":["import { Menu as BaseMenu } from \"@base-ui/react/menu\";\nimport { cva } from \"class-variance-authority\";\nimport {\n type CSSProperties,\n createContext,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n useContext,\n} from \"react\";\n\nimport { DotsHorizontalIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { Divider } from \"../divider\";\nimport { IconButton } from \"../icon-button\";\nimport {\n Modal,\n type ModalChangeEventDetails,\n ModalClose,\n ModalContent,\n ModalTrigger,\n} from \"../modal/modal\";\nimport { DESKTOP_MEDIA_QUERY, useMediaQuery } from \"../modal/use-media-query\";\n\n/**\n * `popover`: the floating 240px menu next to the trigger. `sheet`: a bottom sheet\n * (`Modal layout=\"sheet\"`) with the same options in 48px rows.\n */\nexport type MenuLayout = \"popover\" | \"sheet\";\n\nexport type MenuChangeEventDetails =\n | BaseMenu.Root.ChangeEventDetails\n | ModalChangeEventDetails;\n\nconst MenuLayoutContext = createContext<MenuLayout | null>(null);\n\nfunction useMenuLayout(part: string): MenuLayout {\n const layout = useContext(MenuLayoutContext);\n if (layout === null) {\n throw new Error(`MediCore: <${part}> must be used inside <Menu>.`);\n }\n return layout;\n}\n\nexport interface MenuProps {\n /**\n * Leave it unset and it follows the screen: a sheet below `md` (a 240px menu next\n * to the thumb covers what the person is looking at) and a popover from `md` up.\n */\n layout?: MenuLayout;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean, eventDetails: MenuChangeEventDetails) => void;\n /** Disables the whole menu (popover only). */\n disabled?: boolean;\n children?: ReactNode;\n}\n\n/**\n * Contextual menu: what the \"⋯\" of a Card, an appointment or the chat header opens.\n * It is where everything that isn't the block's single primary action lives.\n *\n * Up to four options and, if there is one, the destructive option LAST, after a\n * `MenuSeparator` — never in the middle, where a thumb going down the list would\n * cross \"Cancelar cita\" on its way to \"Compartir\". If you need more than five, those\n * things deserve their own screen.\n *\n * Keyboard: arrow keys move between options, Enter picks, Escape closes and focus\n * returns to the trigger.\n */\nexport function Menu({\n layout,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n children,\n}: MenuProps): React.JSX.Element {\n const isDesktop = useMediaQuery(DESKTOP_MEDIA_QUERY);\n const resolvedLayout: MenuLayout =\n layout ?? (isDesktop ? \"popover\" : \"sheet\");\n\n return (\n <MenuLayoutContext.Provider value={resolvedLayout}>\n {resolvedLayout === \"sheet\" ? (\n <Modal\n layout=\"sheet\"\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n >\n {children}\n </Modal>\n ) : (\n <BaseMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n disabled={disabled}\n >\n {children}\n </BaseMenu.Root>\n )}\n </MenuLayoutContext.Provider>\n );\n}\n\nexport interface MenuTriggerProps\n extends Omit<\n BaseMenu.Trigger.Props,\n | \"className\"\n | \"style\"\n | \"render\"\n | \"handle\"\n | \"payload\"\n | \"openOnHover\"\n | \"delay\"\n | \"closeDelay\"\n > {\n className?: string;\n style?: CSSProperties;\n /** Your own trigger element. Without `render` and `children` you get the \"⋯\" button. */\n render?: ReactElement;\n}\n\n/**\n * Opens the menu. With no `children` or `render` it is the \"⋯\" ghost icon button,\n * named with the MediCore `moreOptions` label — pass `aria-label` to be more\n * specific (\"Opciones de la cita\").\n */\nexport function MenuTrigger({\n render,\n children,\n \"aria-label\": ariaLabel,\n ...props\n}: MenuTriggerProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuTrigger\");\n const labels = useMediCoreLabels();\n const Trigger = layout === \"sheet\" ? ModalTrigger : BaseMenu.Trigger;\n\n if (render === undefined && children === undefined) {\n return (\n <Trigger\n data-slot=\"menu-trigger\"\n render={\n <IconButton\n variant=\"ghost\"\n icon={<DotsHorizontalIcon />}\n aria-label={ariaLabel ?? labels.moreOptions}\n />\n }\n {...props}\n />\n );\n }\n\n return (\n <Trigger\n data-slot=\"menu-trigger\"\n aria-label={ariaLabel}\n render={render}\n {...props}\n >\n {children}\n </Trigger>\n );\n}\n\nexport interface MenuContentProps {\n children?: ReactNode;\n className?: string;\n /** Popover only: which side of the trigger it opens on. Flips when there is no room. */\n side?: \"top\" | \"bottom\";\n /** Popover only. `end` by default: the \"⋯\" usually sits at the right edge. */\n align?: \"start\" | \"center\" | \"end\";\n /** Sheet only: accessible name of the sheet. Defaults to the `moreOptions` label. */\n label?: string;\n}\n\n/** The list of options. 240px popover with `shadow-md` and a border, or a bottom sheet. */\nexport function MenuContent({\n children,\n className,\n side = \"bottom\",\n align = \"end\",\n label,\n}: MenuContentProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuContent\");\n const labels = useMediCoreLabels();\n\n if (layout === \"sheet\") {\n return (\n <ModalContent aria-label={label ?? labels.moreOptions}>\n <div\n data-slot=\"menu-content\"\n className={cn(\"-mx-3 flex flex-col\", className)}\n >\n {children}\n </div>\n </ModalContent>\n );\n }\n\n return (\n <BaseMenu.Portal>\n <BaseMenu.Positioner\n side={side}\n align={align}\n sideOffset={4}\n className=\"z-50 outline-hidden\"\n >\n <BaseMenu.Popup\n data-slot=\"menu-content\"\n className={cn(\n // The shadow separates it from the content below; the border, where the\n // shadow doesn't show (dark backgrounds, poor screens).\n \"flex w-60 flex-col rounded-lg border border-border-subtle bg-background p-1 shadow-md outline-hidden\",\n \"origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out\",\n \"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0\",\n \"motion-reduce:transition-none\",\n className,\n )}\n >\n {children}\n </BaseMenu.Popup>\n </BaseMenu.Positioner>\n </BaseMenu.Portal>\n );\n}\n\nconst menuItemVariants = cva(\n [\n \"flex w-full shrink-0 cursor-pointer select-none items-center gap-3 rounded-md px-3 text-left text-label-default\",\n \"outline-hidden focus-visible:outline-2 focus-visible:outline-border-focus focus-visible:outline-solid focus-visible:-outline-offset-2\",\n \"[&_svg]:size-icon-md [&_svg]:shrink-0\",\n \"data-disabled:cursor-not-allowed data-disabled:text-text-disabled disabled:cursor-not-allowed disabled:text-text-disabled\",\n ],\n {\n variants: {\n type: {\n default: \"text-text-primary\",\n destructive: \"text-text-error\",\n },\n layout: {\n popover: \"h-control-md\",\n sheet: \"h-12\",\n },\n },\n // Base UI marks the pointer/keyboard position with data-highlighted in the popover;\n // the sheet's rows are plain buttons, so they use :hover.\n compoundVariants: [\n {\n type: \"default\",\n layout: \"popover\",\n className: \"not-data-disabled:data-highlighted:bg-surface\",\n },\n {\n type: \"destructive\",\n layout: \"popover\",\n className: \"not-data-disabled:data-highlighted:bg-error-subtle\",\n },\n {\n type: \"default\",\n layout: \"sheet\",\n className: \"enabled:hover:bg-surface\",\n },\n {\n type: \"destructive\",\n layout: \"sheet\",\n className: \"enabled:hover:bg-error-subtle\",\n },\n ],\n defaultVariants: {\n type: \"default\",\n layout: \"popover\",\n },\n },\n);\n\nexport interface MenuItemProps {\n /** The option's label. */\n children: ReactNode;\n /** 20px icon before the label. */\n icon?: ReactNode;\n /** `destructive` goes in text-error and always last, after a `MenuSeparator`. */\n type?: \"default\" | \"destructive\";\n disabled?: boolean;\n onClick?: (event: MouseEvent<HTMLElement>) => void;\n /** Closes the menu after the click (default). */\n closeOnClick?: boolean;\n className?: string;\n}\n\n/** One option. 40px tall in the popover (48 in the sheet) so neighbours aren't tapped by mistake. */\nexport function MenuItem({\n children,\n icon,\n type = \"default\",\n disabled = false,\n onClick,\n closeOnClick = true,\n className,\n}: MenuItemProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuItem\");\n const classes = cn(menuItemVariants({ type, layout }), className);\n\n if (layout === \"sheet\") {\n return (\n <ModalClose\n data-slot=\"menu-item\"\n data-type={type}\n disabled={disabled}\n className={classes}\n onClick={(event) => {\n onClick?.(event);\n if (!closeOnClick) {\n event.preventBaseUIHandler();\n }\n }}\n >\n {icon}\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n </ModalClose>\n );\n }\n\n return (\n <BaseMenu.Item\n data-slot=\"menu-item\"\n data-type={type}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={onClick}\n className={classes}\n >\n {icon}\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n </BaseMenu.Item>\n );\n}\n\nexport interface MenuSeparatorProps {\n className?: string;\n}\n\n/** The line before the destructive option. */\nexport function MenuSeparator({\n className,\n}: MenuSeparatorProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuSeparator\");\n\n if (layout === \"sheet\") {\n return <Divider className={cn(\"my-1\", className)} />;\n }\n\n return (\n <BaseMenu.Separator\n data-slot=\"menu-separator\"\n className={cn(\"my-1 h-px shrink-0 bg-border-subtle\", className)}\n />\n );\n}\n"],"names":["BaseMenu"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"menu.js","sources":["../../../src/components/menu/menu.tsx"],"sourcesContent":["import { Menu as BaseMenu } from \"@base-ui/react/menu\";\nimport { cva } from \"class-variance-authority\";\nimport {\n type CSSProperties,\n createContext,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n useContext,\n} from \"react\";\n\nimport { DotsHorizontalIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { Divider } from \"../divider\";\nimport { IconButton } from \"../icon-button\";\nimport {\n Modal,\n type ModalChangeEventDetails,\n ModalClose,\n ModalContent,\n ModalTrigger,\n} from \"../modal/modal\";\nimport { DESKTOP_MEDIA_QUERY, useMediaQuery } from \"../modal/use-media-query\";\n\n/**\n * `popover`: the floating 240px menu next to the trigger. `sheet`: a bottom sheet\n * (`Modal layout=\"sheet\"`) with the same options in 48px rows.\n */\nexport type MenuLayout = \"popover\" | \"sheet\";\n\nexport type MenuChangeEventDetails =\n | BaseMenu.Root.ChangeEventDetails\n | ModalChangeEventDetails;\n\nconst MenuLayoutContext = createContext<MenuLayout | null>(null);\n\nfunction useMenuLayout(part: string): MenuLayout {\n const layout = useContext(MenuLayoutContext);\n if (layout === null) {\n throw new Error(`MediCore: <${part}> must be used inside <Menu>.`);\n }\n return layout;\n}\n\nexport interface MenuProps {\n /**\n * Leave it unset and it follows the screen: a sheet below `md` (a 240px menu next\n * to the thumb covers what the person is looking at) and a popover from `md` up.\n */\n layout?: MenuLayout;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean, eventDetails: MenuChangeEventDetails) => void;\n /** Disables the whole menu (popover only). */\n disabled?: boolean;\n children?: ReactNode;\n}\n\n/**\n * Contextual menu: what the \"⋯\" of a Card, an appointment or the chat header opens.\n * It is where everything that isn't the block's single primary action lives.\n *\n * Up to four options and, if there is one, the destructive option LAST, after a\n * `MenuSeparator` — never in the middle, where a thumb going down the list would\n * cross \"Cancelar cita\" on its way to \"Compartir\". If you need more than five, those\n * things deserve their own screen.\n *\n * Keyboard: arrow keys move between options, Enter picks, Escape closes and focus\n * returns to the trigger.\n */\nexport function Menu({\n layout,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n children,\n}: MenuProps): React.JSX.Element {\n const isDesktop = useMediaQuery(DESKTOP_MEDIA_QUERY);\n const resolvedLayout: MenuLayout =\n layout ?? (isDesktop ? \"popover\" : \"sheet\");\n\n return (\n <MenuLayoutContext.Provider value={resolvedLayout}>\n {resolvedLayout === \"sheet\" ? (\n <Modal\n layout=\"sheet\"\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n >\n {children}\n </Modal>\n ) : (\n <BaseMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n disabled={disabled}\n >\n {children}\n </BaseMenu.Root>\n )}\n </MenuLayoutContext.Provider>\n );\n}\n\nexport interface MenuTriggerProps\n extends Omit<\n BaseMenu.Trigger.Props,\n | \"className\"\n | \"style\"\n | \"render\"\n | \"handle\"\n | \"payload\"\n | \"openOnHover\"\n | \"delay\"\n | \"closeDelay\"\n > {\n className?: string;\n style?: CSSProperties;\n /** Your own trigger element. Without `render` and `children` you get the \"⋯\" button. */\n render?: ReactElement;\n}\n\n/**\n * Opens the menu. With no `children` or `render` it is the \"⋯\" ghost icon button,\n * named with the MediCore `moreOptions` label — pass `aria-label` to be more\n * specific (\"Opciones de la cita\").\n */\nexport function MenuTrigger({\n render,\n children,\n \"aria-label\": ariaLabel,\n ...props\n}: MenuTriggerProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuTrigger\");\n const labels = useMediCoreLabels();\n const Trigger = layout === \"sheet\" ? ModalTrigger : BaseMenu.Trigger;\n\n if (render === undefined && children === undefined) {\n return (\n <Trigger\n data-slot=\"menu-trigger\"\n render={\n <IconButton\n variant=\"ghost\"\n icon={<DotsHorizontalIcon />}\n aria-label={ariaLabel ?? labels.moreOptions}\n />\n }\n {...props}\n />\n );\n }\n\n return (\n <Trigger\n data-slot=\"menu-trigger\"\n aria-label={ariaLabel}\n render={render}\n {...props}\n >\n {children}\n </Trigger>\n );\n}\n\nexport interface MenuContentProps {\n children?: ReactNode;\n className?: string;\n /** Popover only: which side of the trigger it opens on. Flips when there is no room. */\n side?: \"top\" | \"bottom\";\n /** Popover only. `end` by default: the \"⋯\" usually sits at the right edge. */\n align?: \"start\" | \"center\" | \"end\";\n /** Sheet only: accessible name of the sheet. Defaults to the `moreOptions` label. */\n label?: string;\n}\n\n/** The list of options. 240px popover with `shadow-lg` and a border, or a bottom sheet. */\nexport function MenuContent({\n children,\n className,\n side = \"bottom\",\n align = \"end\",\n label,\n}: MenuContentProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuContent\");\n const labels = useMediCoreLabels();\n\n if (layout === \"sheet\") {\n return (\n <ModalContent aria-label={label ?? labels.moreOptions}>\n <div\n data-slot=\"menu-content\"\n className={cn(\"-mx-3 flex flex-col\", className)}\n >\n {children}\n </div>\n </ModalContent>\n );\n }\n\n return (\n <BaseMenu.Portal>\n <BaseMenu.Positioner\n side={side}\n align={align}\n sideOffset={4}\n className=\"z-50 outline-hidden\"\n >\n <BaseMenu.Popup\n data-slot=\"menu-content\"\n className={cn(\n // The shadow separates it from the content below; the border, where the\n // shadow doesn't show (dark backgrounds, poor screens).\n \"flex w-60 flex-col rounded-xl border border-border-subtle bg-background p-1.5 shadow-lg outline-hidden\",\n \"origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out\",\n \"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0\",\n \"motion-reduce:transition-none\",\n className,\n )}\n >\n {children}\n </BaseMenu.Popup>\n </BaseMenu.Positioner>\n </BaseMenu.Portal>\n );\n}\n\nconst menuItemVariants = cva(\n [\n \"flex w-full shrink-0 cursor-pointer select-none items-center gap-3 rounded-lg px-3 text-left text-label-default\",\n \"outline-hidden focus-visible:outline-2 focus-visible:outline-border-focus focus-visible:outline-solid focus-visible:-outline-offset-2\",\n \"[&_svg]:size-icon-md [&_svg]:shrink-0\",\n \"data-disabled:cursor-not-allowed data-disabled:text-text-disabled disabled:cursor-not-allowed disabled:text-text-disabled\",\n ],\n {\n variants: {\n type: {\n default: \"text-text-primary\",\n destructive: \"text-text-error\",\n },\n layout: {\n popover: \"h-control-md\",\n sheet: \"h-12\",\n },\n },\n // Base UI marks the pointer/keyboard position with data-highlighted in the popover;\n // the sheet's rows are plain buttons, so they use :hover.\n compoundVariants: [\n {\n type: \"default\",\n layout: \"popover\",\n className: \"not-data-disabled:data-highlighted:bg-surface\",\n },\n {\n type: \"destructive\",\n layout: \"popover\",\n className: \"not-data-disabled:data-highlighted:bg-error-subtle\",\n },\n {\n type: \"default\",\n layout: \"sheet\",\n className: \"enabled:hover:bg-surface\",\n },\n {\n type: \"destructive\",\n layout: \"sheet\",\n className: \"enabled:hover:bg-error-subtle\",\n },\n ],\n defaultVariants: {\n type: \"default\",\n layout: \"popover\",\n },\n },\n);\n\nexport interface MenuItemProps {\n /** The option's label. */\n children: ReactNode;\n /** 20px icon before the label. */\n icon?: ReactNode;\n /** `destructive` goes in text-error and always last, after a `MenuSeparator`. */\n type?: \"default\" | \"destructive\";\n disabled?: boolean;\n onClick?: (event: MouseEvent<HTMLElement>) => void;\n /** Closes the menu after the click (default). */\n closeOnClick?: boolean;\n className?: string;\n}\n\n/** One option. 40px tall in the popover (48 in the sheet) so neighbours aren't tapped by mistake. */\nexport function MenuItem({\n children,\n icon,\n type = \"default\",\n disabled = false,\n onClick,\n closeOnClick = true,\n className,\n}: MenuItemProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuItem\");\n const classes = cn(menuItemVariants({ type, layout }), className);\n\n if (layout === \"sheet\") {\n return (\n <ModalClose\n data-slot=\"menu-item\"\n data-type={type}\n disabled={disabled}\n className={classes}\n onClick={(event) => {\n onClick?.(event);\n if (!closeOnClick) {\n event.preventBaseUIHandler();\n }\n }}\n >\n {icon}\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n </ModalClose>\n );\n }\n\n return (\n <BaseMenu.Item\n data-slot=\"menu-item\"\n data-type={type}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={onClick}\n className={classes}\n >\n {icon}\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n </BaseMenu.Item>\n );\n}\n\nexport interface MenuSeparatorProps {\n className?: string;\n}\n\n/** The line before the destructive option. */\nexport function MenuSeparator({\n className,\n}: MenuSeparatorProps): React.JSX.Element {\n const layout = useMenuLayout(\"MenuSeparator\");\n\n if (layout === \"sheet\") {\n return <Divider className={cn(\"my-1\", className)} />;\n }\n\n return (\n <BaseMenu.Separator\n data-slot=\"menu-separator\"\n className={cn(\"my-1 h-px shrink-0 bg-border-subtle\", className)}\n />\n );\n}\n"],"names":["BaseMenu"],"mappings":";;;;;;;;;;;AAmCA,MAAM,oBAAoB,cAAiC,IAAI;AAE/D,SAAS,cAAc,MAA0B;AAC/C,QAAM,SAAS,WAAW,iBAAiB;AAC3C,MAAI,WAAW,MAAM;AACnB,UAAM,IAAI,MAAM,cAAc,IAAI,+BAA+B;AAAA,EACnE;AACA,SAAO;AACT;AA4BO,SAAS,KAAK;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,YAAY,cAAc,mBAAmB;AACnD,QAAM,iBACJ,WAAW,YAAY,YAAY;AAErC,6BACG,kBAAkB,UAAlB,EAA2B,OAAO,gBAChC,6BAAmB,UAClB;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,QAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MAEC;AAAA,IAAA;AAAA,EAAA,IAGH;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MAEC;AAAA,IAAA;AAAA,EAAA,GAGP;AAEJ;AAyBO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAwC;AACtC,QAAM,SAAS,cAAc,aAAa;AAC1C,QAAM,SAAS,kBAAA;AACf,QAAM,UAAU,WAAW,UAAU,eAAeA,OAAS;AAE7D,MAAI,WAAW,UAAa,aAAa,QAAW;AAClD,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,QACE;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,SAAQ;AAAA,YACR,0BAAO,oBAAA,EAAmB;AAAA,YAC1B,cAAY,aAAa,OAAO;AAAA,UAAA;AAAA,QAAA;AAAA,QAGnC,GAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAGV;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,cAAY;AAAA,MACZ;AAAA,MACC,GAAG;AAAA,MAEH;AAAA,IAAA;AAAA,EAAA;AAGP;AAcO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,QAAQ;AAAA,EACR;AACF,GAAwC;AACtC,QAAM,SAAS,cAAc,aAAa;AAC1C,QAAM,SAAS,kBAAA;AAEf,MAAI,WAAW,SAAS;AACtB,WACE,oBAAC,cAAA,EAAa,cAAY,SAAS,OAAO,aACxC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW,GAAG,uBAAuB,SAAS;AAAA,QAE7C;AAAA,MAAA;AAAA,IAAA,GAEL;AAAA,EAEJ;AAEA,SACE,oBAACA,OAAS,QAAT,EACC,UAAA;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC;AAAA,MACA;AAAA,MACA,YAAY;AAAA,MACZ,WAAU;AAAA,MAEV,UAAA;AAAA,QAACA,OAAS;AAAA,QAAT;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA;AAAA;AAAA,YAGT;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UAAA;AAAA,UAGD;AAAA,QAAA;AAAA,MAAA;AAAA,IACH;AAAA,EAAA,GAEJ;AAEJ;AAEA,MAAM,mBAAmB;AAAA,EACvB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,aAAa;AAAA,MAAA;AAAA,MAEf,QAAQ;AAAA,QACN,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,IACT;AAAA;AAAA;AAAA,IAIF,kBAAkB;AAAA,MAChB;AAAA,QACE,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,MACN,QAAQ;AAAA,IAAA;AAAA,EACV;AAEJ;AAiBO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA,eAAe;AAAA,EACf;AACF,GAAqC;AACnC,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,GAAG,iBAAiB,EAAE,MAAM,OAAA,CAAQ,GAAG,SAAS;AAEhE,MAAI,WAAW,SAAS;AACtB,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,aAAW;AAAA,QACX;AAAA,QACA,WAAW;AAAA,QACX,SAAS,CAAC,UAAU;AAClB,oBAAU,KAAK;AACf,cAAI,CAAC,cAAc;AACjB,kBAAM,qBAAA;AAAA,UACR;AAAA,QACF;AAAA,QAEC,UAAA;AAAA,UAAA;AAAA,UACD,oBAAC,QAAA,EAAK,WAAU,2BAA2B,SAAA,CAAS;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAG1D;AAEA,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MAEV,UAAA;AAAA,QAAA;AAAA,QACD,oBAAC,QAAA,EAAK,WAAU,2BAA2B,SAAA,CAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG1D;AAOO,SAAS,cAAc;AAAA,EAC5B;AACF,GAA0C;AACxC,QAAM,SAAS,cAAc,eAAe;AAE5C,MAAI,WAAW,SAAS;AACtB,+BAAQ,SAAA,EAAQ,WAAW,GAAG,QAAQ,SAAS,GAAG;AAAA,EACpD;AAEA,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,uCAAuC,SAAS;AAAA,IAAA;AAAA,EAAA;AAGpE;"}
|
|
@@ -93,7 +93,7 @@ function ModalContent({
|
|
|
93
93
|
initialFocus: initialFocus ?? popupRef,
|
|
94
94
|
className: cn(
|
|
95
95
|
"flex max-h-[90dvh] w-full flex-col gap-4 overflow-y-auto overscroll-contain",
|
|
96
|
-
"rounded-t-
|
|
96
|
+
"rounded-t-2xl bg-background px-6 pt-3 pb-8 text-text-primary shadow-xl outline-hidden",
|
|
97
97
|
"[transform:translateY(var(--drawer-swipe-movement-y,0px))] transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]",
|
|
98
98
|
"data-starting-style:[transform:translateY(100%)] data-ending-style:[transform:translateY(100%)]",
|
|
99
99
|
"data-ending-style:duration-[calc(var(--drawer-swipe-strength,1)*400ms)] data-swiping:select-none",
|
|
@@ -134,7 +134,7 @@ function ModalContent({
|
|
|
134
134
|
initialFocus: initialFocus ?? popupRef,
|
|
135
135
|
className: cn(
|
|
136
136
|
"flex max-h-full w-120 max-w-full flex-col gap-4 overflow-y-auto overscroll-contain",
|
|
137
|
-
"rounded-
|
|
137
|
+
"rounded-2xl bg-background p-6 text-text-primary shadow-xl outline-hidden",
|
|
138
138
|
"transition-[opacity,scale] duration-200 ease-out",
|
|
139
139
|
"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0",
|
|
140
140
|
"motion-reduce:transition-none",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.js","sources":["../../../src/components/modal/modal.tsx"],"sourcesContent":["import { Dialog } from \"@base-ui/react/dialog\";\nimport { Drawer } from \"@base-ui/react/drawer\";\nimport {\n type ComponentProps,\n type CSSProperties,\n createContext,\n type ReactNode,\n type Ref,\n type RefObject,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { XCloseIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { IconButton } from \"../icon-button\";\nimport { DESKTOP_MEDIA_QUERY, useMediaQuery } from \"./use-media-query\";\n\nexport type ModalLayout = \"center\" | \"sheet\";\n\nexport type ModalChangeEventDetails =\n | Dialog.Root.ChangeEventDetails\n | Drawer.Root.ChangeEventDetails;\n\nexport interface ModalActions {\n /** Closes the modal as if the person had dismissed it. */\n close: () => void;\n /** Unmounts the popup after an externally controlled closing animation. */\n unmount: () => void;\n}\n\ninterface ModalContextValue {\n layout: ModalLayout;\n}\n\nconst ModalContext = createContext<ModalContextValue | null>(null);\n\nfunction useModalContext(part: string): ModalContextValue {\n const context = useContext(ModalContext);\n if (context === null) {\n throw new Error(`MediCore: <${part}> must be used inside <Modal>.`);\n }\n return context;\n}\n\nexport interface ModalProps {\n /**\n * `center` is the 480px dialog, `sheet` the bottom sheet. Leave it unset and it\n * follows the screen: sheet below `md`, centered dialog from `md` up.\n */\n layout?: ModalLayout;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean, eventDetails: ModalChangeEventDetails) => void;\n /** Called after the open/close animation finishes. */\n onOpenChangeComplete?: (open: boolean) => void;\n /** Keeps the modal open when the person clicks or taps outside it. */\n disablePointerDismissal?: boolean;\n /** Imperative `close()` / `unmount()`. */\n actionsRef?: RefObject<ModalActions | null>;\n children?: ReactNode;\n}\n\n/**\n * Confirmation and interruption dialog. One component, two layouts with the same\n * content: a centered dialog on desktop and a bottom sheet on mobile.\n *\n * - `center`: close X in the header, actions in a row aligned right.\n * - `sheet`: drag handle and no X (it closes by swiping down or tapping outside),\n * actions stacked full width with the primary on top.\n *\n * Focus moves into the dialog when it opens, Tab stays inside, Escape closes it\n * and focus returns to the element that opened it.\n *\n * There is no destructive Modal: what is destructive is the action, so pass a\n * `destructive` Button as the primary action (or use `ConfirmModal`).\n */\nexport function Modal({\n layout,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n children,\n ...props\n}: ModalProps): React.JSX.Element {\n const isDesktop = useMediaQuery(DESKTOP_MEDIA_QUERY);\n const resolvedLayout: ModalLayout =\n layout ?? (isDesktop ? \"center\" : \"sheet\");\n\n // The open state lives here (not in Base UI) so the modal stays open when the\n // screen crosses the breakpoint and the Dialog root is swapped for the Drawer one.\n const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n const open = openProp ?? uncontrolledOpen;\n const isControlled = openProp !== undefined;\n\n const handleOpenChange = useCallback(\n (nextOpen: boolean, eventDetails: ModalChangeEventDetails) => {\n onOpenChange?.(nextOpen, eventDetails);\n if (!isControlled && !eventDetails.isCanceled) {\n setUncontrolledOpen(nextOpen);\n }\n },\n [isControlled, onOpenChange],\n );\n\n const context = useMemo(() => ({ layout: resolvedLayout }), [resolvedLayout]);\n\n return (\n <ModalContext.Provider value={context}>\n {resolvedLayout === \"sheet\" ? (\n <Drawer.Root open={open} onOpenChange={handleOpenChange} {...props}>\n {children}\n </Drawer.Root>\n ) : (\n <Dialog.Root open={open} onOpenChange={handleOpenChange} {...props}>\n {children}\n </Dialog.Root>\n )}\n </ModalContext.Provider>\n );\n}\n\nexport interface ModalTriggerProps extends Dialog.Trigger.Props {}\n\n/**\n * The element that opens the modal. Unstyled: pass the look with\n * `render={<Button variant=\"…\" />}`.\n */\nexport function ModalTrigger(props: ModalTriggerProps): React.JSX.Element {\n // Drawer.Trigger is Dialog.Trigger, so the same part works for both layouts.\n return <Dialog.Trigger data-slot=\"modal-trigger\" {...props} />;\n}\n\nexport interface ModalCloseProps extends Dialog.Close.Props {}\n\n/** Closes the modal. Use it for the \"Cancelar\" action: `render={<Button variant=\"secondary\" />}`. */\nexport function ModalClose(props: ModalCloseProps): React.JSX.Element {\n return <Dialog.Close data-slot=\"modal-close\" {...props} />;\n}\n\nconst backdropBase =\n \"fixed inset-0 z-50 min-h-dvh bg-scrim-image transition-opacity duration-200 data-starting-style:opacity-0 data-ending-style:opacity-0 motion-reduce:transition-none supports-[-webkit-touch-callout:none]:absolute\";\n\nexport interface ModalContentProps\n extends Omit<Dialog.Popup.Props, \"className\" | \"style\" | \"render\" | \"ref\"> {\n className?: string;\n style?: CSSProperties;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * The dialog itself, with the scrim behind it. Children go in a column with 16px\n * between them: `ModalHeader`, then `ModalBody` (or your own content, sized to\n * the popup width) and `ModalFooter`.\n *\n * On open, focus goes to the dialog itself so screen readers announce its title and\n * message, and a destructive primary action is never focused by default. Pass\n * `initialFocus` to focus a field instead (e.g. in a form).\n */\nexport function ModalContent({\n className,\n children,\n initialFocus,\n ref,\n ...props\n}: ModalContentProps): React.JSX.Element {\n const { layout } = useModalContext(\"ModalContent\");\n const popupRef = useRef<HTMLDivElement | null>(null);\n\n const setPopupRef = useCallback(\n (node: HTMLDivElement | null) => {\n popupRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref],\n );\n\n if (layout === \"sheet\") {\n return (\n <Drawer.Portal>\n <Drawer.Backdrop\n data-slot=\"modal-backdrop\"\n className={cn(\n backdropBase,\n \"opacity-[calc(1-var(--drawer-swipe-progress,0))] data-swiping:duration-0\",\n )}\n />\n <Drawer.Viewport\n data-slot=\"modal-viewport\"\n className=\"fixed inset-0 z-50 flex items-end justify-center\"\n >\n <Drawer.Popup\n ref={setPopupRef}\n data-slot=\"modal-content\"\n data-layout=\"sheet\"\n initialFocus={initialFocus ?? popupRef}\n className={cn(\n \"flex max-h-[90dvh] w-full flex-col gap-4 overflow-y-auto overscroll-contain\",\n \"rounded-t-xl bg-background px-6 pt-3 pb-8 text-text-primary shadow-xl outline-hidden\",\n \"[transform:translateY(var(--drawer-swipe-movement-y,0px))] transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]\",\n \"data-starting-style:[transform:translateY(100%)] data-ending-style:[transform:translateY(100%)]\",\n \"data-ending-style:duration-[calc(var(--drawer-swipe-strength,1)*400ms)] data-swiping:select-none\",\n \"motion-reduce:transition-none\",\n className,\n )}\n {...props}\n >\n <div\n aria-hidden=\"true\"\n data-slot=\"modal-handle\"\n className=\"mx-auto h-1 w-9 shrink-0 rounded-full bg-border\"\n />\n <Drawer.Content className=\"flex flex-col gap-4\">\n {children}\n </Drawer.Content>\n </Drawer.Popup>\n </Drawer.Viewport>\n </Drawer.Portal>\n );\n }\n\n return (\n <Dialog.Portal>\n <Dialog.Backdrop data-slot=\"modal-backdrop\" className={backdropBase} />\n <Dialog.Viewport\n data-slot=\"modal-viewport\"\n className=\"fixed inset-0 z-50 flex items-center justify-center p-6\"\n >\n <Dialog.Popup\n ref={setPopupRef}\n data-slot=\"modal-content\"\n data-layout=\"center\"\n initialFocus={initialFocus ?? popupRef}\n className={cn(\n \"flex max-h-full w-120 max-w-full flex-col gap-4 overflow-y-auto overscroll-contain\",\n \"rounded-xl bg-background p-6 text-text-primary shadow-xl outline-hidden\",\n \"transition-[opacity,scale] duration-200 ease-out\",\n \"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0\",\n \"motion-reduce:transition-none\",\n className,\n )}\n {...props}\n >\n {children}\n </Dialog.Popup>\n </Dialog.Viewport>\n </Dialog.Portal>\n );\n}\n\nexport interface ModalHeaderProps extends ComponentProps<\"div\"> {\n /**\n * Shows the close X (default). Only in the `center` layout: the sheet never has one,\n * it closes by swiping or tapping outside.\n */\n closeButton?: boolean;\n}\n\n/** Title row. Put a `ModalTitle` inside; the close X is added for you. */\nexport function ModalHeader({\n closeButton = true,\n className,\n children,\n ...props\n}: ModalHeaderProps): React.JSX.Element {\n const { layout } = useModalContext(\"ModalHeader\");\n const labels = useMediCoreLabels();\n\n return (\n <div\n data-slot=\"modal-header\"\n className={cn(\"flex items-start gap-3\", className)}\n {...props}\n >\n {children}\n {closeButton && layout === \"center\" && (\n <Dialog.Close\n render={\n <IconButton\n variant=\"ghost\"\n icon={<XCloseIcon />}\n aria-label={labels.close}\n />\n }\n />\n )}\n </div>\n );\n}\n\nexport interface ModalTitleProps extends Omit<Dialog.Title.Props, \"className\"> {\n className?: string;\n}\n\n/** Names the dialog (it becomes its accessible name). Renders an `<h2>`. */\nexport function ModalTitle({\n className,\n ...props\n}: ModalTitleProps): React.JSX.Element {\n return (\n <Dialog.Title\n data-slot=\"modal-title\"\n className={cn(\n \"min-w-0 flex-1 text-heading-h3 text-text-primary\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface ModalBodyProps\n extends Omit<Dialog.Description.Props, \"className\"> {\n className?: string;\n}\n\n/**\n * The default content: the dialog's message, announced as its description.\n * For your own content (a form, a list) put it directly inside `ModalContent`\n * instead of `ModalBody`.\n */\nexport function ModalBody({\n className,\n ...props\n}: ModalBodyProps): React.JSX.Element {\n return (\n <Dialog.Description\n data-slot=\"modal-body\"\n className={cn(\"text-body-default text-text-secondary\", className)}\n {...props}\n />\n );\n}\n\nexport interface ModalFooterProps\n extends Omit<ComponentProps<\"div\">, \"children\"> {\n /** The main action, usually a `Button` (`variant=\"destructive\"` for a destructive confirmation). */\n primaryAction: ReactNode;\n /** Usually `<ModalClose render={<Button variant=\"secondary\" />}>Cancelar</ModalClose>`. */\n secondaryAction?: ReactNode;\n}\n\n/**\n * The actions. `center`: in a row aligned right, secondary first. `sheet`: stacked\n * full width with the primary on top, away from the resting thumb.\n */\nexport function ModalFooter({\n primaryAction,\n secondaryAction,\n className,\n ...props\n}: ModalFooterProps): React.JSX.Element {\n const { layout } = useModalContext(\"ModalFooter\");\n const isSheet = layout === \"sheet\";\n\n return (\n <div\n data-slot=\"modal-footer\"\n className={cn(\n \"flex gap-2\",\n isSheet ? \"flex-col *:w-full\" : \"flex-row flex-wrap justify-end\",\n className,\n )}\n {...props}\n >\n {isSheet ? primaryAction : secondaryAction}\n {isSheet ? secondaryAction : primaryAction}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"modal.js","sources":["../../../src/components/modal/modal.tsx"],"sourcesContent":["import { Dialog } from \"@base-ui/react/dialog\";\nimport { Drawer } from \"@base-ui/react/drawer\";\nimport {\n type ComponentProps,\n type CSSProperties,\n createContext,\n type ReactNode,\n type Ref,\n type RefObject,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { XCloseIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { IconButton } from \"../icon-button\";\nimport { DESKTOP_MEDIA_QUERY, useMediaQuery } from \"./use-media-query\";\n\nexport type ModalLayout = \"center\" | \"sheet\";\n\nexport type ModalChangeEventDetails =\n | Dialog.Root.ChangeEventDetails\n | Drawer.Root.ChangeEventDetails;\n\nexport interface ModalActions {\n /** Closes the modal as if the person had dismissed it. */\n close: () => void;\n /** Unmounts the popup after an externally controlled closing animation. */\n unmount: () => void;\n}\n\ninterface ModalContextValue {\n layout: ModalLayout;\n}\n\nconst ModalContext = createContext<ModalContextValue | null>(null);\n\nfunction useModalContext(part: string): ModalContextValue {\n const context = useContext(ModalContext);\n if (context === null) {\n throw new Error(`MediCore: <${part}> must be used inside <Modal>.`);\n }\n return context;\n}\n\nexport interface ModalProps {\n /**\n * `center` is the 480px dialog, `sheet` the bottom sheet. Leave it unset and it\n * follows the screen: sheet below `md`, centered dialog from `md` up.\n */\n layout?: ModalLayout;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean, eventDetails: ModalChangeEventDetails) => void;\n /** Called after the open/close animation finishes. */\n onOpenChangeComplete?: (open: boolean) => void;\n /** Keeps the modal open when the person clicks or taps outside it. */\n disablePointerDismissal?: boolean;\n /** Imperative `close()` / `unmount()`. */\n actionsRef?: RefObject<ModalActions | null>;\n children?: ReactNode;\n}\n\n/**\n * Confirmation and interruption dialog. One component, two layouts with the same\n * content: a centered dialog on desktop and a bottom sheet on mobile.\n *\n * - `center`: close X in the header, actions in a row aligned right.\n * - `sheet`: drag handle and no X (it closes by swiping down or tapping outside),\n * actions stacked full width with the primary on top.\n *\n * Focus moves into the dialog when it opens, Tab stays inside, Escape closes it\n * and focus returns to the element that opened it.\n *\n * There is no destructive Modal: what is destructive is the action, so pass a\n * `destructive` Button as the primary action (or use `ConfirmModal`).\n */\nexport function Modal({\n layout,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n children,\n ...props\n}: ModalProps): React.JSX.Element {\n const isDesktop = useMediaQuery(DESKTOP_MEDIA_QUERY);\n const resolvedLayout: ModalLayout =\n layout ?? (isDesktop ? \"center\" : \"sheet\");\n\n // The open state lives here (not in Base UI) so the modal stays open when the\n // screen crosses the breakpoint and the Dialog root is swapped for the Drawer one.\n const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n const open = openProp ?? uncontrolledOpen;\n const isControlled = openProp !== undefined;\n\n const handleOpenChange = useCallback(\n (nextOpen: boolean, eventDetails: ModalChangeEventDetails) => {\n onOpenChange?.(nextOpen, eventDetails);\n if (!isControlled && !eventDetails.isCanceled) {\n setUncontrolledOpen(nextOpen);\n }\n },\n [isControlled, onOpenChange],\n );\n\n const context = useMemo(() => ({ layout: resolvedLayout }), [resolvedLayout]);\n\n return (\n <ModalContext.Provider value={context}>\n {resolvedLayout === \"sheet\" ? (\n <Drawer.Root open={open} onOpenChange={handleOpenChange} {...props}>\n {children}\n </Drawer.Root>\n ) : (\n <Dialog.Root open={open} onOpenChange={handleOpenChange} {...props}>\n {children}\n </Dialog.Root>\n )}\n </ModalContext.Provider>\n );\n}\n\nexport interface ModalTriggerProps extends Dialog.Trigger.Props {}\n\n/**\n * The element that opens the modal. Unstyled: pass the look with\n * `render={<Button variant=\"…\" />}`.\n */\nexport function ModalTrigger(props: ModalTriggerProps): React.JSX.Element {\n // Drawer.Trigger is Dialog.Trigger, so the same part works for both layouts.\n return <Dialog.Trigger data-slot=\"modal-trigger\" {...props} />;\n}\n\nexport interface ModalCloseProps extends Dialog.Close.Props {}\n\n/** Closes the modal. Use it for the \"Cancelar\" action: `render={<Button variant=\"secondary\" />}`. */\nexport function ModalClose(props: ModalCloseProps): React.JSX.Element {\n return <Dialog.Close data-slot=\"modal-close\" {...props} />;\n}\n\nconst backdropBase =\n \"fixed inset-0 z-50 min-h-dvh bg-scrim-image transition-opacity duration-200 data-starting-style:opacity-0 data-ending-style:opacity-0 motion-reduce:transition-none supports-[-webkit-touch-callout:none]:absolute\";\n\nexport interface ModalContentProps\n extends Omit<Dialog.Popup.Props, \"className\" | \"style\" | \"render\" | \"ref\"> {\n className?: string;\n style?: CSSProperties;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * The dialog itself, with the scrim behind it. Children go in a column with 16px\n * between them: `ModalHeader`, then `ModalBody` (or your own content, sized to\n * the popup width) and `ModalFooter`.\n *\n * On open, focus goes to the dialog itself so screen readers announce its title and\n * message, and a destructive primary action is never focused by default. Pass\n * `initialFocus` to focus a field instead (e.g. in a form).\n */\nexport function ModalContent({\n className,\n children,\n initialFocus,\n ref,\n ...props\n}: ModalContentProps): React.JSX.Element {\n const { layout } = useModalContext(\"ModalContent\");\n const popupRef = useRef<HTMLDivElement | null>(null);\n\n const setPopupRef = useCallback(\n (node: HTMLDivElement | null) => {\n popupRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref],\n );\n\n if (layout === \"sheet\") {\n return (\n <Drawer.Portal>\n <Drawer.Backdrop\n data-slot=\"modal-backdrop\"\n className={cn(\n backdropBase,\n \"opacity-[calc(1-var(--drawer-swipe-progress,0))] data-swiping:duration-0\",\n )}\n />\n <Drawer.Viewport\n data-slot=\"modal-viewport\"\n className=\"fixed inset-0 z-50 flex items-end justify-center\"\n >\n <Drawer.Popup\n ref={setPopupRef}\n data-slot=\"modal-content\"\n data-layout=\"sheet\"\n initialFocus={initialFocus ?? popupRef}\n className={cn(\n \"flex max-h-[90dvh] w-full flex-col gap-4 overflow-y-auto overscroll-contain\",\n \"rounded-t-2xl bg-background px-6 pt-3 pb-8 text-text-primary shadow-xl outline-hidden\",\n \"[transform:translateY(var(--drawer-swipe-movement-y,0px))] transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]\",\n \"data-starting-style:[transform:translateY(100%)] data-ending-style:[transform:translateY(100%)]\",\n \"data-ending-style:duration-[calc(var(--drawer-swipe-strength,1)*400ms)] data-swiping:select-none\",\n \"motion-reduce:transition-none\",\n className,\n )}\n {...props}\n >\n <div\n aria-hidden=\"true\"\n data-slot=\"modal-handle\"\n className=\"mx-auto h-1 w-9 shrink-0 rounded-full bg-border\"\n />\n <Drawer.Content className=\"flex flex-col gap-4\">\n {children}\n </Drawer.Content>\n </Drawer.Popup>\n </Drawer.Viewport>\n </Drawer.Portal>\n );\n }\n\n return (\n <Dialog.Portal>\n <Dialog.Backdrop data-slot=\"modal-backdrop\" className={backdropBase} />\n <Dialog.Viewport\n data-slot=\"modal-viewport\"\n className=\"fixed inset-0 z-50 flex items-center justify-center p-6\"\n >\n <Dialog.Popup\n ref={setPopupRef}\n data-slot=\"modal-content\"\n data-layout=\"center\"\n initialFocus={initialFocus ?? popupRef}\n className={cn(\n \"flex max-h-full w-120 max-w-full flex-col gap-4 overflow-y-auto overscroll-contain\",\n \"rounded-2xl bg-background p-6 text-text-primary shadow-xl outline-hidden\",\n \"transition-[opacity,scale] duration-200 ease-out\",\n \"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0\",\n \"motion-reduce:transition-none\",\n className,\n )}\n {...props}\n >\n {children}\n </Dialog.Popup>\n </Dialog.Viewport>\n </Dialog.Portal>\n );\n}\n\nexport interface ModalHeaderProps extends ComponentProps<\"div\"> {\n /**\n * Shows the close X (default). Only in the `center` layout: the sheet never has one,\n * it closes by swiping or tapping outside.\n */\n closeButton?: boolean;\n}\n\n/** Title row. Put a `ModalTitle` inside; the close X is added for you. */\nexport function ModalHeader({\n closeButton = true,\n className,\n children,\n ...props\n}: ModalHeaderProps): React.JSX.Element {\n const { layout } = useModalContext(\"ModalHeader\");\n const labels = useMediCoreLabels();\n\n return (\n <div\n data-slot=\"modal-header\"\n className={cn(\"flex items-start gap-3\", className)}\n {...props}\n >\n {children}\n {closeButton && layout === \"center\" && (\n <Dialog.Close\n render={\n <IconButton\n variant=\"ghost\"\n icon={<XCloseIcon />}\n aria-label={labels.close}\n />\n }\n />\n )}\n </div>\n );\n}\n\nexport interface ModalTitleProps extends Omit<Dialog.Title.Props, \"className\"> {\n className?: string;\n}\n\n/** Names the dialog (it becomes its accessible name). Renders an `<h2>`. */\nexport function ModalTitle({\n className,\n ...props\n}: ModalTitleProps): React.JSX.Element {\n return (\n <Dialog.Title\n data-slot=\"modal-title\"\n className={cn(\n \"min-w-0 flex-1 text-heading-h3 text-text-primary\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface ModalBodyProps\n extends Omit<Dialog.Description.Props, \"className\"> {\n className?: string;\n}\n\n/**\n * The default content: the dialog's message, announced as its description.\n * For your own content (a form, a list) put it directly inside `ModalContent`\n * instead of `ModalBody`.\n */\nexport function ModalBody({\n className,\n ...props\n}: ModalBodyProps): React.JSX.Element {\n return (\n <Dialog.Description\n data-slot=\"modal-body\"\n className={cn(\"text-body-default text-text-secondary\", className)}\n {...props}\n />\n );\n}\n\nexport interface ModalFooterProps\n extends Omit<ComponentProps<\"div\">, \"children\"> {\n /** The main action, usually a `Button` (`variant=\"destructive\"` for a destructive confirmation). */\n primaryAction: ReactNode;\n /** Usually `<ModalClose render={<Button variant=\"secondary\" />}>Cancelar</ModalClose>`. */\n secondaryAction?: ReactNode;\n}\n\n/**\n * The actions. `center`: in a row aligned right, secondary first. `sheet`: stacked\n * full width with the primary on top, away from the resting thumb.\n */\nexport function ModalFooter({\n primaryAction,\n secondaryAction,\n className,\n ...props\n}: ModalFooterProps): React.JSX.Element {\n const { layout } = useModalContext(\"ModalFooter\");\n const isSheet = layout === \"sheet\";\n\n return (\n <div\n data-slot=\"modal-footer\"\n className={cn(\n \"flex gap-2\",\n isSheet ? \"flex-col *:w-full\" : \"flex-row flex-wrap justify-end\",\n className,\n )}\n {...props}\n >\n {isSheet ? primaryAction : secondaryAction}\n {isSheet ? secondaryAction : primaryAction}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -142,11 +142,11 @@ function PhoneInput({
|
|
|
142
142
|
{
|
|
143
143
|
"data-slot": "phone-input-prefix",
|
|
144
144
|
className: cn(
|
|
145
|
-
"group/control relative w-24 shrink-0",
|
|
146
145
|
controlBox,
|
|
147
146
|
controlRow,
|
|
148
147
|
controlHeight.default,
|
|
149
148
|
wrappedControlStates,
|
|
149
|
+
"group/control relative w-24 shrink-0",
|
|
150
150
|
// The number carries aria-invalid (it is the Field control): the prefix follows it.
|
|
151
151
|
"group-has-[[aria-invalid=true]]/phone:border-error group-has-[[aria-invalid=true]]/phone:ring-error"
|
|
152
152
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phone-input.js","sources":["../../../src/components/phone-input/phone-input.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\nimport { ChevronDownIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { useFieldContext } from \"../field/field-context\";\nimport {\n controlBox,\n controlHeight,\n controlRow,\n wrappedControlStates,\n} from \"../input/control-styles\";\nimport { Input, type InputProps } from \"../input/input\";\n\nexport interface PhonePrefix {\n /** Dial code, with the plus: \"+34\". It is what the box shows and what is submitted. */\n value: string;\n /** Country name for the list: \"España\". The option reads \"España (+34)\". */\n label: string;\n}\n\ntype InputChangeEvent = Parameters<NonNullable<InputProps[\"onChange\"]>>[0];\n\n/** E.164 allows at most 15 digits. */\nconst MAX_DIGITS = 15;\n\nconst defaultRegions: ReadonlyArray<readonly [region: string, dial: string]> = [\n [\"ES\", \"+34\"],\n [\"PT\", \"+351\"],\n [\"FR\", \"+33\"],\n [\"IT\", \"+39\"],\n [\"DE\", \"+49\"],\n [\"GB\", \"+44\"],\n [\"MX\", \"+52\"],\n [\"CO\", \"+57\"],\n [\"AR\", \"+54\"],\n [\"CL\", \"+56\"],\n];\n\n/**\n * A short default list (Spain, neighbours and the main Latin American markets). The\n * country names come from `Intl.DisplayNames` in the page language (`<html lang>`),\n * so nothing is hardcoded. The real list comes from the app.\n */\nexport function getDefaultPhonePrefixes(): PhonePrefix[] {\n const lang =\n typeof document === \"undefined\"\n ? undefined\n : document.documentElement.lang || undefined;\n let names: Intl.DisplayNames | undefined;\n try {\n names = new Intl.DisplayNames(lang ? [lang] : undefined, {\n type: \"region\",\n });\n } catch {\n names = undefined;\n }\n return defaultRegions.map(([region, value]) => ({\n value,\n label: names?.of(region) ?? region,\n }));\n}\n\n/** 612345678 → \"612 345 678\": groups of three, as the person reads it aloud. */\nfunction formatDigits(digits: string): string {\n return digits.replace(/(\\d{3})(?=\\d)/g, \"$1 \");\n}\n\n/** Where the caret goes in `formatted` so that `digits` digits are before it. */\nfunction caretAfterDigits(formatted: string, digits: number): number {\n if (digits <= 0) {\n return 0;\n }\n let seen = 0;\n for (let index = 0; index < formatted.length; index++) {\n if (/\\d/.test(formatted.charAt(index))) {\n seen++;\n if (seen === digits) {\n return index + 1;\n }\n }\n }\n return formatted.length;\n}\n\nexport interface PhoneInputProps\n extends Omit<\n InputProps,\n | \"type\"\n | \"value\"\n | \"defaultValue\"\n | \"onValueChange\"\n | \"size\"\n | \"leadingIcon\"\n | \"trailingIcon\"\n | \"clearable\"\n | \"name\"\n > {\n /** Number without spaces (digits only). Use when controlled. */\n value?: string;\n /** Number without spaces. Use when uncontrolled. */\n defaultValue?: string;\n /** Called with the digits only, never with the grouping spaces. */\n onValueChange?: (value: string) => void;\n /** Countries in the prefix list. Defaults to `getDefaultPhonePrefixes()`. */\n prefixes?: readonly PhonePrefix[];\n /** Chosen dial code (\"+34\"). Use when controlled. */\n prefix?: string;\n /** Initial dial code, the country chosen when entering. Default \"+34\". */\n defaultPrefix?: string;\n onPrefixChange?: (prefix: string) => void;\n /** Form name of the number. It is submitted without spaces. */\n name?: string;\n /** Form name of the dial code. */\n prefixName?: string;\n}\n\n/**\n * Mobile number with country prefix: two controls in a row, a native prefix select\n * (+34 by default, `autocomplete=\"tel-country-code\"`) and the number\n * (`type=\"tel\" inputmode=\"tel\" autocomplete=\"tel-national\"`).\n *\n * The number is shown in groups of three (612 345 678) but `onValueChange` and the\n * form get it without spaces. Put it inside `Field` for the label and the message,\n * and make the error say what happens (\"Este número no parece un móvil español: tiene\n * que empezar por 6 o 7 y tener 9 cifras\"), not just \"Número no válido\".\n *\n * The prefix list opens the operating system's picker (a real `<select>` under the\n * \"+34\"), so it is searchable and accessible on every device.\n */\nexport function PhoneInput({\n value,\n defaultValue,\n onValueChange,\n prefixes: prefixesProp,\n prefix: prefixProp,\n defaultPrefix = \"+34\",\n onPrefixChange,\n name,\n prefixName,\n disabled: disabledProp,\n invalid,\n className,\n onChange,\n ref,\n \"aria-label\": ariaLabel,\n ...props\n}: PhoneInputProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const field = useFieldContext();\n const disabled = Boolean(disabledProp || field?.disabled);\n\n const prefixes = useMemo(\n () => prefixesProp ?? getDefaultPhonePrefixes(),\n [prefixesProp],\n );\n const [uncontrolledPrefix, setUncontrolledPrefix] = useState(defaultPrefix);\n const prefix = prefixProp ?? uncontrolledPrefix;\n\n const [uncontrolledDigits, setUncontrolledDigits] = useState(() =>\n (defaultValue ?? \"\").replace(/\\D/g, \"\").slice(0, MAX_DIGITS),\n );\n const digits =\n value === undefined\n ? uncontrolledDigits\n : value.replace(/\\D/g, \"\").slice(0, MAX_DIGITS);\n const formatted = formatDigits(digits);\n\n const inputRef = useRef<HTMLInputElement | null>(null);\n const pendingCaret = useRef<number | null>(null);\n\n const setRefs = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref],\n );\n\n // Reformatting moves the caret to the end; put it back after the same digit.\n // biome-ignore lint/correctness/useExhaustiveDependencies: runs when the formatted text changes.\n useLayoutEffect(() => {\n const input = inputRef.current;\n const digitsBefore = pendingCaret.current;\n pendingCaret.current = null;\n if (digitsBefore === null || !input || document.activeElement !== input) {\n return;\n }\n const position = caretAfterDigits(input.value, digitsBefore);\n input.setSelectionRange(position, position);\n }, [formatted]);\n\n function handleChange(event: InputChangeEvent): void {\n const input = event.currentTarget;\n const raw = input.value;\n const caret = input.selectionStart ?? raw.length;\n let next = raw.replace(/\\D/g, \"\");\n let digitsBefore = raw.slice(0, caret).replace(/\\D/g, \"\").length;\n\n const compact = raw.replace(/[^\\d+]/g, \"\");\n if (compact.startsWith(\"+\")) {\n // A full number pasted or autofilled with its prefix: keep the national part.\n next = compact.startsWith(prefix)\n ? compact.slice(prefix.length)\n : compact.slice(1);\n digitsBefore = next.length;\n } else if (\n (event.nativeEvent as InputEvent).inputType === \"deleteContentBackward\" &&\n next === digits &&\n digitsBefore > 0\n ) {\n // Backspace on a grouping space: delete the digit before it instead.\n next = next.slice(0, digitsBefore - 1) + next.slice(digitsBefore);\n digitsBefore -= 1;\n }\n\n next = next.slice(0, MAX_DIGITS);\n pendingCaret.current = Math.min(digitsBefore, next.length);\n if (value === undefined) {\n setUncontrolledDigits(next);\n }\n if (next !== digits) {\n onValueChange?.(next);\n }\n onChange?.(event);\n }\n\n return (\n <div\n data-slot=\"phone-input\"\n className={cn(\"group/phone flex w-full min-w-0 gap-2\", className)}\n >\n {/* A <label> around the select: its \"+34\" is then the select's own text, which\n also keeps it out of contrast checks when the control is disabled. */}\n <label\n data-slot=\"phone-input-prefix\"\n className={cn(\n \"group/control relative w-24 shrink-0\",\n controlBox,\n controlRow,\n controlHeight.default,\n wrappedControlStates,\n // The number carries aria-invalid (it is the Field control): the prefix follows it.\n \"group-has-[[aria-invalid=true]]/phone:border-error group-has-[[aria-invalid=true]]/phone:ring-error\",\n )}\n >\n <span aria-hidden className=\"min-w-0 flex-1 truncate\">\n {prefix}\n </span>\n <ChevronDownIcon\n size=\"sm\"\n className=\"text-text-primary group-has-[select:disabled]/control:text-text-disabled\"\n />\n <select\n aria-label={labels.phonePrefix}\n autoComplete=\"tel-country-code\"\n name={prefixName}\n value={prefix}\n disabled={disabled}\n onChange={(event) => {\n const next = event.currentTarget.value;\n if (prefixProp === undefined) {\n setUncontrolledPrefix(next);\n }\n onPrefixChange?.(next);\n }}\n className=\"absolute inset-0 size-full cursor-pointer appearance-none opacity-0 disabled:cursor-not-allowed\"\n >\n {prefixes.some((option) => option.value === prefix) ? null : (\n <option value={prefix}>{prefix}</option>\n )}\n {prefixes.map((option) => (\n <option\n key={`${option.value}-${option.label}`}\n value={option.value}\n >\n {`${option.label} (${option.value})`}\n </option>\n ))}\n </select>\n </label>\n <Input\n ref={setRefs}\n type=\"tel\"\n inputMode=\"tel\"\n autoComplete=\"tel-national\"\n aria-label={ariaLabel ?? labels.phoneNumber}\n value={formatted}\n disabled={disabled}\n invalid={invalid}\n onChange={handleChange}\n className=\"flex-1\"\n {...props}\n />\n {name && (\n <input type=\"hidden\" name={name} value={digits} disabled={disabled} />\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"phone-input.js","sources":["../../../src/components/phone-input/phone-input.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\nimport { ChevronDownIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { useFieldContext } from \"../field/field-context\";\nimport {\n controlBox,\n controlHeight,\n controlRow,\n wrappedControlStates,\n} from \"../input/control-styles\";\nimport { Input, type InputProps } from \"../input/input\";\n\nexport interface PhonePrefix {\n /** Dial code, with the plus: \"+34\". It is what the box shows and what is submitted. */\n value: string;\n /** Country name for the list: \"España\". The option reads \"España (+34)\". */\n label: string;\n}\n\ntype InputChangeEvent = Parameters<NonNullable<InputProps[\"onChange\"]>>[0];\n\n/** E.164 allows at most 15 digits. */\nconst MAX_DIGITS = 15;\n\nconst defaultRegions: ReadonlyArray<readonly [region: string, dial: string]> = [\n [\"ES\", \"+34\"],\n [\"PT\", \"+351\"],\n [\"FR\", \"+33\"],\n [\"IT\", \"+39\"],\n [\"DE\", \"+49\"],\n [\"GB\", \"+44\"],\n [\"MX\", \"+52\"],\n [\"CO\", \"+57\"],\n [\"AR\", \"+54\"],\n [\"CL\", \"+56\"],\n];\n\n/**\n * A short default list (Spain, neighbours and the main Latin American markets). The\n * country names come from `Intl.DisplayNames` in the page language (`<html lang>`),\n * so nothing is hardcoded. The real list comes from the app.\n */\nexport function getDefaultPhonePrefixes(): PhonePrefix[] {\n const lang =\n typeof document === \"undefined\"\n ? undefined\n : document.documentElement.lang || undefined;\n let names: Intl.DisplayNames | undefined;\n try {\n names = new Intl.DisplayNames(lang ? [lang] : undefined, {\n type: \"region\",\n });\n } catch {\n names = undefined;\n }\n return defaultRegions.map(([region, value]) => ({\n value,\n label: names?.of(region) ?? region,\n }));\n}\n\n/** 612345678 → \"612 345 678\": groups of three, as the person reads it aloud. */\nfunction formatDigits(digits: string): string {\n return digits.replace(/(\\d{3})(?=\\d)/g, \"$1 \");\n}\n\n/** Where the caret goes in `formatted` so that `digits` digits are before it. */\nfunction caretAfterDigits(formatted: string, digits: number): number {\n if (digits <= 0) {\n return 0;\n }\n let seen = 0;\n for (let index = 0; index < formatted.length; index++) {\n if (/\\d/.test(formatted.charAt(index))) {\n seen++;\n if (seen === digits) {\n return index + 1;\n }\n }\n }\n return formatted.length;\n}\n\nexport interface PhoneInputProps\n extends Omit<\n InputProps,\n | \"type\"\n | \"value\"\n | \"defaultValue\"\n | \"onValueChange\"\n | \"size\"\n | \"leadingIcon\"\n | \"trailingIcon\"\n | \"clearable\"\n | \"name\"\n > {\n /** Number without spaces (digits only). Use when controlled. */\n value?: string;\n /** Number without spaces. Use when uncontrolled. */\n defaultValue?: string;\n /** Called with the digits only, never with the grouping spaces. */\n onValueChange?: (value: string) => void;\n /** Countries in the prefix list. Defaults to `getDefaultPhonePrefixes()`. */\n prefixes?: readonly PhonePrefix[];\n /** Chosen dial code (\"+34\"). Use when controlled. */\n prefix?: string;\n /** Initial dial code, the country chosen when entering. Default \"+34\". */\n defaultPrefix?: string;\n onPrefixChange?: (prefix: string) => void;\n /** Form name of the number. It is submitted without spaces. */\n name?: string;\n /** Form name of the dial code. */\n prefixName?: string;\n}\n\n/**\n * Mobile number with country prefix: two controls in a row, a native prefix select\n * (+34 by default, `autocomplete=\"tel-country-code\"`) and the number\n * (`type=\"tel\" inputmode=\"tel\" autocomplete=\"tel-national\"`).\n *\n * The number is shown in groups of three (612 345 678) but `onValueChange` and the\n * form get it without spaces. Put it inside `Field` for the label and the message,\n * and make the error say what happens (\"Este número no parece un móvil español: tiene\n * que empezar por 6 o 7 y tener 9 cifras\"), not just \"Número no válido\".\n *\n * The prefix list opens the operating system's picker (a real `<select>` under the\n * \"+34\"), so it is searchable and accessible on every device.\n */\nexport function PhoneInput({\n value,\n defaultValue,\n onValueChange,\n prefixes: prefixesProp,\n prefix: prefixProp,\n defaultPrefix = \"+34\",\n onPrefixChange,\n name,\n prefixName,\n disabled: disabledProp,\n invalid,\n className,\n onChange,\n ref,\n \"aria-label\": ariaLabel,\n ...props\n}: PhoneInputProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const field = useFieldContext();\n const disabled = Boolean(disabledProp || field?.disabled);\n\n const prefixes = useMemo(\n () => prefixesProp ?? getDefaultPhonePrefixes(),\n [prefixesProp],\n );\n const [uncontrolledPrefix, setUncontrolledPrefix] = useState(defaultPrefix);\n const prefix = prefixProp ?? uncontrolledPrefix;\n\n const [uncontrolledDigits, setUncontrolledDigits] = useState(() =>\n (defaultValue ?? \"\").replace(/\\D/g, \"\").slice(0, MAX_DIGITS),\n );\n const digits =\n value === undefined\n ? uncontrolledDigits\n : value.replace(/\\D/g, \"\").slice(0, MAX_DIGITS);\n const formatted = formatDigits(digits);\n\n const inputRef = useRef<HTMLInputElement | null>(null);\n const pendingCaret = useRef<number | null>(null);\n\n const setRefs = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref],\n );\n\n // Reformatting moves the caret to the end; put it back after the same digit.\n // biome-ignore lint/correctness/useExhaustiveDependencies: runs when the formatted text changes.\n useLayoutEffect(() => {\n const input = inputRef.current;\n const digitsBefore = pendingCaret.current;\n pendingCaret.current = null;\n if (digitsBefore === null || !input || document.activeElement !== input) {\n return;\n }\n const position = caretAfterDigits(input.value, digitsBefore);\n input.setSelectionRange(position, position);\n }, [formatted]);\n\n function handleChange(event: InputChangeEvent): void {\n const input = event.currentTarget;\n const raw = input.value;\n const caret = input.selectionStart ?? raw.length;\n let next = raw.replace(/\\D/g, \"\");\n let digitsBefore = raw.slice(0, caret).replace(/\\D/g, \"\").length;\n\n const compact = raw.replace(/[^\\d+]/g, \"\");\n if (compact.startsWith(\"+\")) {\n // A full number pasted or autofilled with its prefix: keep the national part.\n next = compact.startsWith(prefix)\n ? compact.slice(prefix.length)\n : compact.slice(1);\n digitsBefore = next.length;\n } else if (\n (event.nativeEvent as InputEvent).inputType === \"deleteContentBackward\" &&\n next === digits &&\n digitsBefore > 0\n ) {\n // Backspace on a grouping space: delete the digit before it instead.\n next = next.slice(0, digitsBefore - 1) + next.slice(digitsBefore);\n digitsBefore -= 1;\n }\n\n next = next.slice(0, MAX_DIGITS);\n pendingCaret.current = Math.min(digitsBefore, next.length);\n if (value === undefined) {\n setUncontrolledDigits(next);\n }\n if (next !== digits) {\n onValueChange?.(next);\n }\n onChange?.(event);\n }\n\n return (\n <div\n data-slot=\"phone-input\"\n className={cn(\"group/phone flex w-full min-w-0 gap-2\", className)}\n >\n {/* A <label> around the select: its \"+34\" is then the select's own text, which\n also keeps it out of contrast checks when the control is disabled. */}\n <label\n data-slot=\"phone-input-prefix\"\n className={cn(\n controlBox,\n controlRow,\n controlHeight.default,\n wrappedControlStates,\n \"group/control relative w-24 shrink-0\",\n // The number carries aria-invalid (it is the Field control): the prefix follows it.\n \"group-has-[[aria-invalid=true]]/phone:border-error group-has-[[aria-invalid=true]]/phone:ring-error\",\n )}\n >\n <span aria-hidden className=\"min-w-0 flex-1 truncate\">\n {prefix}\n </span>\n <ChevronDownIcon\n size=\"sm\"\n className=\"text-text-primary group-has-[select:disabled]/control:text-text-disabled\"\n />\n <select\n aria-label={labels.phonePrefix}\n autoComplete=\"tel-country-code\"\n name={prefixName}\n value={prefix}\n disabled={disabled}\n onChange={(event) => {\n const next = event.currentTarget.value;\n if (prefixProp === undefined) {\n setUncontrolledPrefix(next);\n }\n onPrefixChange?.(next);\n }}\n className=\"absolute inset-0 size-full cursor-pointer appearance-none opacity-0 disabled:cursor-not-allowed\"\n >\n {prefixes.some((option) => option.value === prefix) ? null : (\n <option value={prefix}>{prefix}</option>\n )}\n {prefixes.map((option) => (\n <option\n key={`${option.value}-${option.label}`}\n value={option.value}\n >\n {`${option.label} (${option.value})`}\n </option>\n ))}\n </select>\n </label>\n <Input\n ref={setRefs}\n type=\"tel\"\n inputMode=\"tel\"\n autoComplete=\"tel-national\"\n aria-label={ariaLabel ?? labels.phoneNumber}\n value={formatted}\n disabled={disabled}\n invalid={invalid}\n onChange={handleChange}\n className=\"flex-1\"\n {...props}\n />\n {name && (\n <input type=\"hidden\" name={name} value={digits} disabled={disabled} />\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -76,7 +76,7 @@ function Select({
|
|
|
76
76
|
// Same surface as Menu: shadow-md to lift it and a border for when the
|
|
77
77
|
// shadow doesn't show (dark backgrounds, poor screens).
|
|
78
78
|
"max-h-[min(20rem,var(--available-height))] min-w-(--anchor-width) origin-(--transform-origin) overflow-y-auto",
|
|
79
|
-
"rounded-
|
|
79
|
+
"rounded-xl border border-border-subtle bg-background p-1.5 shadow-lg outline-none",
|
|
80
80
|
"transition-[opacity,scale] duration-150 motion-reduce:transition-none",
|
|
81
81
|
"data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
|
|
82
82
|
),
|
|
@@ -105,7 +105,7 @@ function SelectItem({
|
|
|
105
105
|
{
|
|
106
106
|
"data-slot": "select-item",
|
|
107
107
|
className: cn(
|
|
108
|
-
"flex h-control-md cursor-pointer select-none items-center gap-3 rounded-
|
|
108
|
+
"flex h-control-md cursor-pointer select-none items-center gap-3 rounded-lg px-3 text-label-default text-text-primary outline-none",
|
|
109
109
|
"data-highlighted:bg-surface focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-inset",
|
|
110
110
|
"data-disabled:cursor-not-allowed data-disabled:text-text-disabled",
|
|
111
111
|
className
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.js","sources":["../../../src/components/select/select.tsx"],"sourcesContent":["import { Select as BaseSelect } from \"@base-ui/react/select\";\nimport { type ReactNode, type Ref, useLayoutEffect } from \"react\";\n\nimport { CheckIcon, ChevronDownIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useFieldContext } from \"../field/field-context\";\nimport {\n type ControlSize,\n controlBox,\n controlHeight,\n controlIcon,\n controlRow,\n triggerControlStates,\n} from \"../input/control-styles\";\n\nexport interface SelectOption<Value> {\n value: Value;\n label: ReactNode;\n disabled?: boolean;\n}\n\nexport interface SelectProps<Value>\n extends Omit<\n BaseSelect.Root.Props<Value, false>,\n \"multiple\" | \"items\" | \"children\"\n > {\n /** The options. Also lets the trigger show the label of the chosen value. */\n items?: readonly SelectOption<Value>[];\n /** Custom options (`SelectItem`) instead of `items`. */\n children?: ReactNode;\n /** Shown in text-secondary while nothing is chosen: \"Elige una opción\". */\n placeholder?: ReactNode;\n /** `default` 40px, `small` 36px: the same heights as Input and Button. */\n size?: ControlSize;\n /** Red 2px border and `aria-invalid`. The message goes under it, in `Field`. */\n invalid?: boolean;\n /** Content icon before the value. The chevron is structure, not a prop. */\n leadingIcon?: ReactNode;\n /** Applied to the trigger. */\n className?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * Dropdown with a styled list (Base UI Select). The trigger has exactly the geometry\n * of Input, so a field and a select in the same row only differ by the chevron.\n *\n * The open list is not in Figma on purpose. Prefer `NativeSelect` on mobile and\n * whenever the options are plain text: it opens the operating system's picker and\n * is accessible out of the box. Use this one on desktop screens (the clinics admin)\n * where the list has to look like MediCore. Options that an `<option>` can't draw\n * (a doctor with photo and licence number) are a `Menu`, not this.\n *\n * Name it with `Field`'s label, or with `aria-label` when there is no visible label.\n */\nexport function Select<Value>({\n items,\n children,\n placeholder,\n size = \"default\",\n invalid = false,\n leadingIcon,\n className,\n disabled,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ref,\n ...props\n}: SelectProps<Value>): React.JSX.Element {\n const setNativeLabel = useFieldContext()?.setNativeLabel;\n\n // A native <label> around a button would open the list on click: Field renders a\n // non-native label while a Select is inside.\n useLayoutEffect(() => {\n if (!setNativeLabel) {\n return undefined;\n }\n setNativeLabel(false);\n return () => setNativeLabel(true);\n }, [setNativeLabel]);\n\n return (\n <BaseSelect.Root items={items} disabled={disabled} {...props}>\n <BaseSelect.Trigger\n ref={ref}\n data-slot=\"select\"\n // Only when given: an explicit undefined would override Field's aria-labelledby.\n {...(ariaLabel !== undefined && { \"aria-label\": ariaLabel })}\n {...(ariaLabelledBy !== undefined && {\n \"aria-labelledby\": ariaLabelledBy,\n })}\n aria-describedby={ariaDescribedBy}\n aria-invalid={invalid && !disabled ? true : undefined}\n className={cn(\n \"group/control cursor-pointer text-start\",\n controlBox,\n controlRow,\n controlHeight[size],\n triggerControlStates,\n className,\n )}\n >\n {leadingIcon && <span className={controlIcon}>{leadingIcon}</span>}\n <BaseSelect.Value\n placeholder={placeholder}\n className=\"min-w-0 flex-1 truncate data-placeholder:text-text-secondary group-data-disabled/control:text-text-disabled\"\n />\n <BaseSelect.Icon className=\"flex shrink-0 text-text-primary group-data-disabled/control:text-text-disabled\">\n <ChevronDownIcon size=\"sm\" />\n </BaseSelect.Icon>\n </BaseSelect.Trigger>\n <BaseSelect.Portal>\n <BaseSelect.Positioner\n alignItemWithTrigger={false}\n sideOffset={4}\n className=\"z-50 outline-none\"\n >\n <BaseSelect.Popup\n data-slot=\"select-popup\"\n className={cn(\n // Same surface as Menu: shadow-md to lift it and a border for when the\n // shadow doesn't show (dark backgrounds, poor screens).\n \"max-h-[min(20rem,var(--available-height))] min-w-(--anchor-width) origin-(--transform-origin) overflow-y-auto\",\n \"rounded-
|
|
1
|
+
{"version":3,"file":"select.js","sources":["../../../src/components/select/select.tsx"],"sourcesContent":["import { Select as BaseSelect } from \"@base-ui/react/select\";\nimport { type ReactNode, type Ref, useLayoutEffect } from \"react\";\n\nimport { CheckIcon, ChevronDownIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useFieldContext } from \"../field/field-context\";\nimport {\n type ControlSize,\n controlBox,\n controlHeight,\n controlIcon,\n controlRow,\n triggerControlStates,\n} from \"../input/control-styles\";\n\nexport interface SelectOption<Value> {\n value: Value;\n label: ReactNode;\n disabled?: boolean;\n}\n\nexport interface SelectProps<Value>\n extends Omit<\n BaseSelect.Root.Props<Value, false>,\n \"multiple\" | \"items\" | \"children\"\n > {\n /** The options. Also lets the trigger show the label of the chosen value. */\n items?: readonly SelectOption<Value>[];\n /** Custom options (`SelectItem`) instead of `items`. */\n children?: ReactNode;\n /** Shown in text-secondary while nothing is chosen: \"Elige una opción\". */\n placeholder?: ReactNode;\n /** `default` 40px, `small` 36px: the same heights as Input and Button. */\n size?: ControlSize;\n /** Red 2px border and `aria-invalid`. The message goes under it, in `Field`. */\n invalid?: boolean;\n /** Content icon before the value. The chevron is structure, not a prop. */\n leadingIcon?: ReactNode;\n /** Applied to the trigger. */\n className?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * Dropdown with a styled list (Base UI Select). The trigger has exactly the geometry\n * of Input, so a field and a select in the same row only differ by the chevron.\n *\n * The open list is not in Figma on purpose. Prefer `NativeSelect` on mobile and\n * whenever the options are plain text: it opens the operating system's picker and\n * is accessible out of the box. Use this one on desktop screens (the clinics admin)\n * where the list has to look like MediCore. Options that an `<option>` can't draw\n * (a doctor with photo and licence number) are a `Menu`, not this.\n *\n * Name it with `Field`'s label, or with `aria-label` when there is no visible label.\n */\nexport function Select<Value>({\n items,\n children,\n placeholder,\n size = \"default\",\n invalid = false,\n leadingIcon,\n className,\n disabled,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ref,\n ...props\n}: SelectProps<Value>): React.JSX.Element {\n const setNativeLabel = useFieldContext()?.setNativeLabel;\n\n // A native <label> around a button would open the list on click: Field renders a\n // non-native label while a Select is inside.\n useLayoutEffect(() => {\n if (!setNativeLabel) {\n return undefined;\n }\n setNativeLabel(false);\n return () => setNativeLabel(true);\n }, [setNativeLabel]);\n\n return (\n <BaseSelect.Root items={items} disabled={disabled} {...props}>\n <BaseSelect.Trigger\n ref={ref}\n data-slot=\"select\"\n // Only when given: an explicit undefined would override Field's aria-labelledby.\n {...(ariaLabel !== undefined && { \"aria-label\": ariaLabel })}\n {...(ariaLabelledBy !== undefined && {\n \"aria-labelledby\": ariaLabelledBy,\n })}\n aria-describedby={ariaDescribedBy}\n aria-invalid={invalid && !disabled ? true : undefined}\n className={cn(\n \"group/control cursor-pointer text-start\",\n controlBox,\n controlRow,\n controlHeight[size],\n triggerControlStates,\n className,\n )}\n >\n {leadingIcon && <span className={controlIcon}>{leadingIcon}</span>}\n <BaseSelect.Value\n placeholder={placeholder}\n className=\"min-w-0 flex-1 truncate data-placeholder:text-text-secondary group-data-disabled/control:text-text-disabled\"\n />\n <BaseSelect.Icon className=\"flex shrink-0 text-text-primary group-data-disabled/control:text-text-disabled\">\n <ChevronDownIcon size=\"sm\" />\n </BaseSelect.Icon>\n </BaseSelect.Trigger>\n <BaseSelect.Portal>\n <BaseSelect.Positioner\n alignItemWithTrigger={false}\n sideOffset={4}\n className=\"z-50 outline-none\"\n >\n <BaseSelect.Popup\n data-slot=\"select-popup\"\n className={cn(\n // Same surface as Menu: shadow-md to lift it and a border for when the\n // shadow doesn't show (dark backgrounds, poor screens).\n \"max-h-[min(20rem,var(--available-height))] min-w-(--anchor-width) origin-(--transform-origin) overflow-y-auto\",\n \"rounded-xl border border-border-subtle bg-background p-1.5 shadow-lg outline-none\",\n \"transition-[opacity,scale] duration-150 motion-reduce:transition-none\",\n \"data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0\",\n )}\n >\n <BaseSelect.List>\n {children ??\n items?.map((item) => (\n <SelectItem\n key={String(item.value)}\n value={item.value}\n disabled={item.disabled}\n >\n {item.label}\n </SelectItem>\n ))}\n </BaseSelect.List>\n </BaseSelect.Popup>\n </BaseSelect.Positioner>\n </BaseSelect.Portal>\n </BaseSelect.Root>\n );\n}\n\nexport interface SelectItemProps\n extends Omit<BaseSelect.Item.Props, \"className\"> {\n className?: string;\n}\n\n/**\n * One option of `Select`: 40px rows like MenuItem (a list is used with a finger on\n * it), surface on hover / arrow keys, a check on the chosen one.\n */\nexport function SelectItem({\n className,\n children,\n ...props\n}: SelectItemProps): React.JSX.Element {\n return (\n <BaseSelect.Item\n data-slot=\"select-item\"\n className={cn(\n \"flex h-control-md cursor-pointer select-none items-center gap-3 rounded-lg px-3 text-label-default text-text-primary outline-none\",\n \"data-highlighted:bg-surface focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-inset\",\n \"data-disabled:cursor-not-allowed data-disabled:text-text-disabled\",\n className,\n )}\n {...props}\n >\n <BaseSelect.ItemText className=\"min-w-0 flex-1 truncate\">\n {children}\n </BaseSelect.ItemText>\n <BaseSelect.ItemIndicator className=\"flex shrink-0 text-text-brand\">\n <CheckIcon size=\"sm\" />\n </BaseSelect.ItemIndicator>\n </BaseSelect.Item>\n );\n}\n"],"names":["BaseSelect"],"mappings":";;;;;;;;AA0DO,SAAS,OAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA0C;AACxC,QAAM,iBAAiB,mBAAmB;AAI1C,kBAAgB,MAAM;AACpB,QAAI,CAAC,gBAAgB;AACnB,aAAO;AAAA,IACT;AACA,mBAAe,KAAK;AACpB,WAAO,MAAM,eAAe,IAAI;AAAA,EAClC,GAAG,CAAC,cAAc,CAAC;AAEnB,8BACGA,SAAW,MAAX,EAAgB,OAAc,UAAqB,GAAG,OACrD,UAAA;AAAA,IAAA;AAAA,MAACA,SAAW;AAAA,MAAX;AAAA,QACC;AAAA,QACA,aAAU;AAAA,QAET,GAAI,cAAc,UAAa,EAAE,cAAc,UAAA;AAAA,QAC/C,GAAI,mBAAmB,UAAa;AAAA,UACnC,mBAAmB;AAAA,QAAA;AAAA,QAErB,oBAAkB;AAAA,QAClB,gBAAc,WAAW,CAAC,WAAW,OAAO;AAAA,QAC5C,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,cAAc,IAAI;AAAA,UAClB;AAAA,UACA;AAAA,QAAA;AAAA,QAGD,UAAA;AAAA,UAAA,eAAe,oBAAC,QAAA,EAAK,WAAW,aAAc,UAAA,aAAY;AAAA,UAC3D;AAAA,YAACA,SAAW;AAAA,YAAX;AAAA,cACC;AAAA,cACA,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,UAEZ,oBAACA,SAAW,MAAX,EAAgB,WAAU,kFACzB,UAAA,oBAAC,iBAAA,EAAgB,MAAK,KAAA,CAAK,EAAA,CAC7B;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,oBAACA,SAAW,QAAX,EACC,UAAA;AAAA,MAACA,SAAW;AAAA,MAAX;AAAA,QACC,sBAAsB;AAAA,QACtB,YAAY;AAAA,QACZ,WAAU;AAAA,QAEV,UAAA;AAAA,UAACA,SAAW;AAAA,UAAX;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,YAAA;AAAA,YAGF,UAAA,oBAACA,SAAW,MAAX,EACE,sBACC,OAAO,IAAI,CAAC,SACV;AAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,OAAO,KAAK;AAAA,gBACZ,UAAU,KAAK;AAAA,gBAEd,UAAA,KAAK;AAAA,cAAA;AAAA,cAJD,OAAO,KAAK,KAAK;AAAA,YAAA,CAMzB,EAAA,CACL;AAAA,UAAA;AAAA,QAAA;AAAA,MACF;AAAA,IAAA,EACF,CACF;AAAA,EAAA,GACF;AAEJ;AAWO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuC;AACrC,SACE;AAAA,IAACA,SAAW;AAAA,IAAX;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA,oBAACA,SAAW,UAAX,EAAoB,WAAU,2BAC5B,UACH;AAAA,QACA,oBAACA,SAAW,eAAX,EAAyB,WAAU,iCAClC,UAAA,oBAAC,WAAA,EAAU,MAAK,KAAA,CAAK,EAAA,CACvB;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|