@mediquo/medicore 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/components/advisor-contact/advisor-contact.js +1 -1
  2. package/dist/components/advisor-contact/advisor-contact.js.map +1 -1
  3. package/dist/components/bottom-bar/bottom-bar.js +1 -1
  4. package/dist/components/bottom-bar/bottom-bar.js.map +1 -1
  5. package/dist/components/breadcrumb/breadcrumb.js +1 -1
  6. package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
  7. package/dist/components/button/button.d.ts +2 -1
  8. package/dist/components/button/button.js +8 -4
  9. package/dist/components/button/button.js.map +1 -1
  10. package/dist/components/card/card.d.ts +2 -1
  11. package/dist/components/card/card.js +10 -3
  12. package/dist/components/card/card.js.map +1 -1
  13. package/dist/components/card-article/card-article.js +6 -2
  14. package/dist/components/card-article/card-article.js.map +1 -1
  15. package/dist/components/card-professional-mini/card-professional-mini.js +1 -1
  16. package/dist/components/card-professional-mini/card-professional-mini.js.map +1 -1
  17. package/dist/components/card-program/card-program.js +9 -3
  18. package/dist/components/card-program/card-program.js.map +1 -1
  19. package/dist/components/chat/chat-composer.js +1 -1
  20. package/dist/components/chat/chat-composer.js.map +1 -1
  21. package/dist/components/chat/chat-conversation-row.js +1 -1
  22. package/dist/components/chat/chat-conversation-row.js.map +1 -1
  23. package/dist/components/checkbox/checkbox.js +2 -1
  24. package/dist/components/checkbox/checkbox.js.map +1 -1
  25. package/dist/components/chip/chip.js +2 -2
  26. package/dist/components/chip/chip.js.map +1 -1
  27. package/dist/components/day-cell/day-cell.js +2 -1
  28. package/dist/components/day-cell/day-cell.js.map +1 -1
  29. package/dist/components/hover-arrow/hover-arrow.d.ts +4 -0
  30. package/dist/components/hover-arrow/hover-arrow.js +18 -0
  31. package/dist/components/hover-arrow/hover-arrow.js.map +1 -0
  32. package/dist/components/hover-arrow/index.d.ts +1 -0
  33. package/dist/components/input/control-styles.d.ts +2 -2
  34. package/dist/components/input/control-styles.js +2 -2
  35. package/dist/components/input/control-styles.js.map +1 -1
  36. package/dist/components/link/link.d.ts +2 -1
  37. package/dist/components/link/link.js +8 -5
  38. package/dist/components/link/link.js.map +1 -1
  39. package/dist/components/list-row/list-row.js +1 -1
  40. package/dist/components/list-row/list-row.js.map +1 -1
  41. package/dist/components/menu/menu.js +1 -1
  42. package/dist/components/menu/menu.js.map +1 -1
  43. package/dist/components/modal/modal.js +1 -1
  44. package/dist/components/modal/modal.js.map +1 -1
  45. package/dist/components/nav-item/nav-item.js +2 -2
  46. package/dist/components/nav-item/nav-item.js.map +1 -1
  47. package/dist/components/otp-input/otp-input.js +3 -2
  48. package/dist/components/otp-input/otp-input.js.map +1 -1
  49. package/dist/components/radio/radio.js +2 -1
  50. package/dist/components/radio/radio.js.map +1 -1
  51. package/dist/components/segmented-control/segmented-control.js +1 -1
  52. package/dist/components/segmented-control/segmented-control.js.map +1 -1
  53. package/dist/components/select/select.js +1 -1
  54. package/dist/components/select/select.js.map +1 -1
  55. package/dist/components/selection-card/selection-card.js +2 -1
  56. package/dist/components/selection-card/selection-card.js.map +1 -1
  57. package/dist/components/switch/switch.js +3 -2
  58. package/dist/components/switch/switch.js.map +1 -1
  59. package/dist/components/table/table.js +3 -3
  60. package/dist/components/table/table.js.map +1 -1
  61. package/dist/components/tabs/tabs.js +3 -3
  62. package/dist/components/tabs/tabs.js.map +1 -1
  63. package/dist/components/time-slot/time-slot.js +2 -1
  64. package/dist/components/time-slot/time-slot.js.map +1 -1
  65. package/dist/components/toast/toast.js +1 -1
  66. package/dist/components/toast/toast.js.map +1 -1
  67. package/dist/components/tooltip/tooltip.js +1 -1
  68. package/dist/components/tooltip/tooltip.js.map +1 -1
  69. package/dist/components/video/video-control-button.js +1 -1
  70. package/dist/components/video/video-control-button.js.map +1 -1
  71. package/dist/icons/create-icon.d.ts +1 -0
  72. package/dist/icons/create-icon.js +7 -1
  73. package/dist/icons/create-icon.js.map +1 -1
  74. package/dist/index.d.ts +1 -0
  75. package/dist/index.js +2 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/styles/tokens.css +3 -0
  78. package/dist/styles/utilities.css +121 -0
  79. package/package.json +1 -1
@@ -8,7 +8,7 @@ import { Spinner } from "../spinner/spinner.js";
8
8
  const navItemVariants = cva(
9
9
  [
10
10
  "group/nav-item relative flex min-w-0 cursor-pointer select-none items-center gap-2.5 rounded-md p-2.5",
11
- "text-body-small text-text-secondary no-underline transition-colors",
11
+ "text-body-small text-text-secondary no-underline interactive",
12
12
  "[&_svg]:size-icon-md [&_svg]:shrink-0",
13
13
  // Inside the item, not around it: in navigation there is no room outside. It is
14
14
  // added on top of hover, so with the keyboard it shows even without the mouse.
@@ -96,7 +96,7 @@ function NavItem({
96
96
  "data-slot": "nav-item-tooltip",
97
97
  className: cn(
98
98
  "max-w-60 rounded-md bg-surface-inverse px-2 py-1.5 text-label-small text-text-inverse",
99
- "origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out",
99
+ "origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out-soft",
100
100
  "data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:opacity-0",
101
101
  "data-instant:transition-none motion-reduce:transition-none"
102
102
  ),
@@ -1 +1 @@
1
- {"version":3,"file":"nav-item.js","sources":["../../../src/components/nav-item/nav-item.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { Tooltip } from \"@base-ui/react/tooltip\";\nimport type { BaseUIEvent } from \"@base-ui/react/types\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva } from \"class-variance-authority\";\nimport type { MouseEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Spinner } from \"../spinner\";\n\nexport const navItemVariants = cva(\n [\n \"group/nav-item relative flex min-w-0 cursor-pointer select-none items-center gap-2.5 rounded-md p-2.5\",\n \"text-body-small text-text-secondary no-underline transition-colors\",\n \"[&_svg]:size-icon-md [&_svg]:shrink-0\",\n // Inside the item, not around it: in navigation there is no room outside. It is\n // added on top of hover, so with the keyboard it shows even without the mouse.\n \"focus-ring focus-visible:-outline-offset-2\",\n // Figma says hover = `surface`, but every nav bar already sits on `surface`, where\n // it would be invisible: the next step up keeps the hover visible.\n \"not-aria-disabled:not-aria-busy:not-aria-[current=page]:hover:bg-surface-raised\",\n \"not-aria-disabled:not-aria-busy:not-aria-[current=page]:hover:text-text-primary\",\n // Active: brand-subtle pill + brand text + the 3px rail (the non-colour cue).\n \"aria-[current=page]:bg-brand-subtle aria-[current=page]:text-text-brand\",\n \"aria-disabled:cursor-not-allowed aria-disabled:text-text-disabled\",\n \"aria-busy:cursor-progress aria-busy:text-text-secondary\",\n ],\n {\n variants: {\n collapsed: {\n true: \"size-10 shrink-0 justify-center\",\n false: \"w-full\",\n },\n },\n defaultVariants: {\n collapsed: false,\n },\n },\n);\n\nexport interface NavItemState extends Record<string, unknown> {\n active: boolean;\n collapsed: boolean;\n disabled: boolean;\n loading: boolean;\n}\n\nexport interface NavItemProps\n extends Omit<\n useRender.ComponentProps<\"a\", NavItemState>,\n \"className\" | \"children\"\n > {\n /** A MediCore icon (`<Home02Icon />`). It is sized to 20 by the item. */\n icon: ReactNode;\n /** The section name (\"Inicio\"). Always required: it stays in the DOM when collapsed. */\n children: ReactNode;\n /**\n * The section on screen: `aria-current=\"page\"`, brand pill and rail. Leave it out if\n * the router link already sets `aria-current=\"page\"` (TanStack Router's `<Link>`\n * does): the styles follow the attribute, not the prop.\n */\n active?: boolean;\n /**\n * Figma \"Show Label\" off: icon only, 40×40. The label becomes screen-reader text and\n * shows in a tooltip on hover and keyboard focus.\n */\n collapsed?: boolean;\n /** Section not available: `aria-disabled`, out of the tab order, ignores clicks. */\n disabled?: boolean;\n /**\n * The section is loading after a click: the icon turns into a spinner, the label\n * stays and further clicks are ignored (without it people click twice).\n */\n loading?: boolean;\n className?: string;\n}\n\n// Cancels the navigation and skips the handlers passed in `props`.\nconst preventNavigation = (\n event: BaseUIEvent<MouseEvent<HTMLAnchorElement>>,\n): void => {\n event.preventDefault();\n event.preventBaseUIHandler();\n};\n\n/**\n * Navigation item: icon + label. The atom of `SidebarItem` (and any other nav bar).\n * Renders an `<a>`; pass the router's link with `render`:\n *\n * ```tsx\n * <NavItem icon={<Home02Icon />} render={<Link to=\"/\" activeOptions={{ exact: true }} />}>\n * Inicio\n * </NavItem>\n * ```\n *\n * - The active item is not told apart by colour alone (brand-subtle on surface is\n * 1.22:1): it carries a 3px rail on the left. Mark it with `active` or let the\n * router set `aria-current=\"page\"`.\n * - Collapsed (icon only) keeps the label in the DOM as its accessible name and adds a\n * tooltip with it.\n * - Hover and focus are CSS: the focus ring is inside the item and adds to the hover.\n */\nexport function NavItem({\n icon,\n children,\n active = false,\n collapsed = false,\n disabled = false,\n loading = false,\n className,\n render,\n ...props\n}: NavItemProps): React.JSX.Element {\n const state: NavItemState = { active, collapsed, disabled, loading };\n\n const ownProps: useRender.ElementProps<\"a\"> & { \"data-slot\": string } = {\n \"data-slot\": \"nav-item\",\n className: cn(navItemVariants({ collapsed }), className),\n \"aria-current\": active ? \"page\" : undefined,\n children: (\n <>\n <span\n aria-hidden\n data-slot=\"nav-item-rail\"\n className=\"absolute inset-y-0 left-0 hidden w-0.75 rounded-xs bg-text-brand group-aria-[current=page]/nav-item:block\"\n />\n {loading ? <Spinner size={20} label={null} /> : icon}\n <span\n data-slot=\"nav-item-label\"\n className={collapsed ? \"sr-only\" : \"min-w-0 flex-1\"}\n >\n {children}\n </span>\n </>\n ),\n };\n\n // Last, so they win over whatever came in `props`.\n const blockedProps: useRender.ElementProps<\"a\"> = disabled\n ? {\n \"aria-disabled\": true,\n tabIndex: -1,\n onClick: preventNavigation,\n }\n : loading\n ? { \"aria-busy\": true, onClick: preventNavigation }\n : {};\n\n const element = useRender({\n defaultTagName: \"a\",\n render,\n state,\n props: mergeProps<\"a\">(ownProps, props, blockedProps),\n });\n\n if (!collapsed) {\n return element;\n }\n\n // Same look as MediCore's `Tooltip`, but to the right (it only opens top/bottom) and\n // without `aria-describedby`: the label is already the link's name, and describing\n // it with the same words would make screen readers say it twice.\n return (\n <Tooltip.Root disabled={disabled}>\n <Tooltip.Trigger render={element} />\n <Tooltip.Portal>\n <Tooltip.Positioner side=\"right\" sideOffset={8} className=\"z-70\">\n <Tooltip.Popup\n data-slot=\"nav-item-tooltip\"\n className={cn(\n \"max-w-60 rounded-md bg-surface-inverse px-2 py-1.5 text-label-small text-text-inverse\",\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:opacity-0\",\n \"data-instant:transition-none motion-reduce:transition-none\",\n )}\n >\n <Tooltip.Arrow\n className={cn(\n \"h-2.5 w-1.5 bg-surface-inverse\",\n \"data-[side=right]:right-full data-[side=right]:[clip-path:polygon(100%_0,100%_100%,0_50%)]\",\n \"data-[side=left]:left-full data-[side=left]:[clip-path:polygon(0_0,100%_50%,0_100%)]\",\n )}\n />\n {children}\n </Tooltip.Popup>\n </Tooltip.Positioner>\n </Tooltip.Portal>\n </Tooltip.Root>\n );\n}\n"],"names":[],"mappings":";;;;;;;AAUO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,WAAW;AAAA,QACT,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,WAAW;AAAA,IAAA;AAAA,EACb;AAEJ;AAwCA,MAAM,oBAAoB,CACxB,UACS;AACT,QAAM,eAAA;AACN,QAAM,qBAAA;AACR;AAmBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoC;AAClC,QAAM,QAAsB,EAAE,QAAQ,WAAW,UAAU,QAAA;AAE3D,QAAM,WAAkE;AAAA,IACtE,aAAa;AAAA,IACb,WAAW,GAAG,gBAAgB,EAAE,UAAA,CAAW,GAAG,SAAS;AAAA,IACvD,gBAAgB,SAAS,SAAS;AAAA,IAClC,UACE,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,eAAW;AAAA,UACX,aAAU;AAAA,UACV,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,MAEX,UAAU,oBAAC,SAAA,EAAQ,MAAM,IAAI,OAAO,MAAM,IAAK;AAAA,MAChD;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,aAAU;AAAA,UACV,WAAW,YAAY,YAAY;AAAA,UAElC;AAAA,QAAA;AAAA,MAAA;AAAA,IACH,EAAA,CACF;AAAA,EAAA;AAKJ,QAAM,eAA4C,WAC9C;AAAA,IACE,iBAAiB;AAAA,IACjB,UAAU;AAAA,IACV,SAAS;AAAA,EAAA,IAEX,UACE,EAAE,aAAa,MAAM,SAAS,kBAAA,IAC9B,CAAA;AAEN,QAAM,UAAU,UAAU;AAAA,IACxB,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA,OAAO,WAAgB,UAAU,OAAO,YAAY;AAAA,EAAA,CACrD;AAED,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AAKA,SACE,qBAAC,QAAQ,MAAR,EAAa,UACZ,UAAA;AAAA,IAAA,oBAAC,QAAQ,SAAR,EAAgB,QAAQ,QAAA,CAAS;AAAA,IAClC,oBAAC,QAAQ,QAAR,EACC,UAAA,oBAAC,QAAQ,YAAR,EAAmB,MAAK,SAAQ,YAAY,GAAG,WAAU,QACxD,UAAA;AAAA,MAAC,QAAQ;AAAA,MAAR;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAGF,UAAA;AAAA,UAAA;AAAA,YAAC,QAAQ;AAAA,YAAR;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cAAA;AAAA,YACF;AAAA,UAAA;AAAA,UAED;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAEL,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"nav-item.js","sources":["../../../src/components/nav-item/nav-item.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { Tooltip } from \"@base-ui/react/tooltip\";\nimport type { BaseUIEvent } from \"@base-ui/react/types\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva } from \"class-variance-authority\";\nimport type { MouseEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Spinner } from \"../spinner\";\n\nexport const navItemVariants = cva(\n [\n \"group/nav-item relative flex min-w-0 cursor-pointer select-none items-center gap-2.5 rounded-md p-2.5\",\n \"text-body-small text-text-secondary no-underline interactive\",\n \"[&_svg]:size-icon-md [&_svg]:shrink-0\",\n // Inside the item, not around it: in navigation there is no room outside. It is\n // added on top of hover, so with the keyboard it shows even without the mouse.\n \"focus-ring focus-visible:-outline-offset-2\",\n // Figma says hover = `surface`, but every nav bar already sits on `surface`, where\n // it would be invisible: the next step up keeps the hover visible.\n \"not-aria-disabled:not-aria-busy:not-aria-[current=page]:hover:bg-surface-raised\",\n \"not-aria-disabled:not-aria-busy:not-aria-[current=page]:hover:text-text-primary\",\n // Active: brand-subtle pill + brand text + the 3px rail (the non-colour cue).\n \"aria-[current=page]:bg-brand-subtle aria-[current=page]:text-text-brand\",\n \"aria-disabled:cursor-not-allowed aria-disabled:text-text-disabled\",\n \"aria-busy:cursor-progress aria-busy:text-text-secondary\",\n ],\n {\n variants: {\n collapsed: {\n true: \"size-10 shrink-0 justify-center\",\n false: \"w-full\",\n },\n },\n defaultVariants: {\n collapsed: false,\n },\n },\n);\n\nexport interface NavItemState extends Record<string, unknown> {\n active: boolean;\n collapsed: boolean;\n disabled: boolean;\n loading: boolean;\n}\n\nexport interface NavItemProps\n extends Omit<\n useRender.ComponentProps<\"a\", NavItemState>,\n \"className\" | \"children\"\n > {\n /** A MediCore icon (`<Home02Icon />`). It is sized to 20 by the item. */\n icon: ReactNode;\n /** The section name (\"Inicio\"). Always required: it stays in the DOM when collapsed. */\n children: ReactNode;\n /**\n * The section on screen: `aria-current=\"page\"`, brand pill and rail. Leave it out if\n * the router link already sets `aria-current=\"page\"` (TanStack Router's `<Link>`\n * does): the styles follow the attribute, not the prop.\n */\n active?: boolean;\n /**\n * Figma \"Show Label\" off: icon only, 40×40. The label becomes screen-reader text and\n * shows in a tooltip on hover and keyboard focus.\n */\n collapsed?: boolean;\n /** Section not available: `aria-disabled`, out of the tab order, ignores clicks. */\n disabled?: boolean;\n /**\n * The section is loading after a click: the icon turns into a spinner, the label\n * stays and further clicks are ignored (without it people click twice).\n */\n loading?: boolean;\n className?: string;\n}\n\n// Cancels the navigation and skips the handlers passed in `props`.\nconst preventNavigation = (\n event: BaseUIEvent<MouseEvent<HTMLAnchorElement>>,\n): void => {\n event.preventDefault();\n event.preventBaseUIHandler();\n};\n\n/**\n * Navigation item: icon + label. The atom of `SidebarItem` (and any other nav bar).\n * Renders an `<a>`; pass the router's link with `render`:\n *\n * ```tsx\n * <NavItem icon={<Home02Icon />} render={<Link to=\"/\" activeOptions={{ exact: true }} />}>\n * Inicio\n * </NavItem>\n * ```\n *\n * - The active item is not told apart by colour alone (brand-subtle on surface is\n * 1.22:1): it carries a 3px rail on the left. Mark it with `active` or let the\n * router set `aria-current=\"page\"`.\n * - Collapsed (icon only) keeps the label in the DOM as its accessible name and adds a\n * tooltip with it.\n * - Hover and focus are CSS: the focus ring is inside the item and adds to the hover.\n */\nexport function NavItem({\n icon,\n children,\n active = false,\n collapsed = false,\n disabled = false,\n loading = false,\n className,\n render,\n ...props\n}: NavItemProps): React.JSX.Element {\n const state: NavItemState = { active, collapsed, disabled, loading };\n\n const ownProps: useRender.ElementProps<\"a\"> & { \"data-slot\": string } = {\n \"data-slot\": \"nav-item\",\n className: cn(navItemVariants({ collapsed }), className),\n \"aria-current\": active ? \"page\" : undefined,\n children: (\n <>\n <span\n aria-hidden\n data-slot=\"nav-item-rail\"\n className=\"absolute inset-y-0 left-0 hidden w-0.75 rounded-xs bg-text-brand group-aria-[current=page]/nav-item:block\"\n />\n {loading ? <Spinner size={20} label={null} /> : icon}\n <span\n data-slot=\"nav-item-label\"\n className={collapsed ? \"sr-only\" : \"min-w-0 flex-1\"}\n >\n {children}\n </span>\n </>\n ),\n };\n\n // Last, so they win over whatever came in `props`.\n const blockedProps: useRender.ElementProps<\"a\"> = disabled\n ? {\n \"aria-disabled\": true,\n tabIndex: -1,\n onClick: preventNavigation,\n }\n : loading\n ? { \"aria-busy\": true, onClick: preventNavigation }\n : {};\n\n const element = useRender({\n defaultTagName: \"a\",\n render,\n state,\n props: mergeProps<\"a\">(ownProps, props, blockedProps),\n });\n\n if (!collapsed) {\n return element;\n }\n\n // Same look as MediCore's `Tooltip`, but to the right (it only opens top/bottom) and\n // without `aria-describedby`: the label is already the link's name, and describing\n // it with the same words would make screen readers say it twice.\n return (\n <Tooltip.Root disabled={disabled}>\n <Tooltip.Trigger render={element} />\n <Tooltip.Portal>\n <Tooltip.Positioner side=\"right\" sideOffset={8} className=\"z-70\">\n <Tooltip.Popup\n data-slot=\"nav-item-tooltip\"\n className={cn(\n \"max-w-60 rounded-md bg-surface-inverse px-2 py-1.5 text-label-small text-text-inverse\",\n \"origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out-soft\",\n \"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:opacity-0\",\n \"data-instant:transition-none motion-reduce:transition-none\",\n )}\n >\n <Tooltip.Arrow\n className={cn(\n \"h-2.5 w-1.5 bg-surface-inverse\",\n \"data-[side=right]:right-full data-[side=right]:[clip-path:polygon(100%_0,100%_100%,0_50%)]\",\n \"data-[side=left]:left-full data-[side=left]:[clip-path:polygon(0_0,100%_50%,0_100%)]\",\n )}\n />\n {children}\n </Tooltip.Popup>\n </Tooltip.Positioner>\n </Tooltip.Portal>\n </Tooltip.Root>\n );\n}\n"],"names":[],"mappings":";;;;;;;AAUO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,WAAW;AAAA,QACT,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,WAAW;AAAA,IAAA;AAAA,EACb;AAEJ;AAwCA,MAAM,oBAAoB,CACxB,UACS;AACT,QAAM,eAAA;AACN,QAAM,qBAAA;AACR;AAmBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoC;AAClC,QAAM,QAAsB,EAAE,QAAQ,WAAW,UAAU,QAAA;AAE3D,QAAM,WAAkE;AAAA,IACtE,aAAa;AAAA,IACb,WAAW,GAAG,gBAAgB,EAAE,UAAA,CAAW,GAAG,SAAS;AAAA,IACvD,gBAAgB,SAAS,SAAS;AAAA,IAClC,UACE,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,eAAW;AAAA,UACX,aAAU;AAAA,UACV,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,MAEX,UAAU,oBAAC,SAAA,EAAQ,MAAM,IAAI,OAAO,MAAM,IAAK;AAAA,MAChD;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,aAAU;AAAA,UACV,WAAW,YAAY,YAAY;AAAA,UAElC;AAAA,QAAA;AAAA,MAAA;AAAA,IACH,EAAA,CACF;AAAA,EAAA;AAKJ,QAAM,eAA4C,WAC9C;AAAA,IACE,iBAAiB;AAAA,IACjB,UAAU;AAAA,IACV,SAAS;AAAA,EAAA,IAEX,UACE,EAAE,aAAa,MAAM,SAAS,kBAAA,IAC9B,CAAA;AAEN,QAAM,UAAU,UAAU;AAAA,IACxB,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA,OAAO,WAAgB,UAAU,OAAO,YAAY;AAAA,EAAA,CACrD;AAED,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AAKA,SACE,qBAAC,QAAQ,MAAR,EAAa,UACZ,UAAA;AAAA,IAAA,oBAAC,QAAQ,SAAR,EAAgB,QAAQ,QAAA,CAAS;AAAA,IAClC,oBAAC,QAAQ,QAAR,EACC,UAAA,oBAAC,QAAQ,YAAR,EAAmB,MAAK,SAAQ,YAAY,GAAG,WAAU,QACxD,UAAA;AAAA,MAAC,QAAQ;AAAA,MAAR;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAGF,UAAA;AAAA,UAAA;AAAA,YAAC,QAAQ;AAAA,YAAR;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cAAA;AAAA,YACF;AAAA,UAAA;AAAA,UAED;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAEL,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
@@ -63,7 +63,7 @@ function OTPInput({
63
63
  "data-slot": "otp-input",
64
64
  className: cn("inline-flex max-w-full flex-col", className),
65
65
  children: [
66
- /* @__PURE__ */ jsxs("div", { className: "relative flex gap-2", children: [
66
+ /* @__PURE__ */ jsxs("div", { className: "group/otp relative flex gap-2", children: [
67
67
  cells.map((position) => {
68
68
  const digit = code.charAt(position);
69
69
  const active = focused && !validating && !disabled && position === activeCell;
@@ -74,7 +74,8 @@ function OTPInput({
74
74
  "data-slot": "otp-input-cell",
75
75
  className: cn(
76
76
  "flex h-14 w-12 shrink-0 items-center justify-center rounded-md border border-border-input bg-background text-heading-h3 text-text-primary",
77
- "ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]",
77
+ "ring-1 ring-transparent ring-inset interactive",
78
+ !hasError && !active && !disabled && "group-hover/otp:border-border-strong",
78
79
  hasError && "border-error ring-error",
79
80
  active && "border-border-focus ring-border-focus",
80
81
  disabled && "border-border bg-surface text-text-disabled"
@@ -1 +1 @@
1
- {"version":3,"file":"otp-input.js","sources":["../../../src/components/otp-input/otp-input.tsx"],"sourcesContent":["import { Input as BaseInput } from \"@base-ui/react/input\";\nimport { type ReactNode, useId, useState } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { Spinner } from \"../spinner\";\n\nexport interface OTPInputProps\n extends Omit<\n BaseInput.Props,\n | \"className\"\n | \"value\"\n | \"defaultValue\"\n | \"onValueChange\"\n | \"type\"\n | \"size\"\n | \"maxLength\"\n | \"pattern\"\n > {\n /** Number of digits. The Figma spec is 6 (WhatsApp / SMS code). */\n length?: number;\n /** The code typed so far (digits only). Use when controlled. */\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /**\n * Called when the last digit is typed or a full code is pasted. Check the code here\n * and set `validating`: there is no submit button.\n */\n onComplete?: (value: string) => void;\n /** State=Validating: spinner + message, and the code can't be edited meanwhile. */\n validating?: boolean;\n /** Text next to the spinner: \"Comprobando el código…\". Defaults to \"Cargando\". */\n validatingLabel?: ReactNode;\n /**\n * State=Error: every cell in red and this message under them. Say what to do: \"El\n * código no es correcto. Revisa el mensaje o pide uno nuevo.\"\n */\n error?: ReactNode;\n className?: string;\n}\n\nfunction onlyDigits(text: string, length: number): string {\n return text.replace(/\\D/g, \"\").slice(0, length);\n}\n\n/**\n * Verification code sent by WhatsApp or SMS. Drawn as six cells but it is ONE input\n * (`inputmode=\"numeric\" autocomplete=\"one-time-code\"`), laid transparently over the\n * cells: the phone offers the code from the message in one tap, paste works, and a\n * screen reader announces a single field (\"Código de verificación\"), not six.\n *\n * Base UI's OTPField renders one input per cell, which is exactly what the spec rules\n * out, so this is a plain input driving the cells.\n *\n * - The code is checked on its own when complete (`onComplete` → `validating`).\n * - The message under the cells (validating / error) is `aria-live=\"polite\"`.\n * - The accessible name defaults to \"Código de verificación\"; the heading above the\n * cells (\"Escribe el código\") is not a label.\n */\nexport function OTPInput({\n length = 6,\n value,\n defaultValue,\n onValueChange,\n onComplete,\n validating = false,\n validatingLabel,\n error,\n disabled = false,\n readOnly,\n className,\n onFocus,\n onBlur,\n onChange,\n onPaste,\n onSelect,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n ...props\n}: OTPInputProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const messageId = useId();\n const [uncontrolled, setUncontrolled] = useState(() =>\n onlyDigits(defaultValue ?? \"\", length),\n );\n const [focused, setFocused] = useState(false);\n const code = value === undefined ? uncontrolled : onlyDigits(value, length);\n const hasError = error !== undefined && error !== null && error !== false;\n const cells = Array.from({ length }, (_, position) => position);\n const activeCell = Math.min(code.length, length - 1);\n\n function commit(next: string): void {\n if (next === code) {\n return;\n }\n if (value === undefined) {\n setUncontrolled(next);\n }\n onValueChange?.(next);\n if (next.length === length) {\n onComplete?.(next);\n }\n }\n\n // One input, so the caret always sits after the last digit: typing appends and\n // backspace removes the last one. Selecting everything (to retype) is allowed.\n function keepCaretAtEnd(input: HTMLInputElement): void {\n const end = input.value.length;\n const selectsAll = input.selectionStart === 0 && input.selectionEnd === end;\n if (!selectsAll && input.selectionStart !== end) {\n input.setSelectionRange(end, end);\n }\n }\n\n return (\n <div\n data-slot=\"otp-input\"\n className={cn(\"inline-flex max-w-full flex-col\", className)}\n >\n <div className=\"relative flex gap-2\">\n {cells.map((position) => {\n const digit = code.charAt(position);\n const active =\n focused && !validating && !disabled && position === activeCell;\n return (\n <div\n key={position}\n aria-hidden\n data-slot=\"otp-input-cell\"\n className={cn(\n \"flex h-14 w-12 shrink-0 items-center justify-center rounded-md border border-border-input bg-background text-heading-h3 text-text-primary\",\n \"ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]\",\n hasError && \"border-error ring-error\",\n active && \"border-border-focus ring-border-focus\",\n disabled && \"border-border bg-surface text-text-disabled\",\n )}\n >\n {digit ||\n (active && (\n <span className=\"h-6 w-0.5 bg-text-primary motion-safe:animate-pulse\" />\n ))}\n </div>\n );\n })}\n <BaseInput\n data-slot=\"otp-input-control\"\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n pattern=\"[0-9]*\"\n maxLength={length}\n spellCheck={false}\n value={code}\n disabled={disabled}\n readOnly={readOnly || validating}\n aria-label={ariaLabel ?? labels.verificationCode}\n aria-describedby={[messageId, ariaDescribedBy]\n .filter(Boolean)\n .join(\" \")}\n aria-invalid={hasError && !disabled ? true : undefined}\n onChange={(event) => {\n commit(onlyDigits(event.currentTarget.value, length));\n onChange?.(event);\n }}\n onPaste={(event) => {\n onPaste?.(event);\n if (event.defaultPrevented) {\n return;\n }\n // Codes are often pasted as \"482 916\" or \"482-916\": keep the digits.\n event.preventDefault();\n commit(onlyDigits(event.clipboardData.getData(\"text\"), length));\n }}\n onFocus={(event) => {\n setFocused(true);\n keepCaretAtEnd(event.currentTarget);\n onFocus?.(event);\n }}\n onBlur={(event) => {\n setFocused(false);\n onBlur?.(event);\n }}\n onSelect={(event) => {\n keepCaretAtEnd(event.currentTarget);\n onSelect?.(event);\n }}\n className=\"absolute inset-0 size-full cursor-text bg-transparent text-heading-h3 text-transparent caret-transparent outline-none selection:bg-transparent disabled:cursor-not-allowed\"\n {...props}\n />\n </div>\n {/* Always in the DOM so the live region exists before its text arrives. */}\n <div\n id={messageId}\n aria-live=\"polite\"\n className=\"w-0 min-w-full text-body-small\"\n >\n {validating ? (\n <p className=\"mt-3 flex items-center gap-2 text-text-secondary\">\n <Spinner size={16} label={null} />\n {validatingLabel ?? labels.loading}\n </p>\n ) : hasError ? (\n <p className=\"mt-3 text-text-error\">{error}</p>\n ) : null}\n </div>\n </div>\n );\n}\n"],"names":["BaseInput"],"mappings":"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"}
1
+ {"version":3,"file":"otp-input.js","sources":["../../../src/components/otp-input/otp-input.tsx"],"sourcesContent":["import { Input as BaseInput } from \"@base-ui/react/input\";\nimport { type ReactNode, useId, useState } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { Spinner } from \"../spinner\";\n\nexport interface OTPInputProps\n extends Omit<\n BaseInput.Props,\n | \"className\"\n | \"value\"\n | \"defaultValue\"\n | \"onValueChange\"\n | \"type\"\n | \"size\"\n | \"maxLength\"\n | \"pattern\"\n > {\n /** Number of digits. The Figma spec is 6 (WhatsApp / SMS code). */\n length?: number;\n /** The code typed so far (digits only). Use when controlled. */\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /**\n * Called when the last digit is typed or a full code is pasted. Check the code here\n * and set `validating`: there is no submit button.\n */\n onComplete?: (value: string) => void;\n /** State=Validating: spinner + message, and the code can't be edited meanwhile. */\n validating?: boolean;\n /** Text next to the spinner: \"Comprobando el código…\". Defaults to \"Cargando\". */\n validatingLabel?: ReactNode;\n /**\n * State=Error: every cell in red and this message under them. Say what to do: \"El\n * código no es correcto. Revisa el mensaje o pide uno nuevo.\"\n */\n error?: ReactNode;\n className?: string;\n}\n\nfunction onlyDigits(text: string, length: number): string {\n return text.replace(/\\D/g, \"\").slice(0, length);\n}\n\n/**\n * Verification code sent by WhatsApp or SMS. Drawn as six cells but it is ONE input\n * (`inputmode=\"numeric\" autocomplete=\"one-time-code\"`), laid transparently over the\n * cells: the phone offers the code from the message in one tap, paste works, and a\n * screen reader announces a single field (\"Código de verificación\"), not six.\n *\n * Base UI's OTPField renders one input per cell, which is exactly what the spec rules\n * out, so this is a plain input driving the cells.\n *\n * - The code is checked on its own when complete (`onComplete` → `validating`).\n * - The message under the cells (validating / error) is `aria-live=\"polite\"`.\n * - The accessible name defaults to \"Código de verificación\"; the heading above the\n * cells (\"Escribe el código\") is not a label.\n */\nexport function OTPInput({\n length = 6,\n value,\n defaultValue,\n onValueChange,\n onComplete,\n validating = false,\n validatingLabel,\n error,\n disabled = false,\n readOnly,\n className,\n onFocus,\n onBlur,\n onChange,\n onPaste,\n onSelect,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n ...props\n}: OTPInputProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const messageId = useId();\n const [uncontrolled, setUncontrolled] = useState(() =>\n onlyDigits(defaultValue ?? \"\", length),\n );\n const [focused, setFocused] = useState(false);\n const code = value === undefined ? uncontrolled : onlyDigits(value, length);\n const hasError = error !== undefined && error !== null && error !== false;\n const cells = Array.from({ length }, (_, position) => position);\n const activeCell = Math.min(code.length, length - 1);\n\n function commit(next: string): void {\n if (next === code) {\n return;\n }\n if (value === undefined) {\n setUncontrolled(next);\n }\n onValueChange?.(next);\n if (next.length === length) {\n onComplete?.(next);\n }\n }\n\n // One input, so the caret always sits after the last digit: typing appends and\n // backspace removes the last one. Selecting everything (to retype) is allowed.\n function keepCaretAtEnd(input: HTMLInputElement): void {\n const end = input.value.length;\n const selectsAll = input.selectionStart === 0 && input.selectionEnd === end;\n if (!selectsAll && input.selectionStart !== end) {\n input.setSelectionRange(end, end);\n }\n }\n\n return (\n <div\n data-slot=\"otp-input\"\n className={cn(\"inline-flex max-w-full flex-col\", className)}\n >\n <div className=\"group/otp relative flex gap-2\">\n {cells.map((position) => {\n const digit = code.charAt(position);\n const active =\n focused && !validating && !disabled && position === activeCell;\n return (\n <div\n key={position}\n aria-hidden\n data-slot=\"otp-input-cell\"\n className={cn(\n \"flex h-14 w-12 shrink-0 items-center justify-center rounded-md border border-border-input bg-background text-heading-h3 text-text-primary\",\n \"ring-1 ring-transparent ring-inset interactive\",\n !hasError &&\n !active &&\n !disabled &&\n \"group-hover/otp:border-border-strong\",\n hasError && \"border-error ring-error\",\n active && \"border-border-focus ring-border-focus\",\n disabled && \"border-border bg-surface text-text-disabled\",\n )}\n >\n {digit ||\n (active && (\n <span className=\"h-6 w-0.5 bg-text-primary motion-safe:animate-pulse\" />\n ))}\n </div>\n );\n })}\n <BaseInput\n data-slot=\"otp-input-control\"\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n pattern=\"[0-9]*\"\n maxLength={length}\n spellCheck={false}\n value={code}\n disabled={disabled}\n readOnly={readOnly || validating}\n aria-label={ariaLabel ?? labels.verificationCode}\n aria-describedby={[messageId, ariaDescribedBy]\n .filter(Boolean)\n .join(\" \")}\n aria-invalid={hasError && !disabled ? true : undefined}\n onChange={(event) => {\n commit(onlyDigits(event.currentTarget.value, length));\n onChange?.(event);\n }}\n onPaste={(event) => {\n onPaste?.(event);\n if (event.defaultPrevented) {\n return;\n }\n // Codes are often pasted as \"482 916\" or \"482-916\": keep the digits.\n event.preventDefault();\n commit(onlyDigits(event.clipboardData.getData(\"text\"), length));\n }}\n onFocus={(event) => {\n setFocused(true);\n keepCaretAtEnd(event.currentTarget);\n onFocus?.(event);\n }}\n onBlur={(event) => {\n setFocused(false);\n onBlur?.(event);\n }}\n onSelect={(event) => {\n keepCaretAtEnd(event.currentTarget);\n onSelect?.(event);\n }}\n className=\"absolute inset-0 size-full cursor-text bg-transparent text-heading-h3 text-transparent caret-transparent outline-none selection:bg-transparent disabled:cursor-not-allowed\"\n {...props}\n />\n </div>\n {/* Always in the DOM so the live region exists before its text arrives. */}\n <div\n id={messageId}\n aria-live=\"polite\"\n className=\"w-0 min-w-full text-body-small\"\n >\n {validating ? (\n <p className=\"mt-3 flex items-center gap-2 text-text-secondary\">\n <Spinner size={16} label={null} />\n {validatingLabel ?? labels.loading}\n </p>\n ) : hasError ? (\n <p className=\"mt-3 text-text-error\">{error}</p>\n ) : null}\n </div>\n </div>\n );\n}\n"],"names":["BaseInput"],"mappings":"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"}
@@ -8,7 +8,8 @@ import { useFieldGroupLabel } from "./use-field-group-label.js";
8
8
  const radioVariants = cva(
9
9
  [
10
10
  "inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full border",
11
- "border-border-input bg-background transition-colors",
11
+ "border-border-input bg-background interactive",
12
+ "not-data-checked:not-data-disabled:hover:border-text-brand",
12
13
  "data-checked:border-brand data-checked:bg-brand",
13
14
  "data-invalid:data-unchecked:border-error",
14
15
  "data-disabled:cursor-not-allowed data-disabled:border-border data-disabled:bg-surface",
@@ -1 +1 @@
1
- {"version":3,"file":"radio.js","sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import { Radio as BaseRadio } from \"@base-ui/react/radio\";\nimport { RadioGroup as BaseRadioGroup } from \"@base-ui/react/radio-group\";\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { FieldItemLabel } from \"./field-item-label\";\nimport { useFieldGroupLabel } from \"./use-field-group-label\";\n\nexport const radioVariants = cva(\n [\n \"inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full border\",\n \"border-border-input bg-background transition-colors\",\n \"data-checked:border-brand data-checked:bg-brand\",\n \"data-invalid:data-unchecked:border-error\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border data-disabled:bg-surface\",\n \"data-disabled:data-checked:border-border-strong data-disabled:data-checked:bg-border-strong\",\n ],\n {\n variants: {\n // Off when the focus ring and the hit area belong to a bigger container that\n // wraps the radio (SelectionCard).\n standalone: {\n true: \"focus-ring touch-target\",\n false: \"\",\n },\n },\n defaultVariants: {\n standalone: true,\n },\n },\n);\n\n/** The dot of a checked radio: 8px in text-inverse. */\nexport function RadioDot(): React.JSX.Element {\n return (\n <BaseRadio.Indicator className=\"size-2 rounded-full bg-text-inverse\" />\n );\n}\n\nexport interface RadioProps<Value = string>\n extends Omit<BaseRadio.Root.Props<Value>, \"className\" | \"children\"> {\n className?: string;\n}\n\n/**\n * Single choice inside a `RadioGroup`. The label lives outside: use `RadioField`, or\n * wrap radio and text in a `<label>`.\n *\n * A radio never unticks itself. If the person can go back to \"none\", that is a\n * Checkbox or an explicit \"Ninguna\" option. The drawing is 20×20; the hit area is\n * 44×44, and the whole row with `RadioField`.\n */\nexport function Radio<Value = string>({\n className,\n ...props\n}: RadioProps<Value>): React.JSX.Element {\n return (\n <BaseRadio.Root\n data-slot=\"radio\"\n className={cn(radioVariants(), className)}\n {...props}\n >\n <RadioDot />\n </BaseRadio.Root>\n );\n}\n\nexport interface RadioFieldProps<Value = string> extends RadioProps<Value> {\n /** The option's text. */\n label: ReactNode;\n}\n\n/**\n * Radio with its label: a row at least 44px high, all of it pressable, with the\n * text 8px after the radio.\n */\nexport function RadioField<Value = string>({\n label,\n className,\n ...props\n}: RadioFieldProps<Value>): React.JSX.Element {\n return (\n <FieldItemLabel\n data-slot=\"radio-field\"\n className={cn(\n \"flex min-h-11 cursor-pointer items-start gap-2 py-3 text-label-default text-text-primary\",\n \"has-data-disabled:cursor-not-allowed has-data-disabled:text-text-disabled\",\n className,\n )}\n >\n <Radio {...props} />\n <span>{label}</span>\n </FieldItemLabel>\n );\n}\n\nexport interface RadioGroupProps<Value = string>\n extends Omit<BaseRadioGroup.Props<Value>, \"className\"> {\n className?: string;\n}\n\n/**\n * Group of radios with arrow-key navigation. Name it with `aria-labelledby`, or put\n * it inside `Field` (Control=Custom), which labels it and shows the message under it.\n * Don't preselect an option unless there is a real default.\n */\nexport function RadioGroup<Value = string>({\n className,\n ...props\n}: RadioGroupProps<Value>): React.JSX.Element {\n useFieldGroupLabel();\n\n return (\n <BaseRadioGroup\n data-slot=\"radio-group\"\n className={cn(\"flex flex-col\", className)}\n {...props}\n />\n );\n}\n"],"names":["BaseRadio","BaseRadioGroup"],"mappings":";;;;;;;AASO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA;AAAA;AAAA,MAGR,YAAY;AAAA,QACV,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,YAAY;AAAA,IAAA;AAAA,EACd;AAEJ;AAGO,SAAS,WAA8B;AAC5C,SACE,oBAACA,QAAU,WAAV,EAAoB,WAAU,sCAAA,CAAsC;AAEzE;AAeO,SAAS,MAAsB;AAAA,EACpC;AAAA,EACA,GAAG;AACL,GAAyC;AACvC,SACE;AAAA,IAACA,QAAU;AAAA,IAAV;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAA,GAAiB,SAAS;AAAA,MACvC,GAAG;AAAA,MAEJ,8BAAC,UAAA,CAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAGhB;AAWO,SAAS,WAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA8C;AAC5C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAO,GAAG,OAAO;AAAA,QAClB,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGnB;AAYO,SAAS,WAA2B;AAAA,EACzC;AAAA,EACA,GAAG;AACL,GAA8C;AAC5C,qBAAA;AAEA,SACE;AAAA,IAACC;AAAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iBAAiB,SAAS;AAAA,MACvC,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
1
+ {"version":3,"file":"radio.js","sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import { Radio as BaseRadio } from \"@base-ui/react/radio\";\nimport { RadioGroup as BaseRadioGroup } from \"@base-ui/react/radio-group\";\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { FieldItemLabel } from \"./field-item-label\";\nimport { useFieldGroupLabel } from \"./use-field-group-label\";\n\nexport const radioVariants = cva(\n [\n \"inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full border\",\n \"border-border-input bg-background interactive\",\n \"not-data-checked:not-data-disabled:hover:border-text-brand\",\n \"data-checked:border-brand data-checked:bg-brand\",\n \"data-invalid:data-unchecked:border-error\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border data-disabled:bg-surface\",\n \"data-disabled:data-checked:border-border-strong data-disabled:data-checked:bg-border-strong\",\n ],\n {\n variants: {\n // Off when the focus ring and the hit area belong to a bigger container that\n // wraps the radio (SelectionCard).\n standalone: {\n true: \"focus-ring touch-target\",\n false: \"\",\n },\n },\n defaultVariants: {\n standalone: true,\n },\n },\n);\n\n/** The dot of a checked radio: 8px in text-inverse. */\nexport function RadioDot(): React.JSX.Element {\n return (\n <BaseRadio.Indicator className=\"size-2 rounded-full bg-text-inverse\" />\n );\n}\n\nexport interface RadioProps<Value = string>\n extends Omit<BaseRadio.Root.Props<Value>, \"className\" | \"children\"> {\n className?: string;\n}\n\n/**\n * Single choice inside a `RadioGroup`. The label lives outside: use `RadioField`, or\n * wrap radio and text in a `<label>`.\n *\n * A radio never unticks itself. If the person can go back to \"none\", that is a\n * Checkbox or an explicit \"Ninguna\" option. The drawing is 20×20; the hit area is\n * 44×44, and the whole row with `RadioField`.\n */\nexport function Radio<Value = string>({\n className,\n ...props\n}: RadioProps<Value>): React.JSX.Element {\n return (\n <BaseRadio.Root\n data-slot=\"radio\"\n className={cn(radioVariants(), className)}\n {...props}\n >\n <RadioDot />\n </BaseRadio.Root>\n );\n}\n\nexport interface RadioFieldProps<Value = string> extends RadioProps<Value> {\n /** The option's text. */\n label: ReactNode;\n}\n\n/**\n * Radio with its label: a row at least 44px high, all of it pressable, with the\n * text 8px after the radio.\n */\nexport function RadioField<Value = string>({\n label,\n className,\n ...props\n}: RadioFieldProps<Value>): React.JSX.Element {\n return (\n <FieldItemLabel\n data-slot=\"radio-field\"\n className={cn(\n \"flex min-h-11 cursor-pointer items-start gap-2 py-3 text-label-default text-text-primary\",\n \"has-data-disabled:cursor-not-allowed has-data-disabled:text-text-disabled\",\n className,\n )}\n >\n <Radio {...props} />\n <span>{label}</span>\n </FieldItemLabel>\n );\n}\n\nexport interface RadioGroupProps<Value = string>\n extends Omit<BaseRadioGroup.Props<Value>, \"className\"> {\n className?: string;\n}\n\n/**\n * Group of radios with arrow-key navigation. Name it with `aria-labelledby`, or put\n * it inside `Field` (Control=Custom), which labels it and shows the message under it.\n * Don't preselect an option unless there is a real default.\n */\nexport function RadioGroup<Value = string>({\n className,\n ...props\n}: RadioGroupProps<Value>): React.JSX.Element {\n useFieldGroupLabel();\n\n return (\n <BaseRadioGroup\n data-slot=\"radio-group\"\n className={cn(\"flex flex-col\", className)}\n {...props}\n />\n );\n}\n"],"names":["BaseRadio","BaseRadioGroup"],"mappings":";;;;;;;AASO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA;AAAA;AAAA,MAGR,YAAY;AAAA,QACV,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,YAAY;AAAA,IAAA;AAAA,EACd;AAEJ;AAGO,SAAS,WAA8B;AAC5C,SACE,oBAACA,QAAU,WAAV,EAAoB,WAAU,sCAAA,CAAsC;AAEzE;AAeO,SAAS,MAAsB;AAAA,EACpC;AAAA,EACA,GAAG;AACL,GAAyC;AACvC,SACE;AAAA,IAACA,QAAU;AAAA,IAAV;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAA,GAAiB,SAAS;AAAA,MACvC,GAAG;AAAA,MAEJ,8BAAC,UAAA,CAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAGhB;AAWO,SAAS,WAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA8C;AAC5C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAO,GAAG,OAAO;AAAA,QAClB,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGnB;AAYO,SAAS,WAA2B;AAAA,EACzC;AAAA,EACA,GAAG;AACL,GAA8C;AAC5C,qBAAA;AAEA,SACE;AAAA,IAACC;AAAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iBAAiB,SAAS;AAAA,MACvC,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
@@ -24,7 +24,7 @@ function SegmentedControl({
24
24
  "data-slot": "segmented-control-segment",
25
25
  className: cn(
26
26
  "inline-flex min-w-0 cursor-pointer select-none items-center justify-center rounded-md px-3",
27
- "text-label-default text-text-secondary transition-colors",
27
+ "text-label-default text-text-secondary interactive",
28
28
  "not-data-disabled:not-data-checked:hover:text-text-primary",
29
29
  "data-checked:bg-background data-checked:text-label-strong data-checked:text-text-primary data-checked:shadow-xs",
30
30
  "data-disabled:cursor-not-allowed data-disabled:text-text-disabled",
@@ -1 +1 @@
1
- {"version":3,"file":"segmented-control.js","sources":["../../../src/components/segmented-control/segmented-control.tsx"],"sourcesContent":["import { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport interface SegmentedControlOption<Value extends string = string> {\n value: Value;\n /** Short: one or two words. With long labels the segments get cut on mobile. */\n label: ReactNode;\n disabled?: boolean;\n}\n\n/** Two or three segments, never more: the type doesn't let you build a fourth. */\nexport type SegmentedControlOptions<Value extends string = string> =\n | readonly [SegmentedControlOption<Value>, SegmentedControlOption<Value>]\n | readonly [\n SegmentedControlOption<Value>,\n SegmentedControlOption<Value>,\n SegmentedControlOption<Value>,\n ];\n\nexport interface SegmentedControlProps<Value extends string = string>\n extends Omit<RadioGroup.Props<Value>, \"className\" | \"children\"> {\n /** 2 or 3 options. For more use a `Select` or a row of `Chip`s. */\n options: SegmentedControlOptions<Value>;\n className?: string;\n}\n\n/**\n * Exclusive filter of the SAME list: Todos / No leídos in Chats.\n *\n * - It filters, it doesn't switch views: it's a `radiogroup` of `radio`s with\n * `aria-checked` (←/→ move the selection), not a tablist. To change what you are\n * looking at, use `Tabs`.\n * - Name it with `aria-label` (\"Filtrar chats\") or `aria-labelledby`.\n * - The active segment is told apart by shape, not only colour: the only white pill\n * with a shadow on the grey track, label in Medium. Inactive `text-secondary` on\n * `surface-raised` is 4.60:1 — don't lighten the track.\n * - Every segment is the same width even if the labels aren't, so the control\n * doesn't jump when the language changes. It hugs the longest label; pass\n * `className=\"w-full\"` to fill its container.\n */\nexport function SegmentedControl<Value extends string>({\n options,\n className,\n ...props\n}: SegmentedControlProps<Value>): React.JSX.Element {\n return (\n <RadioGroup\n data-slot=\"segmented-control\"\n className={cn(\n \"relative inline-grid h-control-md auto-cols-fr grid-flow-col gap-1 rounded-lg bg-surface-raised p-1\",\n className,\n )}\n {...props}\n >\n {options.map((option) => (\n <Radio.Root\n key={option.value}\n value={option.value}\n disabled={option.disabled}\n data-slot=\"segmented-control-segment\"\n className={cn(\n \"inline-flex min-w-0 cursor-pointer select-none items-center justify-center rounded-md px-3\",\n \"text-label-default text-text-secondary transition-colors\",\n \"not-data-disabled:not-data-checked:hover:text-text-primary\",\n \"data-checked:bg-background data-checked:text-label-strong data-checked:text-text-primary data-checked:shadow-xs\",\n \"data-disabled:cursor-not-allowed data-disabled:text-text-disabled\",\n // 32px drawn, 44px hit area.\n \"focus-ring touch-target\",\n )}\n >\n <span\n // An invisible Medium copy of the label reserves the selected width, so\n // the control doesn't jump when the selection changes.\n data-label={\n typeof option.label === \"string\" ? option.label : undefined\n }\n className=\"flex min-w-0 flex-col after:invisible after:h-0 after:overflow-hidden after:text-label-strong after:content-[attr(data-label)]\"\n >\n <span className=\"truncate\">{option.label}</span>\n </span>\n </Radio.Root>\n ))}\n </RadioGroup>\n );\n}\n"],"names":[],"mappings":";;;;AA2CO,SAAS,iBAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoD;AAClD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA,QAAQ,IAAI,CAAC,WACZ;AAAA,QAAC,MAAM;AAAA,QAAN;AAAA,UAEC,OAAO,OAAO;AAAA,UACd,UAAU,OAAO;AAAA,UACjB,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,YAEA;AAAA,UAAA;AAAA,UAGF,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cAGC,cACE,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,cAEpD,WAAU;AAAA,cAEV,UAAA,oBAAC,QAAA,EAAK,WAAU,YAAY,iBAAO,MAAA,CAAM;AAAA,YAAA;AAAA,UAAA;AAAA,QAC3C;AAAA,QAvBK,OAAO;AAAA,MAAA,CAyBf;AAAA,IAAA;AAAA,EAAA;AAGP;"}
1
+ {"version":3,"file":"segmented-control.js","sources":["../../../src/components/segmented-control/segmented-control.tsx"],"sourcesContent":["import { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport interface SegmentedControlOption<Value extends string = string> {\n value: Value;\n /** Short: one or two words. With long labels the segments get cut on mobile. */\n label: ReactNode;\n disabled?: boolean;\n}\n\n/** Two or three segments, never more: the type doesn't let you build a fourth. */\nexport type SegmentedControlOptions<Value extends string = string> =\n | readonly [SegmentedControlOption<Value>, SegmentedControlOption<Value>]\n | readonly [\n SegmentedControlOption<Value>,\n SegmentedControlOption<Value>,\n SegmentedControlOption<Value>,\n ];\n\nexport interface SegmentedControlProps<Value extends string = string>\n extends Omit<RadioGroup.Props<Value>, \"className\" | \"children\"> {\n /** 2 or 3 options. For more use a `Select` or a row of `Chip`s. */\n options: SegmentedControlOptions<Value>;\n className?: string;\n}\n\n/**\n * Exclusive filter of the SAME list: Todos / No leídos in Chats.\n *\n * - It filters, it doesn't switch views: it's a `radiogroup` of `radio`s with\n * `aria-checked` (←/→ move the selection), not a tablist. To change what you are\n * looking at, use `Tabs`.\n * - Name it with `aria-label` (\"Filtrar chats\") or `aria-labelledby`.\n * - The active segment is told apart by shape, not only colour: the only white pill\n * with a shadow on the grey track, label in Medium. Inactive `text-secondary` on\n * `surface-raised` is 4.60:1 — don't lighten the track.\n * - Every segment is the same width even if the labels aren't, so the control\n * doesn't jump when the language changes. It hugs the longest label; pass\n * `className=\"w-full\"` to fill its container.\n */\nexport function SegmentedControl<Value extends string>({\n options,\n className,\n ...props\n}: SegmentedControlProps<Value>): React.JSX.Element {\n return (\n <RadioGroup\n data-slot=\"segmented-control\"\n className={cn(\n \"relative inline-grid h-control-md auto-cols-fr grid-flow-col gap-1 rounded-lg bg-surface-raised p-1\",\n className,\n )}\n {...props}\n >\n {options.map((option) => (\n <Radio.Root\n key={option.value}\n value={option.value}\n disabled={option.disabled}\n data-slot=\"segmented-control-segment\"\n className={cn(\n \"inline-flex min-w-0 cursor-pointer select-none items-center justify-center rounded-md px-3\",\n \"text-label-default text-text-secondary interactive\",\n \"not-data-disabled:not-data-checked:hover:text-text-primary\",\n \"data-checked:bg-background data-checked:text-label-strong data-checked:text-text-primary data-checked:shadow-xs\",\n \"data-disabled:cursor-not-allowed data-disabled:text-text-disabled\",\n // 32px drawn, 44px hit area.\n \"focus-ring touch-target\",\n )}\n >\n <span\n // An invisible Medium copy of the label reserves the selected width, so\n // the control doesn't jump when the selection changes.\n data-label={\n typeof option.label === \"string\" ? option.label : undefined\n }\n className=\"flex min-w-0 flex-col after:invisible after:h-0 after:overflow-hidden after:text-label-strong after:content-[attr(data-label)]\"\n >\n <span className=\"truncate\">{option.label}</span>\n </span>\n </Radio.Root>\n ))}\n </RadioGroup>\n );\n}\n"],"names":[],"mappings":";;;;AA2CO,SAAS,iBAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoD;AAClD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA,QAAQ,IAAI,CAAC,WACZ;AAAA,QAAC,MAAM;AAAA,QAAN;AAAA,UAEC,OAAO,OAAO;AAAA,UACd,UAAU,OAAO;AAAA,UACjB,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,YAEA;AAAA,UAAA;AAAA,UAGF,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cAGC,cACE,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,cAEpD,WAAU;AAAA,cAEV,UAAA,oBAAC,QAAA,EAAK,WAAU,YAAY,iBAAO,MAAA,CAAM;AAAA,YAAA;AAAA,UAAA;AAAA,QAC3C;AAAA,QAvBK,OAAO;AAAA,MAAA,CAyBf;AAAA,IAAA;AAAA,EAAA;AAGP;"}
@@ -77,7 +77,7 @@ function Select({
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
79
  "rounded-xl border border-border-subtle bg-background p-1.5 shadow-lg outline-none",
80
- "transition-[opacity,scale] duration-150 motion-reduce:transition-none",
80
+ "transition-[opacity,scale] duration-150 ease-out-soft 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
  ),
83
83
  children: /* @__PURE__ */ jsx(Select$1.List, { children: children ?? items?.map((item) => /* @__PURE__ */ jsx(
@@ -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-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;"}
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 ease-out-soft 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;"}
@@ -33,7 +33,8 @@ function SelectionCard({
33
33
  {
34
34
  "data-slot": "selection-card",
35
35
  className: cn(
36
- "group/selection-card flex min-h-14 cursor-pointer items-center gap-3 rounded-lg border border-border bg-background p-4 transition-colors",
36
+ "group/selection-card flex min-h-14 cursor-pointer items-center gap-3 rounded-lg border border-border bg-background p-4 interactive",
37
+ "not-has-data-checked:not-has-data-disabled:hover:border-border-strong not-has-data-checked:not-has-data-disabled:hover:bg-surface",
37
38
  "focus-ring-within",
38
39
  // 2px border without moving the content: the 1px border plus a 1px inset ring.
39
40
  "has-data-checked:border-text-brand has-data-checked:bg-brand-subtle has-data-checked:inset-ring has-data-checked:inset-ring-text-brand",
@@ -1 +1 @@
1
- {"version":3,"file":"selection-card.js","sources":["../../../src/components/selection-card/selection-card.tsx"],"sourcesContent":["import { Radio as BaseRadio } from \"@base-ui/react/radio\";\nimport { type ReactNode, useId } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { IconContainer } from \"../icon-container\";\nimport { FieldItemLabel } from \"../radio/field-item-label\";\nimport {\n RadioDot,\n RadioGroup,\n type RadioGroupProps,\n radioVariants,\n} from \"../radio/radio\";\n\nexport interface SelectionCardGroupProps<Value = string>\n extends RadioGroupProps<Value> {}\n\n/**\n * The group of `SelectionCard`s: one choice, arrow keys to move between cards.\n * Name it with `aria-labelledby` (the question: \"¿Cómo quieres la consulta?\") or put\n * it inside `Field` (Control=Custom).\n */\nexport function SelectionCardGroup<Value = string>({\n className,\n ...props\n}: SelectionCardGroupProps<Value>): React.JSX.Element {\n return (\n <RadioGroup<Value>\n data-slot=\"selection-card-group\"\n className={cn(\"gap-3\", className)}\n {...props}\n />\n );\n}\n\nexport interface SelectionCardProps<Value = string>\n extends Omit<\n BaseRadio.Root.Props<Value>,\n \"className\" | \"children\" | \"title\"\n > {\n /** \"Videoconsulta\". It is the radio's accessible name. */\n title: ReactNode;\n /**\n * What the person needs to know to choose: \"Con cámara, desde el móvil o el\n * ordenador\". Leave it out if it adds nothing new; don't fill it in.\n */\n description?: ReactNode;\n /** A MediCore icon, shown in a 40px IconContainer that turns brand when selected. */\n icon?: ReactNode;\n className?: string;\n}\n\n/**\n * Large selectable option: consultation type, payment method, who the appointment is\n * for. Goes inside `SelectionCardGroup`.\n *\n * The whole card is the target (a `<label>` wrapping a real radio), at least 56px\n * high. Selected reads without colour: 2px border, ticked radio and the icon in brand.\n */\nexport function SelectionCard<Value = string>({\n title,\n description,\n icon,\n className,\n ...props\n}: SelectionCardProps<Value>): React.JSX.Element {\n const id = useId();\n const titleId = `${id}-title`;\n const descriptionId = `${id}-description`;\n\n return (\n <FieldItemLabel\n data-slot=\"selection-card\"\n className={cn(\n \"group/selection-card flex min-h-14 cursor-pointer items-center gap-3 rounded-lg border border-border bg-background p-4 transition-colors\",\n \"focus-ring-within\",\n // 2px border without moving the content: the 1px border plus a 1px inset ring.\n \"has-data-checked:border-text-brand has-data-checked:bg-brand-subtle has-data-checked:inset-ring has-data-checked:inset-ring-text-brand\",\n \"has-data-disabled:cursor-not-allowed has-data-disabled:border-border-subtle has-data-disabled:bg-surface\",\n className,\n )}\n >\n {icon && (\n <IconContainer\n size={40}\n tone=\"neutral\"\n icon={icon}\n className=\"group-has-data-checked/selection-card:bg-brand-subtle group-has-data-checked/selection-card:text-text-brand group-has-data-disabled/selection-card:opacity-50\"\n />\n )}\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span\n id={titleId}\n className=\"text-label-strong text-text-primary group-has-data-disabled/selection-card:text-text-disabled\"\n >\n {title}\n </span>\n {description && (\n <span\n id={descriptionId}\n className=\"text-body-small text-text-secondary group-has-data-checked/selection-card:text-text-primary group-has-data-disabled/selection-card:text-text-disabled\"\n >\n {description}\n </span>\n )}\n </span>\n <BaseRadio.Root\n data-slot=\"selection-card-radio\"\n aria-labelledby={titleId}\n aria-describedby={description ? descriptionId : undefined}\n className={radioVariants({ standalone: false })}\n {...props}\n >\n <RadioDot />\n </BaseRadio.Root>\n </FieldItemLabel>\n );\n}\n"],"names":["BaseRadio"],"mappings":";;;;;;;AAqBO,SAAS,mBAAmC;AAAA,EACjD;AAAA,EACA,GAAG;AACL,GAAsD;AACpD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,SAAS,SAAS;AAAA,MAC/B,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AA0BO,SAAS,cAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiD;AAC/C,QAAM,KAAK,MAAA;AACX,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,gBAAgB,GAAG,EAAE;AAE3B,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,QAAA,QACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAM;AAAA,YACN,MAAK;AAAA,YACL;AAAA,YACA,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,QAGd,qBAAC,QAAA,EAAK,WAAU,wCACd,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAU;AAAA,cAET,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAEF,eACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAU;AAAA,cAET,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,GAEJ;AAAA,QACA;AAAA,UAACA,MAAU;AAAA,UAAV;AAAA,YACC,aAAU;AAAA,YACV,mBAAiB;AAAA,YACjB,oBAAkB,cAAc,gBAAgB;AAAA,YAChD,WAAW,cAAc,EAAE,YAAY,OAAO;AAAA,YAC7C,GAAG;AAAA,YAEJ,8BAAC,UAAA,CAAA,CAAS;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"selection-card.js","sources":["../../../src/components/selection-card/selection-card.tsx"],"sourcesContent":["import { Radio as BaseRadio } from \"@base-ui/react/radio\";\nimport { type ReactNode, useId } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { IconContainer } from \"../icon-container\";\nimport { FieldItemLabel } from \"../radio/field-item-label\";\nimport {\n RadioDot,\n RadioGroup,\n type RadioGroupProps,\n radioVariants,\n} from \"../radio/radio\";\n\nexport interface SelectionCardGroupProps<Value = string>\n extends RadioGroupProps<Value> {}\n\n/**\n * The group of `SelectionCard`s: one choice, arrow keys to move between cards.\n * Name it with `aria-labelledby` (the question: \"¿Cómo quieres la consulta?\") or put\n * it inside `Field` (Control=Custom).\n */\nexport function SelectionCardGroup<Value = string>({\n className,\n ...props\n}: SelectionCardGroupProps<Value>): React.JSX.Element {\n return (\n <RadioGroup<Value>\n data-slot=\"selection-card-group\"\n className={cn(\"gap-3\", className)}\n {...props}\n />\n );\n}\n\nexport interface SelectionCardProps<Value = string>\n extends Omit<\n BaseRadio.Root.Props<Value>,\n \"className\" | \"children\" | \"title\"\n > {\n /** \"Videoconsulta\". It is the radio's accessible name. */\n title: ReactNode;\n /**\n * What the person needs to know to choose: \"Con cámara, desde el móvil o el\n * ordenador\". Leave it out if it adds nothing new; don't fill it in.\n */\n description?: ReactNode;\n /** A MediCore icon, shown in a 40px IconContainer that turns brand when selected. */\n icon?: ReactNode;\n className?: string;\n}\n\n/**\n * Large selectable option: consultation type, payment method, who the appointment is\n * for. Goes inside `SelectionCardGroup`.\n *\n * The whole card is the target (a `<label>` wrapping a real radio), at least 56px\n * high. Selected reads without colour: 2px border, ticked radio and the icon in brand.\n */\nexport function SelectionCard<Value = string>({\n title,\n description,\n icon,\n className,\n ...props\n}: SelectionCardProps<Value>): React.JSX.Element {\n const id = useId();\n const titleId = `${id}-title`;\n const descriptionId = `${id}-description`;\n\n return (\n <FieldItemLabel\n data-slot=\"selection-card\"\n className={cn(\n \"group/selection-card flex min-h-14 cursor-pointer items-center gap-3 rounded-lg border border-border bg-background p-4 interactive\",\n \"not-has-data-checked:not-has-data-disabled:hover:border-border-strong not-has-data-checked:not-has-data-disabled:hover:bg-surface\",\n \"focus-ring-within\",\n // 2px border without moving the content: the 1px border plus a 1px inset ring.\n \"has-data-checked:border-text-brand has-data-checked:bg-brand-subtle has-data-checked:inset-ring has-data-checked:inset-ring-text-brand\",\n \"has-data-disabled:cursor-not-allowed has-data-disabled:border-border-subtle has-data-disabled:bg-surface\",\n className,\n )}\n >\n {icon && (\n <IconContainer\n size={40}\n tone=\"neutral\"\n icon={icon}\n className=\"group-has-data-checked/selection-card:bg-brand-subtle group-has-data-checked/selection-card:text-text-brand group-has-data-disabled/selection-card:opacity-50\"\n />\n )}\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span\n id={titleId}\n className=\"text-label-strong text-text-primary group-has-data-disabled/selection-card:text-text-disabled\"\n >\n {title}\n </span>\n {description && (\n <span\n id={descriptionId}\n className=\"text-body-small text-text-secondary group-has-data-checked/selection-card:text-text-primary group-has-data-disabled/selection-card:text-text-disabled\"\n >\n {description}\n </span>\n )}\n </span>\n <BaseRadio.Root\n data-slot=\"selection-card-radio\"\n aria-labelledby={titleId}\n aria-describedby={description ? descriptionId : undefined}\n className={radioVariants({ standalone: false })}\n {...props}\n >\n <RadioDot />\n </BaseRadio.Root>\n </FieldItemLabel>\n );\n}\n"],"names":["BaseRadio"],"mappings":";;;;;;;AAqBO,SAAS,mBAAmC;AAAA,EACjD;AAAA,EACA,GAAG;AACL,GAAsD;AACpD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,SAAS,SAAS;AAAA,MAC/B,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AA0BO,SAAS,cAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiD;AAC/C,QAAM,KAAK,MAAA;AACX,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,gBAAgB,GAAG,EAAE;AAE3B,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,QAAA,QACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAM;AAAA,YACN,MAAK;AAAA,YACL;AAAA,YACA,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,QAGd,qBAAC,QAAA,EAAK,WAAU,wCACd,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAU;AAAA,cAET,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAEF,eACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAU;AAAA,cAET,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,GAEJ;AAAA,QACA;AAAA,UAACA,MAAU;AAAA,UAAV;AAAA,YACC,aAAU;AAAA,YACV,mBAAiB;AAAA,YACjB,oBAAkB,cAAc,gBAAgB;AAAA,YAChD,WAAW,cAAc,EAAE,YAAY,OAAO;AAAA,YAC7C,GAAG;AAAA,YAEJ,8BAAC,UAAA,CAAA,CAAS;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -10,11 +10,12 @@ function Switch({
10
10
  {
11
11
  "data-slot": "switch",
12
12
  className: cn(
13
- "inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full p-0.5 transition-colors",
13
+ "inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full p-0.5 interactive",
14
14
  // Off is not a solid grey: surface with an inner border-input edge (5.41:1)
15
15
  // reads as "off"; a grey fill that passes 3:1 already reads as "on".
16
16
  "bg-surface inset-ring inset-ring-border-input",
17
17
  "data-checked:bg-brand data-checked:inset-ring-brand",
18
+ "not-data-checked:not-data-disabled:hover:inset-ring-border-strong",
18
19
  "data-disabled:cursor-not-allowed data-disabled:inset-ring-border",
19
20
  "data-disabled:data-checked:bg-border-strong data-disabled:data-checked:inset-ring-border-strong",
20
21
  "focus-ring touch-target",
@@ -26,7 +27,7 @@ function Switch({
26
27
  {
27
28
  className: cn(
28
29
  "size-5 rounded-full border border-border-input bg-text-inverse",
29
- "transition-transform motion-reduce:transition-none",
30
+ "transition-transform duration-200 ease-out-soft motion-reduce:transition-none",
30
31
  // Only Off has an edge: on brand the thumb is already 10.32:1.
31
32
  "data-checked:translate-x-5 data-checked:border-transparent",
32
33
  "data-disabled:border-border data-disabled:data-checked:border-transparent"
@@ -1 +1 @@
1
- {"version":3,"file":"switch.js","sources":["../../../src/components/switch/switch.tsx"],"sourcesContent":["import { Switch as BaseSwitch } from \"@base-ui/react/switch\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport interface SwitchProps\n extends Omit<BaseSwitch.Root.Props, \"className\" | \"children\"> {\n className?: string;\n}\n\n/**\n * Immediate-apply switch: when released, the change is already made. If the change\n * needs a save button, it isn't a Switch, it is a Checkbox.\n *\n * The label goes outside and to the left, with the switch aligned to the right of\n * the row: use `SwitchField`, or pass `aria-label`. Track 44×24 and thumb 20×20 are\n * fixed on purpose (any other pair stops being a circle); the hit area is 44 high.\n */\nexport function Switch({\n className,\n ...props\n}: SwitchProps): React.JSX.Element {\n return (\n <BaseSwitch.Root\n data-slot=\"switch\"\n className={cn(\n \"inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full p-0.5 transition-colors\",\n // Off is not a solid grey: surface with an inner border-input edge (5.41:1)\n // reads as \"off\"; a grey fill that passes 3:1 already reads as \"on\".\n \"bg-surface inset-ring inset-ring-border-input\",\n \"data-checked:bg-brand data-checked:inset-ring-brand\",\n \"data-disabled:cursor-not-allowed data-disabled:inset-ring-border\",\n \"data-disabled:data-checked:bg-border-strong data-disabled:data-checked:inset-ring-border-strong\",\n \"focus-ring touch-target\",\n className,\n )}\n {...props}\n >\n <BaseSwitch.Thumb\n className={cn(\n \"size-5 rounded-full border border-border-input bg-text-inverse\",\n \"transition-transform motion-reduce:transition-none\",\n // Only Off has an edge: on brand the thumb is already 10.32:1.\n \"data-checked:translate-x-5 data-checked:border-transparent\",\n \"data-disabled:border-border data-disabled:data-checked:border-transparent\",\n )}\n />\n </BaseSwitch.Root>\n );\n}\n\nexport interface SwitchFieldProps extends SwitchProps {\n /** What the switch turns on or off: \"Recordatorios por SMS\". */\n label: ReactNode;\n}\n\n/**\n * A settings row: label on the left, switch on the right, at least 44px high and\n * pressable as a whole.\n */\nexport function SwitchField({\n label,\n className,\n ...props\n}: SwitchFieldProps): React.JSX.Element {\n return (\n // biome-ignore lint/a11y/noLabelWithoutControl: the Base UI switch inside renders its own <input>, so the wrapping label is associated.\n <label\n data-slot=\"switch-field\"\n className={cn(\n \"flex min-h-11 cursor-pointer items-center justify-between gap-4 text-label-default text-text-primary\",\n \"has-data-disabled:cursor-not-allowed has-data-disabled:text-text-disabled\",\n className,\n )}\n >\n <span>{label}</span>\n <Switch {...props} />\n </label>\n );\n}\n"],"names":["BaseSwitch"],"mappings":";;;AAkBO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA,GAAG;AACL,GAAmC;AACjC,SACE;AAAA,IAACA,SAAW;AAAA,IAAX;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEJ,UAAA;AAAA,QAACA,SAAW;AAAA,QAAX;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA;AAAA;AAAA,YAEA;AAAA,YACA;AAAA,UAAA;AAAA,QACF;AAAA,MAAA;AAAA,IACF;AAAA,EAAA;AAGN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwC;AACtC;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAGF,UAAA;AAAA,UAAA,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,UACb,oBAAC,QAAA,EAAQ,GAAG,MAAA,CAAO;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA;AAGzB;"}
1
+ {"version":3,"file":"switch.js","sources":["../../../src/components/switch/switch.tsx"],"sourcesContent":["import { Switch as BaseSwitch } from \"@base-ui/react/switch\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport interface SwitchProps\n extends Omit<BaseSwitch.Root.Props, \"className\" | \"children\"> {\n className?: string;\n}\n\n/**\n * Immediate-apply switch: when released, the change is already made. If the change\n * needs a save button, it isn't a Switch, it is a Checkbox.\n *\n * The label goes outside and to the left, with the switch aligned to the right of\n * the row: use `SwitchField`, or pass `aria-label`. Track 44×24 and thumb 20×20 are\n * fixed on purpose (any other pair stops being a circle); the hit area is 44 high.\n */\nexport function Switch({\n className,\n ...props\n}: SwitchProps): React.JSX.Element {\n return (\n <BaseSwitch.Root\n data-slot=\"switch\"\n className={cn(\n \"inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full p-0.5 interactive\",\n // Off is not a solid grey: surface with an inner border-input edge (5.41:1)\n // reads as \"off\"; a grey fill that passes 3:1 already reads as \"on\".\n \"bg-surface inset-ring inset-ring-border-input\",\n \"data-checked:bg-brand data-checked:inset-ring-brand\",\n \"not-data-checked:not-data-disabled:hover:inset-ring-border-strong\",\n \"data-disabled:cursor-not-allowed data-disabled:inset-ring-border\",\n \"data-disabled:data-checked:bg-border-strong data-disabled:data-checked:inset-ring-border-strong\",\n \"focus-ring touch-target\",\n className,\n )}\n {...props}\n >\n <BaseSwitch.Thumb\n className={cn(\n \"size-5 rounded-full border border-border-input bg-text-inverse\",\n \"transition-transform duration-200 ease-out-soft motion-reduce:transition-none\",\n // Only Off has an edge: on brand the thumb is already 10.32:1.\n \"data-checked:translate-x-5 data-checked:border-transparent\",\n \"data-disabled:border-border data-disabled:data-checked:border-transparent\",\n )}\n />\n </BaseSwitch.Root>\n );\n}\n\nexport interface SwitchFieldProps extends SwitchProps {\n /** What the switch turns on or off: \"Recordatorios por SMS\". */\n label: ReactNode;\n}\n\n/**\n * A settings row: label on the left, switch on the right, at least 44px high and\n * pressable as a whole.\n */\nexport function SwitchField({\n label,\n className,\n ...props\n}: SwitchFieldProps): React.JSX.Element {\n return (\n // biome-ignore lint/a11y/noLabelWithoutControl: the Base UI switch inside renders its own <input>, so the wrapping label is associated.\n <label\n data-slot=\"switch-field\"\n className={cn(\n \"flex min-h-11 cursor-pointer items-center justify-between gap-4 text-label-default text-text-primary\",\n \"has-data-disabled:cursor-not-allowed has-data-disabled:text-text-disabled\",\n className,\n )}\n >\n <span>{label}</span>\n <Switch {...props} />\n </label>\n );\n}\n"],"names":["BaseSwitch"],"mappings":";;;AAkBO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA,GAAG;AACL,GAAmC;AACjC,SACE;AAAA,IAACA,SAAW;AAAA,IAAX;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEJ,UAAA;AAAA,QAACA,SAAW;AAAA,QAAX;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA;AAAA;AAAA,YAEA;AAAA,YACA;AAAA,UAAA;AAAA,QACF;AAAA,MAAA;AAAA,IACF;AAAA,EAAA;AAGN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwC;AACtC;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAGF,UAAA;AAAA,UAAA,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,UACb,oBAAC,QAAA,EAAQ,GAAG,MAAA,CAAO;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA;AAGzB;"}
@@ -77,7 +77,7 @@ function TableRow({
77
77
  {
78
78
  "data-slot": "table-row",
79
79
  className: cn(
80
- "transition-colors [tbody_&:last-child>td]:border-b-0 [tbody_&:hover>td]:bg-surface",
80
+ "interactive [tbody_&:last-child>td]:border-b-0 [tbody_&:hover>td]:bg-surface",
81
81
  className
82
82
  ),
83
83
  ...props
@@ -129,7 +129,7 @@ function TableHead({
129
129
  "aria-describedby": descriptionId,
130
130
  className: cn(
131
131
  "flex h-12 w-full cursor-pointer items-center gap-1 whitespace-nowrap px-4",
132
- "transition-colors hover:text-text-primary",
132
+ "interactive hover:text-text-primary",
133
133
  "focus-ring focus-visible:-outline-offset-2",
134
134
  // Numbers: the chevron goes on the inside so the label stays aligned
135
135
  // with the column.
@@ -148,7 +148,7 @@ function TableHead({
148
148
  );
149
149
  }
150
150
  const tableCellVariants = cva(
151
- "h-14 border-b border-border-subtle bg-background px-4 align-middle text-body-small text-text-primary transition-colors",
151
+ "h-14 border-b border-border-subtle bg-background px-4 align-middle text-body-small text-text-primary interactive",
152
152
  {
153
153
  variants: {
154
154
  type: {
@@ -1 +1 @@
1
- {"version":3,"file":"table.js","sources":["../../../src/components/table/table.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\nimport {\n type ComponentProps,\n type MouseEventHandler,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\";\n\nimport { ChevronDownIcon, ChevronUpIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\n\n/** The type decides the alignment: text and badge left, numbers right, action centred. */\nexport type TableCellType = \"text\" | \"numeric\" | \"badge\" | \"action\";\n\nexport type TableSort = \"none\" | \"ascending\" | \"descending\";\n\n/**\n * Maps a headless library's sort state to `TableSort`, e.g. TanStack's\n * `column.getIsSorted()` (`false | \"asc\" | \"desc\"`).\n */\nexport function tableSortFrom(value: false | \"asc\" | \"desc\"): TableSort {\n if (value === \"asc\") return \"ascending\";\n if (value === \"desc\") return \"descending\";\n return \"none\";\n}\n\nexport interface TableProps extends ComponentProps<\"table\"> {\n /**\n * Keeps the first column (usually the name or the date) in place while the rest\n * scrolls sideways. For dense admin tables that still have to work on a tablet.\n */\n stickyFirstColumn?: boolean;\n /** Classes for the scroll container around the `<table>` (border, radius, max height…). */\n containerClassName?: string;\n}\n\n/**\n * Semantic, composable table: `Table` › `TableHeader` / `TableBody` › `TableRow` ›\n * `TableHead` / `TableCell`. Plain elements with styles, so a headless library\n * (`@tanstack/react-table`) can drive it: map `headerGroups` to `TableRow` +\n * `TableHead` (`sort={tableSortFrom(column.getIsSorted())}`,\n * `onSort={column.getToggleSortingHandler()}`)\n * and `rows` to `TableRow` + `TableCell`.\n *\n * - Name it: `aria-label` (\"Facturas\") or `aria-labelledby` the section title.\n * - Desktop pattern (1024+). On mobile a six-column table doesn't fit: render each\n * row as a `ListRow` (title = concept, subtitle = date · specialty, trailing =\n * amount and action) or a stacked row with the actions in a menu, and switch\n * with `hidden lg:block` / `lg:hidden`. Horizontal scroll (automatic, with\n * `stickyFirstColumn` if needed) is the fallback for in-between widths, not the\n * mobile design.\n * - When it overflows, the scroll container becomes focusable so it can be scrolled\n * with the keyboard.\n */\nexport function Table({\n stickyFirstColumn = false,\n containerClassName,\n className,\n ...props\n}: TableProps): React.JSX.Element {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scrollable, setScrollable] = useState(false);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const update = (): void =>\n setScrollable(container.scrollWidth > container.clientWidth);\n update();\n\n const observer = new ResizeObserver(update);\n observer.observe(container);\n if (container.firstElementChild) {\n observer.observe(container.firstElementChild);\n }\n return () => observer.disconnect();\n }, []);\n\n // Only while it overflows: then it's a named region reachable with Tab, so it\n // can be scrolled with the arrow keys (WCAG 2.1.1). Otherwise it's a plain box.\n const scrollRegion = scrollable\n ? {\n role: \"region\",\n tabIndex: 0,\n \"aria-label\": props[\"aria-label\"],\n \"aria-labelledby\": props[\"aria-labelledby\"],\n }\n : {};\n\n return (\n <div\n ref={containerRef}\n data-slot=\"table-container\"\n {...scrollRegion}\n className={cn(\n \"relative w-full overflow-x-auto rounded-xl border border-border-subtle bg-background focus-ring\",\n containerClassName,\n )}\n >\n <table\n data-slot=\"table\"\n className={cn(\n // Separate borders so they travel with sticky cells.\n \"w-full border-separate border-spacing-0 text-left\",\n stickyFirstColumn &&\n \"[&_tr>:first-child]:sticky [&_tr>:first-child]:left-0 [&_tr>:first-child]:z-10 [&_tr>:first-child]:shadow-[inset_-1px_0_0_0_var(--color-border-subtle)]\",\n className,\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport interface TableHeaderProps extends ComponentProps<\"thead\"> {}\n\nexport function TableHeader(props: TableHeaderProps): React.JSX.Element {\n return <thead data-slot=\"table-header\" {...props} />;\n}\n\nexport interface TableBodyProps extends ComponentProps<\"tbody\"> {}\n\nexport function TableBody(props: TableBodyProps): React.JSX.Element {\n return <tbody data-slot=\"table-body\" {...props} />;\n}\n\nexport interface TableRowProps extends ComponentProps<\"tr\"> {}\n\n/**\n * A row. Figma has no Table/Row: a row is just cells as wide as their headers. If a\n * row opens a detail, put a link in its first cell — don't make the `<tr>` clickable.\n */\nexport function TableRow({\n className,\n ...props\n}: TableRowProps): React.JSX.Element {\n return (\n <tr\n data-slot=\"table-row\"\n className={cn(\n \"transition-colors [tbody_&:last-child>td]:border-b-0 [tbody_&:hover>td]:bg-surface\",\n className,\n )}\n {...props}\n />\n );\n}\n\nconst alignment = {\n text: \"text-left\",\n numeric: \"text-right\",\n badge: \"text-left\",\n // Shrinks to its buttons.\n action: \"w-0 whitespace-nowrap text-center\",\n} satisfies Record<TableCellType, string>;\n\nexport const tableHeadVariants = cva(\n \"h-11 border-b border-border-subtle bg-surface px-4 align-middle text-label-small text-text-secondary first:rounded-tl-xl last:rounded-tr-xl data-sorted:text-label-strong data-sorted:text-text-primary\",\n { variants: { type: alignment }, defaultVariants: { type: \"text\" } },\n);\n\nexport interface TableHeadProps extends ComponentProps<\"th\"> {\n /** Same as the column's cells, so the header lines up with them. */\n type?: TableCellType;\n /**\n * Current order of the column. The sorted column is recognised by its chevron,\n * not only by the darker label.\n */\n sort?: TableSort;\n /**\n * Makes the column sortable: the label becomes a `<button>` inside the `<th>`,\n * which carries `aria-sort`. Cycling the order is up to the app.\n */\n onSort?: MouseEventHandler<HTMLButtonElement>;\n}\n\n/** Column header (`<th scope=\"col\">`). Sortable when it gets `onSort`. */\nexport function TableHead({\n type = \"text\",\n sort = \"none\",\n onSort,\n className,\n children,\n ...props\n}: TableHeadProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const descriptionId = useId();\n const sorted = sort !== \"none\";\n const sortLabel = {\n none: labels.notSorted,\n ascending: labels.sortAscending,\n descending: labels.sortDescending,\n }[sort];\n\n return (\n <th\n data-slot=\"table-head\"\n scope=\"col\"\n aria-sort={sorted ? sort : undefined}\n data-sorted={sorted || undefined}\n className={cn(tableHeadVariants({ type }), onSort && \"p-0\", className)}\n {...props}\n >\n {onSort ? (\n <button\n type=\"button\"\n onClick={onSort}\n // The state is announced when the button gets focus, where aria-sort on\n // the <th> isn't always read.\n aria-describedby={descriptionId}\n className={cn(\n \"flex h-12 w-full cursor-pointer items-center gap-1 whitespace-nowrap px-4\",\n \"transition-colors hover:text-text-primary\",\n \"focus-ring focus-visible:-outline-offset-2\",\n // Numbers: the chevron goes on the inside so the label stays aligned\n // with the column.\n type === \"numeric\"\n ? \"flex-row-reverse\"\n : type === \"action\"\n ? \"justify-center\"\n : \"justify-start\",\n )}\n >\n <span>{children}</span>\n {sort === \"ascending\" && <ChevronUpIcon size=\"sm\" />}\n {sort === \"descending\" && <ChevronDownIcon size=\"sm\" />}\n </button>\n ) : (\n children\n )}\n {onSort && (\n <span id={descriptionId} hidden>\n {sortLabel}\n </span>\n )}\n </th>\n );\n}\n\nexport const tableCellVariants = cva(\n \"h-14 border-b border-border-subtle bg-background px-4 align-middle text-body-small text-text-primary transition-colors\",\n {\n variants: {\n type: {\n ...alignment,\n // Tabular figures so decimals line up and amounts can be compared.\n numeric: \"whitespace-nowrap text-right text-label-strong tabular-nums\",\n },\n },\n defaultVariants: { type: \"text\" },\n },\n);\n\nexport interface TableCellProps extends ComponentProps<\"td\"> {\n /**\n * - `text`: left.\n * - `numeric`: right, tabular figures. The value comes formatted by the app in\n * es-ES (\"29,00 €\", symbol after), the cell doesn't format.\n * - `badge`: left, a `Badge`.\n * - `action`: centred, a ghost small icon button whose `aria-label` carries the\n * row's context (\"Descargar factura del 5 de enero\") — twenty \"Descargar\"\n * buttons are indistinguishable for a screen reader.\n */\n type?: TableCellType;\n}\n\nexport function TableCell({\n type,\n className,\n ...props\n}: TableCellProps): React.JSX.Element {\n return (\n <td\n data-slot=\"table-cell\"\n className={cn(tableCellVariants({ type }), className)}\n {...props}\n />\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"table.js","sources":["../../../src/components/table/table.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\nimport {\n type ComponentProps,\n type MouseEventHandler,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\";\n\nimport { ChevronDownIcon, ChevronUpIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\n\n/** The type decides the alignment: text and badge left, numbers right, action centred. */\nexport type TableCellType = \"text\" | \"numeric\" | \"badge\" | \"action\";\n\nexport type TableSort = \"none\" | \"ascending\" | \"descending\";\n\n/**\n * Maps a headless library's sort state to `TableSort`, e.g. TanStack's\n * `column.getIsSorted()` (`false | \"asc\" | \"desc\"`).\n */\nexport function tableSortFrom(value: false | \"asc\" | \"desc\"): TableSort {\n if (value === \"asc\") return \"ascending\";\n if (value === \"desc\") return \"descending\";\n return \"none\";\n}\n\nexport interface TableProps extends ComponentProps<\"table\"> {\n /**\n * Keeps the first column (usually the name or the date) in place while the rest\n * scrolls sideways. For dense admin tables that still have to work on a tablet.\n */\n stickyFirstColumn?: boolean;\n /** Classes for the scroll container around the `<table>` (border, radius, max height…). */\n containerClassName?: string;\n}\n\n/**\n * Semantic, composable table: `Table` › `TableHeader` / `TableBody` › `TableRow` ›\n * `TableHead` / `TableCell`. Plain elements with styles, so a headless library\n * (`@tanstack/react-table`) can drive it: map `headerGroups` to `TableRow` +\n * `TableHead` (`sort={tableSortFrom(column.getIsSorted())}`,\n * `onSort={column.getToggleSortingHandler()}`)\n * and `rows` to `TableRow` + `TableCell`.\n *\n * - Name it: `aria-label` (\"Facturas\") or `aria-labelledby` the section title.\n * - Desktop pattern (1024+). On mobile a six-column table doesn't fit: render each\n * row as a `ListRow` (title = concept, subtitle = date · specialty, trailing =\n * amount and action) or a stacked row with the actions in a menu, and switch\n * with `hidden lg:block` / `lg:hidden`. Horizontal scroll (automatic, with\n * `stickyFirstColumn` if needed) is the fallback for in-between widths, not the\n * mobile design.\n * - When it overflows, the scroll container becomes focusable so it can be scrolled\n * with the keyboard.\n */\nexport function Table({\n stickyFirstColumn = false,\n containerClassName,\n className,\n ...props\n}: TableProps): React.JSX.Element {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scrollable, setScrollable] = useState(false);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const update = (): void =>\n setScrollable(container.scrollWidth > container.clientWidth);\n update();\n\n const observer = new ResizeObserver(update);\n observer.observe(container);\n if (container.firstElementChild) {\n observer.observe(container.firstElementChild);\n }\n return () => observer.disconnect();\n }, []);\n\n // Only while it overflows: then it's a named region reachable with Tab, so it\n // can be scrolled with the arrow keys (WCAG 2.1.1). Otherwise it's a plain box.\n const scrollRegion = scrollable\n ? {\n role: \"region\",\n tabIndex: 0,\n \"aria-label\": props[\"aria-label\"],\n \"aria-labelledby\": props[\"aria-labelledby\"],\n }\n : {};\n\n return (\n <div\n ref={containerRef}\n data-slot=\"table-container\"\n {...scrollRegion}\n className={cn(\n \"relative w-full overflow-x-auto rounded-xl border border-border-subtle bg-background focus-ring\",\n containerClassName,\n )}\n >\n <table\n data-slot=\"table\"\n className={cn(\n // Separate borders so they travel with sticky cells.\n \"w-full border-separate border-spacing-0 text-left\",\n stickyFirstColumn &&\n \"[&_tr>:first-child]:sticky [&_tr>:first-child]:left-0 [&_tr>:first-child]:z-10 [&_tr>:first-child]:shadow-[inset_-1px_0_0_0_var(--color-border-subtle)]\",\n className,\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport interface TableHeaderProps extends ComponentProps<\"thead\"> {}\n\nexport function TableHeader(props: TableHeaderProps): React.JSX.Element {\n return <thead data-slot=\"table-header\" {...props} />;\n}\n\nexport interface TableBodyProps extends ComponentProps<\"tbody\"> {}\n\nexport function TableBody(props: TableBodyProps): React.JSX.Element {\n return <tbody data-slot=\"table-body\" {...props} />;\n}\n\nexport interface TableRowProps extends ComponentProps<\"tr\"> {}\n\n/**\n * A row. Figma has no Table/Row: a row is just cells as wide as their headers. If a\n * row opens a detail, put a link in its first cell — don't make the `<tr>` clickable.\n */\nexport function TableRow({\n className,\n ...props\n}: TableRowProps): React.JSX.Element {\n return (\n <tr\n data-slot=\"table-row\"\n className={cn(\n \"interactive [tbody_&:last-child>td]:border-b-0 [tbody_&:hover>td]:bg-surface\",\n className,\n )}\n {...props}\n />\n );\n}\n\nconst alignment = {\n text: \"text-left\",\n numeric: \"text-right\",\n badge: \"text-left\",\n // Shrinks to its buttons.\n action: \"w-0 whitespace-nowrap text-center\",\n} satisfies Record<TableCellType, string>;\n\nexport const tableHeadVariants = cva(\n \"h-11 border-b border-border-subtle bg-surface px-4 align-middle text-label-small text-text-secondary first:rounded-tl-xl last:rounded-tr-xl data-sorted:text-label-strong data-sorted:text-text-primary\",\n { variants: { type: alignment }, defaultVariants: { type: \"text\" } },\n);\n\nexport interface TableHeadProps extends ComponentProps<\"th\"> {\n /** Same as the column's cells, so the header lines up with them. */\n type?: TableCellType;\n /**\n * Current order of the column. The sorted column is recognised by its chevron,\n * not only by the darker label.\n */\n sort?: TableSort;\n /**\n * Makes the column sortable: the label becomes a `<button>` inside the `<th>`,\n * which carries `aria-sort`. Cycling the order is up to the app.\n */\n onSort?: MouseEventHandler<HTMLButtonElement>;\n}\n\n/** Column header (`<th scope=\"col\">`). Sortable when it gets `onSort`. */\nexport function TableHead({\n type = \"text\",\n sort = \"none\",\n onSort,\n className,\n children,\n ...props\n}: TableHeadProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const descriptionId = useId();\n const sorted = sort !== \"none\";\n const sortLabel = {\n none: labels.notSorted,\n ascending: labels.sortAscending,\n descending: labels.sortDescending,\n }[sort];\n\n return (\n <th\n data-slot=\"table-head\"\n scope=\"col\"\n aria-sort={sorted ? sort : undefined}\n data-sorted={sorted || undefined}\n className={cn(tableHeadVariants({ type }), onSort && \"p-0\", className)}\n {...props}\n >\n {onSort ? (\n <button\n type=\"button\"\n onClick={onSort}\n // The state is announced when the button gets focus, where aria-sort on\n // the <th> isn't always read.\n aria-describedby={descriptionId}\n className={cn(\n \"flex h-12 w-full cursor-pointer items-center gap-1 whitespace-nowrap px-4\",\n \"interactive hover:text-text-primary\",\n \"focus-ring focus-visible:-outline-offset-2\",\n // Numbers: the chevron goes on the inside so the label stays aligned\n // with the column.\n type === \"numeric\"\n ? \"flex-row-reverse\"\n : type === \"action\"\n ? \"justify-center\"\n : \"justify-start\",\n )}\n >\n <span>{children}</span>\n {sort === \"ascending\" && <ChevronUpIcon size=\"sm\" />}\n {sort === \"descending\" && <ChevronDownIcon size=\"sm\" />}\n </button>\n ) : (\n children\n )}\n {onSort && (\n <span id={descriptionId} hidden>\n {sortLabel}\n </span>\n )}\n </th>\n );\n}\n\nexport const tableCellVariants = cva(\n \"h-14 border-b border-border-subtle bg-background px-4 align-middle text-body-small text-text-primary interactive\",\n {\n variants: {\n type: {\n ...alignment,\n // Tabular figures so decimals line up and amounts can be compared.\n numeric: \"whitespace-nowrap text-right text-label-strong tabular-nums\",\n },\n },\n defaultVariants: { type: \"text\" },\n },\n);\n\nexport interface TableCellProps extends ComponentProps<\"td\"> {\n /**\n * - `text`: left.\n * - `numeric`: right, tabular figures. The value comes formatted by the app in\n * es-ES (\"29,00 €\", symbol after), the cell doesn't format.\n * - `badge`: left, a `Badge`.\n * - `action`: centred, a ghost small icon button whose `aria-label` carries the\n * row's context (\"Descargar factura del 5 de enero\") — twenty \"Descargar\"\n * buttons are indistinguishable for a screen reader.\n */\n type?: TableCellType;\n}\n\nexport function TableCell({\n type,\n className,\n ...props\n}: TableCellProps): React.JSX.Element {\n return (\n <td\n data-slot=\"table-cell\"\n className={cn(tableCellVariants({ type }), className)}\n {...props}\n />\n );\n}\n"],"names":[],"mappings":"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"}
@@ -43,10 +43,10 @@ function TabsTab({
43
43
  "data-slot": "tabs-tab",
44
44
  className: cn(
45
45
  "relative inline-flex h-control-lg shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-md px-4",
46
- "text-label-default text-text-secondary transition-colors",
47
- "after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-transparent",
46
+ "text-label-default text-text-secondary interactive",
47
+ "after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:scale-x-0 after:bg-transparent after:transition-[scale,background-color] after:duration-200 after:ease-out-soft motion-reduce:after:transition-none",
48
48
  "not-data-disabled:not-data-active:hover:text-text-primary",
49
- "data-active:text-label-strong data-active:text-text-brand data-active:after:bg-text-brand",
49
+ "data-active:text-label-strong data-active:text-text-brand data-active:after:scale-x-100 data-active:after:bg-text-brand",
50
50
  "data-disabled:cursor-not-allowed data-disabled:text-text-disabled",
51
51
  // Inside the tab, not around it: in navigation there is no room outside.
52
52
  "focus-ring focus-visible:-outline-offset-2",
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","sources":["../../../src/components/tabs/tabs.tsx"],"sourcesContent":["import { Tabs as BaseTabs } from \"@base-ui/react/tabs\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Badge } from \"../badge\";\n\nexport interface TabsProps extends Omit<BaseTabs.Root.Props, \"className\"> {\n className?: string;\n}\n\n/**\n * Switches WHAT you are looking at inside the same page: Próximas / Pasadas in\n * Mis citas, Datos personales / Historial / Facturación in the profile.\n *\n * - **Tabs are not a SegmentedControl.** A tab changes the content (another\n * section); a segmented control filters the same list (Todos / No leídos). If the\n * list keeps its structure and only its items change, use `SegmentedControl`.\n * - If every tab is its own URL they are links with `aria-current=\"page\"`, not\n * tabs: don't use this component for route navigation.\n * - `role=\"tablist\"` / `role=\"tab\"` with `aria-selected` and ←/→ between tabs come\n * from Base UI. Put the panels right after the list with `TabsPanel`.\n *\n * ```tsx\n * <Tabs defaultValue=\"upcoming\">\n * <TabsList aria-label=\"Mis citas\">\n * <TabsTab value=\"upcoming\" count={3}>Próximas</TabsTab>\n * <TabsTab value=\"past\">Pasadas</TabsTab>\n * </TabsList>\n * <TabsPanel value=\"upcoming\">…</TabsPanel>\n * <TabsPanel value=\"past\">…</TabsPanel>\n * </Tabs>\n * ```\n */\nexport function Tabs({ className, ...props }: TabsProps): React.JSX.Element {\n return (\n <BaseTabs.Root\n data-slot=\"tabs\"\n className={cn(\"flex flex-col gap-4\", className)}\n {...props}\n />\n );\n}\n\nexport interface TabsListProps extends Omit<BaseTabs.List.Props, \"className\"> {\n className?: string;\n}\n\n/**\n * The row of tabs. It sits on a full-width 1px `border-subtle` line that the active\n * tab's bar covers. When the tabs don't fit (mobile) the row scrolls sideways.\n */\nexport function TabsList({\n className,\n ...props\n}: TabsListProps): React.JSX.Element {\n return (\n <BaseTabs.List\n data-slot=\"tabs-list\"\n className={cn(\n \"flex overflow-x-auto [scrollbar-width:none]\",\n // The baseline is an inset shadow and not a border so it stays full width\n // while the tabs scroll, and the active bar can paint over it.\n \"shadow-[inset_0_-1px_0_0_var(--color-border-subtle)]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface TabsTabProps extends Omit<BaseTabs.Tab.Props, \"className\"> {\n /**\n * Counter next to the label (\"3\"): unread messages, pending appointments. Only for\n * something that updates live; a fixed number is noise.\n */\n count?: ReactNode;\n className?: string;\n}\n\n/**\n * One tab. 44px tall (`control-lg`): a tab row lives at the top of the screen, the\n * hardest place to reach with a thumb.\n *\n * The active tab is not told apart by colour alone: 2px bar underneath, Medium label\n * and brand text (9.67:1; default 5.07:1).\n */\nexport function TabsTab({\n count,\n className,\n children,\n ...props\n}: TabsTabProps): React.JSX.Element {\n return (\n <BaseTabs.Tab\n data-slot=\"tabs-tab\"\n className={cn(\n \"relative inline-flex h-control-lg shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-md px-4\",\n \"text-label-default text-text-secondary transition-colors\",\n \"after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-transparent\",\n \"not-data-disabled:not-data-active:hover:text-text-primary\",\n \"data-active:text-label-strong data-active:text-text-brand data-active:after:bg-text-brand\",\n \"data-disabled:cursor-not-allowed data-disabled:text-text-disabled\",\n // Inside the tab, not around it: in navigation there is no room outside.\n \"focus-ring focus-visible:-outline-offset-2\",\n className,\n )}\n {...props}\n >\n <span\n // An invisible Medium copy of the label reserves the active width, so the\n // row doesn't jump when the active tab changes.\n data-label={typeof children === \"string\" ? children : undefined}\n className=\"flex flex-col after:invisible after:h-0 after:overflow-hidden after:text-label-strong after:content-[attr(data-label)]\"\n >\n {children}\n </span>\n {count !== undefined && count !== null && (\n <Badge variant=\"brand\">{count}</Badge>\n )}\n </BaseTabs.Tab>\n );\n}\n\nexport interface TabsPanelProps\n extends Omit<BaseTabs.Panel.Props, \"className\"> {\n className?: string;\n}\n\n/** The content of one tab. It is only mounted while its tab is active (`keepMounted` to keep it). */\nexport function TabsPanel({\n className,\n ...props\n}: TabsPanelProps): React.JSX.Element {\n return (\n <BaseTabs.Panel\n data-slot=\"tabs-panel\"\n className={cn(\"focus-ring\", className)}\n {...props}\n />\n );\n}\n"],"names":["BaseTabs"],"mappings":";;;;AAiCO,SAAS,KAAK,EAAE,WAAW,GAAG,SAAuC;AAC1E,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,uBAAuB,SAAS;AAAA,MAC7C,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAUO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA,GAAG;AACL,GAAqC;AACnC,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAkBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoC;AAClC,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YAGC,cAAY,OAAO,aAAa,WAAW,WAAW;AAAA,YACtD,WAAU;AAAA,YAET;AAAA,UAAA;AAAA,QAAA;AAAA,QAEF,UAAU,UAAa,UAAU,4BAC/B,OAAA,EAAM,SAAQ,SAAS,UAAA,MAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAItC;AAQO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA,GAAG;AACL,GAAsC;AACpC,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,SAAS;AAAA,MACpC,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
1
+ {"version":3,"file":"tabs.js","sources":["../../../src/components/tabs/tabs.tsx"],"sourcesContent":["import { Tabs as BaseTabs } from \"@base-ui/react/tabs\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Badge } from \"../badge\";\n\nexport interface TabsProps extends Omit<BaseTabs.Root.Props, \"className\"> {\n className?: string;\n}\n\n/**\n * Switches WHAT you are looking at inside the same page: Próximas / Pasadas in\n * Mis citas, Datos personales / Historial / Facturación in the profile.\n *\n * - **Tabs are not a SegmentedControl.** A tab changes the content (another\n * section); a segmented control filters the same list (Todos / No leídos). If the\n * list keeps its structure and only its items change, use `SegmentedControl`.\n * - If every tab is its own URL they are links with `aria-current=\"page\"`, not\n * tabs: don't use this component for route navigation.\n * - `role=\"tablist\"` / `role=\"tab\"` with `aria-selected` and ←/→ between tabs come\n * from Base UI. Put the panels right after the list with `TabsPanel`.\n *\n * ```tsx\n * <Tabs defaultValue=\"upcoming\">\n * <TabsList aria-label=\"Mis citas\">\n * <TabsTab value=\"upcoming\" count={3}>Próximas</TabsTab>\n * <TabsTab value=\"past\">Pasadas</TabsTab>\n * </TabsList>\n * <TabsPanel value=\"upcoming\">…</TabsPanel>\n * <TabsPanel value=\"past\">…</TabsPanel>\n * </Tabs>\n * ```\n */\nexport function Tabs({ className, ...props }: TabsProps): React.JSX.Element {\n return (\n <BaseTabs.Root\n data-slot=\"tabs\"\n className={cn(\"flex flex-col gap-4\", className)}\n {...props}\n />\n );\n}\n\nexport interface TabsListProps extends Omit<BaseTabs.List.Props, \"className\"> {\n className?: string;\n}\n\n/**\n * The row of tabs. It sits on a full-width 1px `border-subtle` line that the active\n * tab's bar covers. When the tabs don't fit (mobile) the row scrolls sideways.\n */\nexport function TabsList({\n className,\n ...props\n}: TabsListProps): React.JSX.Element {\n return (\n <BaseTabs.List\n data-slot=\"tabs-list\"\n className={cn(\n \"flex overflow-x-auto [scrollbar-width:none]\",\n // The baseline is an inset shadow and not a border so it stays full width\n // while the tabs scroll, and the active bar can paint over it.\n \"shadow-[inset_0_-1px_0_0_var(--color-border-subtle)]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface TabsTabProps extends Omit<BaseTabs.Tab.Props, \"className\"> {\n /**\n * Counter next to the label (\"3\"): unread messages, pending appointments. Only for\n * something that updates live; a fixed number is noise.\n */\n count?: ReactNode;\n className?: string;\n}\n\n/**\n * One tab. 44px tall (`control-lg`): a tab row lives at the top of the screen, the\n * hardest place to reach with a thumb.\n *\n * The active tab is not told apart by colour alone: 2px bar underneath, Medium label\n * and brand text (9.67:1; default 5.07:1).\n */\nexport function TabsTab({\n count,\n className,\n children,\n ...props\n}: TabsTabProps): React.JSX.Element {\n return (\n <BaseTabs.Tab\n data-slot=\"tabs-tab\"\n className={cn(\n \"relative inline-flex h-control-lg shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-md px-4\",\n \"text-label-default text-text-secondary interactive\",\n \"after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:scale-x-0 after:bg-transparent after:transition-[scale,background-color] after:duration-200 after:ease-out-soft motion-reduce:after:transition-none\",\n \"not-data-disabled:not-data-active:hover:text-text-primary\",\n \"data-active:text-label-strong data-active:text-text-brand data-active:after:scale-x-100 data-active:after:bg-text-brand\",\n \"data-disabled:cursor-not-allowed data-disabled:text-text-disabled\",\n // Inside the tab, not around it: in navigation there is no room outside.\n \"focus-ring focus-visible:-outline-offset-2\",\n className,\n )}\n {...props}\n >\n <span\n // An invisible Medium copy of the label reserves the active width, so the\n // row doesn't jump when the active tab changes.\n data-label={typeof children === \"string\" ? children : undefined}\n className=\"flex flex-col after:invisible after:h-0 after:overflow-hidden after:text-label-strong after:content-[attr(data-label)]\"\n >\n {children}\n </span>\n {count !== undefined && count !== null && (\n <Badge variant=\"brand\">{count}</Badge>\n )}\n </BaseTabs.Tab>\n );\n}\n\nexport interface TabsPanelProps\n extends Omit<BaseTabs.Panel.Props, \"className\"> {\n className?: string;\n}\n\n/** The content of one tab. It is only mounted while its tab is active (`keepMounted` to keep it). */\nexport function TabsPanel({\n className,\n ...props\n}: TabsPanelProps): React.JSX.Element {\n return (\n <BaseTabs.Panel\n data-slot=\"tabs-panel\"\n className={cn(\"focus-ring\", className)}\n {...props}\n />\n );\n}\n"],"names":["BaseTabs"],"mappings":";;;;AAiCO,SAAS,KAAK,EAAE,WAAW,GAAG,SAAuC;AAC1E,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,uBAAuB,SAAS;AAAA,MAC7C,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAUO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA,GAAG;AACL,GAAqC;AACnC,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAkBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoC;AAClC,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YAGC,cAAY,OAAO,aAAa,WAAW,WAAW;AAAA,YACtD,WAAU;AAAA,YAET;AAAA,UAAA;AAAA,QAAA;AAAA,QAEF,UAAU,UAAa,UAAU,4BAC/B,OAAA,EAAM,SAAQ,SAAS,UAAA,MAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAItC;AAQO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA,GAAG;AACL,GAAsC;AACpC,SACE;AAAA,IAACA,OAAS;AAAA,IAAT;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,SAAS;AAAA,MACpC,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
@@ -51,7 +51,8 @@ function TimeSlot(props) {
51
51
  disabled,
52
52
  className: cn(
53
53
  "inline-flex h-control-lg w-16 shrink-0 cursor-pointer select-none items-center justify-center rounded-md border border-transparent px-1 py-2",
54
- "bg-brand-subtle text-label-default text-text-brand transition-colors",
54
+ "bg-brand-subtle text-label-default text-text-brand interactive pressable",
55
+ "not-data-checked:not-data-disabled:hover:border-text-brand",
55
56
  "focus-ring",
56
57
  "data-checked:border-[1.5px] data-checked:border-brand data-checked:bg-brand data-checked:text-label-strong data-checked:text-text-inverse",
57
58
  "data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:bg-surface data-disabled:text-text-secondary data-disabled:line-through",
@@ -1 +1 @@
1
- {"version":3,"file":"time-slot.js","sources":["../../../src/components/time-slot/time-slot.tsx"],"sourcesContent":["import { Radio as BaseRadio } from \"@base-ui/react/radio\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { RadioGroup, type RadioGroupProps } from \"../radio\";\nimport { Skeleton } from \"../skeleton\";\n\nexport type TimeSlotGroupLayout = \"columns\" | \"rows\";\n\nexport interface TimeSlotGroupProps<Value = string>\n extends RadioGroupProps<Value> {\n /**\n * `columns` (default): `AvailabilityColumn`s side by side, as in `CardProfessional`.\n * The days that don't fit are hidden, never squeezed: a slot never goes below\n * 64×44, the rest of the week is in \"Ver agenda completa\".\n * `rows`: `AvailabilityRow`s one under another.\n */\n layout?: TimeSlotGroupLayout;\n}\n\n// A column is 64px and the gap 8px: n days need 72n − 8. Below that width the\n// n-th day and the ones after it are hidden (the container is the wrapper div).\nconst columnsClasses = [\n \"flex-row items-start justify-between gap-2\",\n \"@max-[208px]:[&>*:nth-child(n+3)]:hidden\",\n \"@max-[280px]:[&>*:nth-child(n+4)]:hidden\",\n \"@max-[352px]:[&>*:nth-child(n+5)]:hidden\",\n \"@max-[424px]:[&>*:nth-child(n+6)]:hidden\",\n \"@max-[496px]:[&>*:nth-child(n+7)]:hidden\",\n];\n\n/**\n * The single choice of an agenda: every `TimeSlot` inside it (across all the days)\n * is one radio group, so there is one tab stop and the arrow keys move from hour to\n * hour. Name it with `aria-label` (\"Horas disponibles con la Dra. Díaz\").\n */\nexport function TimeSlotGroup<Value = string>({\n layout = \"columns\",\n className,\n ...props\n}: TimeSlotGroupProps<Value>): React.JSX.Element {\n const group = (\n <RadioGroup<Value>\n data-slot=\"time-slot-group\"\n className={cn(layout === \"columns\" ? columnsClasses : \"gap-2\", className)}\n {...props}\n />\n );\n\n if (layout === \"rows\") {\n return group;\n }\n\n return <div className=\"@container w-full min-w-0\">{group}</div>;\n}\n\ninterface TimeSlotRadioProps<Value>\n extends Omit<BaseRadio.Root.Props<Value>, \"className\" | \"children\"> {\n /** The time, formatted in the patient's time zone: \"11:00\" (no \"h\", no space). */\n children: ReactNode;\n /**\n * Unavailable (taken): struck through and not selectable. A screen reader hears\n * \"11:00, no disponible\" (`timeSlotUnavailable` label): the line isn't read.\n */\n disabled?: boolean;\n loading?: false;\n className?: string;\n}\n\ninterface TimeSlotLoadingProps {\n /** Grey placeholder while the availability arrives, so the grid doesn't jump. */\n loading: true;\n value?: undefined;\n children?: undefined;\n className?: string;\n}\n\nexport type TimeSlotProps<Value = string> =\n | TimeSlotRadioProps<Value>\n | TimeSlotLoadingProps;\n\n/**\n * One hour of the agenda, 64×44 (`control-lg`: tapped with the thumb in a dense\n * grid). A radio inside `TimeSlotGroup`.\n *\n * - Available: `brand-subtle` fill with brand text, no border.\n * - Selected: brand fill, inverse text, 1.5px border and Medium label — three cues\n * besides colour.\n * - Unavailable (`disabled`): struck through, the cue that doesn't depend on colour.\n * - `loading`: the grey slot while the availability loads (no radio, `aria-hidden`).\n */\nexport function TimeSlot<Value = string>(\n props: TimeSlotProps<Value>,\n): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n if (props.loading) {\n return (\n <Skeleton\n data-slot=\"time-slot\"\n data-loading=\"\"\n shape=\"block\"\n className={cn(\"h-control-lg w-16 shrink-0 rounded-md\", props.className)}\n />\n );\n }\n\n const { children, disabled, className, loading: _loading, ...rest } = props;\n\n return (\n <BaseRadio.Root<Value>\n data-slot=\"time-slot\"\n disabled={disabled}\n className={cn(\n \"inline-flex h-control-lg w-16 shrink-0 cursor-pointer select-none items-center justify-center rounded-md border border-transparent px-1 py-2\",\n \"bg-brand-subtle text-label-default text-text-brand transition-colors\",\n \"focus-ring\",\n \"data-checked:border-[1.5px] data-checked:border-brand data-checked:bg-brand data-checked:text-label-strong data-checked:text-text-inverse\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:bg-surface data-disabled:text-text-secondary data-disabled:line-through\",\n className,\n )}\n {...rest}\n >\n {children}\n {disabled && (\n <span className=\"sr-only\">, {labels.timeSlotUnavailable}</span>\n )}\n </BaseRadio.Root>\n );\n}\n"],"names":["BaseRadio"],"mappings":";;;;;;AAuBA,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,SAAS,cAA8B;AAAA,EAC5C,SAAS;AAAA,EACT;AAAA,EACA,GAAG;AACL,GAAiD;AAC/C,QAAM,QACJ;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,WAAW,YAAY,iBAAiB,SAAS,SAAS;AAAA,MACvE,GAAG;AAAA,IAAA;AAAA,EAAA;AAIR,MAAI,WAAW,QAAQ;AACrB,WAAO;AAAA,EACT;AAEA,SAAO,oBAAC,OAAA,EAAI,WAAU,6BAA6B,UAAA,OAAM;AAC3D;AAqCO,SAAS,SACd,OACmB;AACnB,QAAM,SAAS,kBAAA;AAEf,MAAI,MAAM,SAAS;AACjB,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,gBAAa;AAAA,QACb,OAAM;AAAA,QACN,WAAW,GAAG,yCAAyC,MAAM,SAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAG5E;AAEA,QAAM,EAAE,UAAU,UAAU,WAAW,SAAS,UAAU,GAAG,SAAS;AAEtE,SACE;AAAA,IAACA,MAAU;AAAA,IAAV;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA;AAAA,QACA,YACC,qBAAC,QAAA,EAAK,WAAU,WAAU,UAAA;AAAA,UAAA;AAAA,UAAG,OAAO;AAAA,QAAA,EAAA,CAAoB;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAIhE;"}
1
+ {"version":3,"file":"time-slot.js","sources":["../../../src/components/time-slot/time-slot.tsx"],"sourcesContent":["import { Radio as BaseRadio } from \"@base-ui/react/radio\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { RadioGroup, type RadioGroupProps } from \"../radio\";\nimport { Skeleton } from \"../skeleton\";\n\nexport type TimeSlotGroupLayout = \"columns\" | \"rows\";\n\nexport interface TimeSlotGroupProps<Value = string>\n extends RadioGroupProps<Value> {\n /**\n * `columns` (default): `AvailabilityColumn`s side by side, as in `CardProfessional`.\n * The days that don't fit are hidden, never squeezed: a slot never goes below\n * 64×44, the rest of the week is in \"Ver agenda completa\".\n * `rows`: `AvailabilityRow`s one under another.\n */\n layout?: TimeSlotGroupLayout;\n}\n\n// A column is 64px and the gap 8px: n days need 72n − 8. Below that width the\n// n-th day and the ones after it are hidden (the container is the wrapper div).\nconst columnsClasses = [\n \"flex-row items-start justify-between gap-2\",\n \"@max-[208px]:[&>*:nth-child(n+3)]:hidden\",\n \"@max-[280px]:[&>*:nth-child(n+4)]:hidden\",\n \"@max-[352px]:[&>*:nth-child(n+5)]:hidden\",\n \"@max-[424px]:[&>*:nth-child(n+6)]:hidden\",\n \"@max-[496px]:[&>*:nth-child(n+7)]:hidden\",\n];\n\n/**\n * The single choice of an agenda: every `TimeSlot` inside it (across all the days)\n * is one radio group, so there is one tab stop and the arrow keys move from hour to\n * hour. Name it with `aria-label` (\"Horas disponibles con la Dra. Díaz\").\n */\nexport function TimeSlotGroup<Value = string>({\n layout = \"columns\",\n className,\n ...props\n}: TimeSlotGroupProps<Value>): React.JSX.Element {\n const group = (\n <RadioGroup<Value>\n data-slot=\"time-slot-group\"\n className={cn(layout === \"columns\" ? columnsClasses : \"gap-2\", className)}\n {...props}\n />\n );\n\n if (layout === \"rows\") {\n return group;\n }\n\n return <div className=\"@container w-full min-w-0\">{group}</div>;\n}\n\ninterface TimeSlotRadioProps<Value>\n extends Omit<BaseRadio.Root.Props<Value>, \"className\" | \"children\"> {\n /** The time, formatted in the patient's time zone: \"11:00\" (no \"h\", no space). */\n children: ReactNode;\n /**\n * Unavailable (taken): struck through and not selectable. A screen reader hears\n * \"11:00, no disponible\" (`timeSlotUnavailable` label): the line isn't read.\n */\n disabled?: boolean;\n loading?: false;\n className?: string;\n}\n\ninterface TimeSlotLoadingProps {\n /** Grey placeholder while the availability arrives, so the grid doesn't jump. */\n loading: true;\n value?: undefined;\n children?: undefined;\n className?: string;\n}\n\nexport type TimeSlotProps<Value = string> =\n | TimeSlotRadioProps<Value>\n | TimeSlotLoadingProps;\n\n/**\n * One hour of the agenda, 64×44 (`control-lg`: tapped with the thumb in a dense\n * grid). A radio inside `TimeSlotGroup`.\n *\n * - Available: `brand-subtle` fill with brand text, no border.\n * - Selected: brand fill, inverse text, 1.5px border and Medium label — three cues\n * besides colour.\n * - Unavailable (`disabled`): struck through, the cue that doesn't depend on colour.\n * - `loading`: the grey slot while the availability loads (no radio, `aria-hidden`).\n */\nexport function TimeSlot<Value = string>(\n props: TimeSlotProps<Value>,\n): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n if (props.loading) {\n return (\n <Skeleton\n data-slot=\"time-slot\"\n data-loading=\"\"\n shape=\"block\"\n className={cn(\"h-control-lg w-16 shrink-0 rounded-md\", props.className)}\n />\n );\n }\n\n const { children, disabled, className, loading: _loading, ...rest } = props;\n\n return (\n <BaseRadio.Root<Value>\n data-slot=\"time-slot\"\n disabled={disabled}\n className={cn(\n \"inline-flex h-control-lg w-16 shrink-0 cursor-pointer select-none items-center justify-center rounded-md border border-transparent px-1 py-2\",\n \"bg-brand-subtle text-label-default text-text-brand interactive pressable\",\n \"not-data-checked:not-data-disabled:hover:border-text-brand\",\n \"focus-ring\",\n \"data-checked:border-[1.5px] data-checked:border-brand data-checked:bg-brand data-checked:text-label-strong data-checked:text-text-inverse\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:bg-surface data-disabled:text-text-secondary data-disabled:line-through\",\n className,\n )}\n {...rest}\n >\n {children}\n {disabled && (\n <span className=\"sr-only\">, {labels.timeSlotUnavailable}</span>\n )}\n </BaseRadio.Root>\n );\n}\n"],"names":["BaseRadio"],"mappings":";;;;;;AAuBA,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,SAAS,cAA8B;AAAA,EAC5C,SAAS;AAAA,EACT;AAAA,EACA,GAAG;AACL,GAAiD;AAC/C,QAAM,QACJ;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,WAAW,YAAY,iBAAiB,SAAS,SAAS;AAAA,MACvE,GAAG;AAAA,IAAA;AAAA,EAAA;AAIR,MAAI,WAAW,QAAQ;AACrB,WAAO;AAAA,EACT;AAEA,SAAO,oBAAC,OAAA,EAAI,WAAU,6BAA6B,UAAA,OAAM;AAC3D;AAqCO,SAAS,SACd,OACmB;AACnB,QAAM,SAAS,kBAAA;AAEf,MAAI,MAAM,SAAS;AACjB,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,gBAAa;AAAA,QACb,OAAM;AAAA,QACN,WAAW,GAAG,yCAAyC,MAAM,SAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAG5E;AAEA,QAAM,EAAE,UAAU,UAAU,WAAW,SAAS,UAAU,GAAG,SAAS;AAEtE,SACE;AAAA,IAACA,MAAU;AAAA,IAAV;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA;AAAA,QACA,YACC,qBAAC,QAAA,EAAK,WAAU,WAAU,UAAA;AAAA,UAAA;AAAA,UAAG,OAAO;AAAA,QAAA,EAAA,CAAoB;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAIhE;"}
@@ -84,7 +84,7 @@ const toastVariants = cva(
84
84
  [
85
85
  "flex items-center gap-3 rounded-lg px-4 py-3 text-text-inverse shadow-lg select-none",
86
86
  "[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-swipe-movement-y))]",
87
- "transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none",
87
+ "transition-[opacity,transform] duration-200 ease-out-soft motion-reduce:transition-none",
88
88
  "data-starting-style:opacity-0 data-starting-style:[transform:translateY(100%)] md:data-starting-style:[transform:translateY(-100%)]",
89
89
  "data-ending-style:opacity-0",
90
90
  "data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",