@mediquo/medicore 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +38 -5
  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 +129 -0
  79. package/package.json +1 -1
@@ -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%))]",
@@ -1 +1 @@
1
- {"version":3,"file":"toast.js","sources":["../../../src/components/toast/toast.tsx"],"sourcesContent":["import {\n Toast as BaseToast,\n type ToastObject,\n type UseToastManagerReturnValue,\n} from \"@base-ui/react/toast\";\nimport { cva } from \"class-variance-authority\";\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useMemo,\n} from \"react\";\n\nimport { AlertCircleIcon, CheckCircleIcon, InfoCircleIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { Button } from \"../button\";\n\n/**\n * There is no `warning` on purpose: a warning that matters can't vanish on its own,\n * and one that can vanish wasn't a warning. Use a persistent `Banner`.\n */\nexport type ToastStatus = \"neutral\" | \"success\" | \"error\";\n\nexport interface ToastActionOptions {\n /** Defaults to the MediCore `undo` label (\"Deshacer\"). */\n label?: string;\n /** Runs on click; the toast closes right after. */\n onClick: () => void;\n}\n\nexport interface ToastOptions {\n /**\n * What happened, concrete: \"Cita guardada\", \"No se pudo guardar la cita\". Never\n * \"Operación completada con éxito\" or \"Ha ocurrido un error\".\n */\n message: ReactNode;\n /** `neutral` (default) for system state, `success` in past tense, `error` for what failed. */\n status?: ToastStatus;\n /** Undo for success, retry for errors. Leave it out when there is nothing to do. */\n action?: ToastActionOptions;\n /** The icon follows the status; pass `false` to hide it. */\n icon?: boolean;\n /** Milliseconds before it closes on its own (the provider's default otherwise). `0` keeps it open. */\n timeout?: number;\n /** Passing the id of a visible toast updates it in place. */\n id?: string;\n onClose?: () => void;\n}\n\nexport interface UseToastReturnValue {\n /** Shows a toast and returns its id. */\n toast: (options: ToastOptions) => string;\n /** Closes a toast by id, or all of them. */\n close: (id?: string) => void;\n}\n\ninterface ToastData {\n icon: boolean;\n}\n\ntype ToastApi = Pick<UseToastManagerReturnValue<ToastData>, \"add\" | \"close\">;\n\nconst ToastApiContext = createContext<ToastApi | null>(null);\n\n/**\n * Shows transient notices. Needs a `ToastProvider` above.\n *\n * ```tsx\n * const { toast } = useToast();\n * toast({ message: \"Cita guardada\", status: \"success\", action: { onClick: undo } });\n * ```\n */\nexport function useToast(): UseToastReturnValue {\n const manager = useContext(ToastApiContext);\n const labels = useMediCoreLabels();\n\n if (manager === null) {\n throw new Error(\n \"MediCore: useToast() must be used inside <ToastProvider>.\",\n );\n }\n\n const toast = useCallback(\n ({\n message,\n status = \"neutral\",\n action,\n icon = true,\n ...options\n }: ToastOptions): string => {\n let id = \"\";\n id = manager.add({\n ...options,\n title: message,\n type: status,\n // High priority is announced assertively, so only errors use it.\n priority: status === \"error\" ? \"high\" : \"low\",\n data: { icon },\n actionProps: action && {\n children: action.label ?? labels.undo,\n onClick: () => {\n action.onClick();\n manager.close(id);\n },\n },\n });\n return id;\n },\n [manager, labels.undo],\n );\n\n return useMemo(() => ({ toast, close: manager.close }), [toast, manager]);\n}\n\nexport interface ToastProviderProps {\n children?: ReactNode;\n /** Default time on screen in milliseconds. Hovering or focusing the toasts pauses it. */\n timeout?: number;\n /** Toasts visible at once; older ones are hidden until there is room. */\n limit?: number;\n /** Accessible name of the notifications region. */\n label?: string;\n}\n\n/**\n * Wrap the app once. Renders the toast region: at the bottom on mobile (full width\n * minus 16px per side) and top right on desktop (360px), at most three at a time.\n * The timer pauses on hover and keyboard focus, and F6 jumps into the region.\n */\nexport function ToastProvider({\n children,\n timeout,\n limit = 3,\n label = \"Notificaciones\",\n}: ToastProviderProps): React.JSX.Element {\n return (\n <BaseToast.Provider timeout={timeout} limit={limit}>\n <ToastApiBridge>{children}</ToastApiBridge>\n <Toaster label={label} />\n </BaseToast.Provider>\n );\n}\n\n/**\n * Hands `add`/`close` to `useToast()` without the toast list, so components that only\n * show toasts don't re-render every time one appears or goes away. They talk to the\n * provider's store directly, so a toast added from a mount effect isn't lost.\n */\nfunction ToastApiBridge({\n children,\n}: {\n children?: ReactNode;\n}): React.JSX.Element {\n const { add, close } = BaseToast.useToastManager<ToastData>();\n const api = useMemo(() => ({ add, close }), [add, close]);\n\n return (\n <ToastApiContext.Provider value={api}>{children}</ToastApiContext.Provider>\n );\n}\n\nfunction Toaster({ label }: { label: string }): React.JSX.Element {\n const { toasts } = BaseToast.useToastManager<ToastData>();\n\n return (\n <BaseToast.Portal>\n <BaseToast.Viewport\n data-slot=\"toast-viewport\"\n aria-label={label}\n className={cn(\n \"fixed inset-x-4 bottom-4 z-60 flex flex-col-reverse gap-2 outline-hidden\",\n \"md:inset-x-auto md:top-8 md:right-8 md:bottom-auto md:w-90 md:flex-col\",\n )}\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} />\n ))}\n </BaseToast.Viewport>\n </BaseToast.Portal>\n );\n}\n\nconst toastVariants = cva(\n [\n \"flex items-center gap-3 rounded-lg px-4 py-3 text-text-inverse shadow-lg select-none\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-swipe-movement-y))]\",\n \"transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none\",\n \"data-starting-style:opacity-0 data-starting-style:[transform:translateY(100%)] md:data-starting-style:[transform:translateY(-100%)]\",\n \"data-ending-style:opacity-0\",\n \"data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))]\",\n \"data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))]\",\n \"data-limited:hidden focus-ring\",\n ],\n {\n variants: {\n status: {\n neutral: \"bg-surface-inverse\",\n // success-STRONG, not the status green: white on #34C759 is 2.22:1.\n success: \"bg-success-strong\",\n error: \"bg-error-strong\",\n },\n },\n defaultVariants: {\n status: \"neutral\",\n },\n },\n);\n\nconst statusIcons = {\n neutral: InfoCircleIcon,\n success: CheckCircleIcon,\n error: AlertCircleIcon,\n} as const;\n\nfunction isToastStatus(value: string | undefined): value is ToastStatus {\n return value === \"neutral\" || value === \"success\" || value === \"error\";\n}\n\nfunction ToastItem({\n toast,\n}: {\n toast: ToastObject<ToastData>;\n}): React.JSX.Element {\n const status = isToastStatus(toast.type) ? toast.type : \"neutral\";\n const Icon = statusIcons[status];\n\n return (\n <BaseToast.Root\n toast={toast}\n data-slot=\"toast\"\n className={toastVariants({ status })}\n >\n {toast.data?.icon !== false && <Icon size=\"md\" className=\"shrink-0\" />}\n <BaseToast.Title\n render={<p />}\n className=\"min-w-0 flex-1 text-body-small\"\n />\n {/* The 36px button overflows the 12px padding so the toast stays 48px tall. */}\n <BaseToast.Action\n render={\n <Button\n variant=\"inverse-ghost\"\n size=\"small\"\n className=\"-my-1.5 underline underline-offset-2\"\n />\n }\n />\n </BaseToast.Root>\n );\n}\n"],"names":["BaseToast"],"mappings":"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"}
1
+ {"version":3,"file":"toast.js","sources":["../../../src/components/toast/toast.tsx"],"sourcesContent":["import {\n Toast as BaseToast,\n type ToastObject,\n type UseToastManagerReturnValue,\n} from \"@base-ui/react/toast\";\nimport { cva } from \"class-variance-authority\";\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useMemo,\n} from \"react\";\n\nimport { AlertCircleIcon, CheckCircleIcon, InfoCircleIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { Button } from \"../button\";\n\n/**\n * There is no `warning` on purpose: a warning that matters can't vanish on its own,\n * and one that can vanish wasn't a warning. Use a persistent `Banner`.\n */\nexport type ToastStatus = \"neutral\" | \"success\" | \"error\";\n\nexport interface ToastActionOptions {\n /** Defaults to the MediCore `undo` label (\"Deshacer\"). */\n label?: string;\n /** Runs on click; the toast closes right after. */\n onClick: () => void;\n}\n\nexport interface ToastOptions {\n /**\n * What happened, concrete: \"Cita guardada\", \"No se pudo guardar la cita\". Never\n * \"Operación completada con éxito\" or \"Ha ocurrido un error\".\n */\n message: ReactNode;\n /** `neutral` (default) for system state, `success` in past tense, `error` for what failed. */\n status?: ToastStatus;\n /** Undo for success, retry for errors. Leave it out when there is nothing to do. */\n action?: ToastActionOptions;\n /** The icon follows the status; pass `false` to hide it. */\n icon?: boolean;\n /** Milliseconds before it closes on its own (the provider's default otherwise). `0` keeps it open. */\n timeout?: number;\n /** Passing the id of a visible toast updates it in place. */\n id?: string;\n onClose?: () => void;\n}\n\nexport interface UseToastReturnValue {\n /** Shows a toast and returns its id. */\n toast: (options: ToastOptions) => string;\n /** Closes a toast by id, or all of them. */\n close: (id?: string) => void;\n}\n\ninterface ToastData {\n icon: boolean;\n}\n\ntype ToastApi = Pick<UseToastManagerReturnValue<ToastData>, \"add\" | \"close\">;\n\nconst ToastApiContext = createContext<ToastApi | null>(null);\n\n/**\n * Shows transient notices. Needs a `ToastProvider` above.\n *\n * ```tsx\n * const { toast } = useToast();\n * toast({ message: \"Cita guardada\", status: \"success\", action: { onClick: undo } });\n * ```\n */\nexport function useToast(): UseToastReturnValue {\n const manager = useContext(ToastApiContext);\n const labels = useMediCoreLabels();\n\n if (manager === null) {\n throw new Error(\n \"MediCore: useToast() must be used inside <ToastProvider>.\",\n );\n }\n\n const toast = useCallback(\n ({\n message,\n status = \"neutral\",\n action,\n icon = true,\n ...options\n }: ToastOptions): string => {\n let id = \"\";\n id = manager.add({\n ...options,\n title: message,\n type: status,\n // High priority is announced assertively, so only errors use it.\n priority: status === \"error\" ? \"high\" : \"low\",\n data: { icon },\n actionProps: action && {\n children: action.label ?? labels.undo,\n onClick: () => {\n action.onClick();\n manager.close(id);\n },\n },\n });\n return id;\n },\n [manager, labels.undo],\n );\n\n return useMemo(() => ({ toast, close: manager.close }), [toast, manager]);\n}\n\nexport interface ToastProviderProps {\n children?: ReactNode;\n /** Default time on screen in milliseconds. Hovering or focusing the toasts pauses it. */\n timeout?: number;\n /** Toasts visible at once; older ones are hidden until there is room. */\n limit?: number;\n /** Accessible name of the notifications region. */\n label?: string;\n}\n\n/**\n * Wrap the app once. Renders the toast region: at the bottom on mobile (full width\n * minus 16px per side) and top right on desktop (360px), at most three at a time.\n * The timer pauses on hover and keyboard focus, and F6 jumps into the region.\n */\nexport function ToastProvider({\n children,\n timeout,\n limit = 3,\n label = \"Notificaciones\",\n}: ToastProviderProps): React.JSX.Element {\n return (\n <BaseToast.Provider timeout={timeout} limit={limit}>\n <ToastApiBridge>{children}</ToastApiBridge>\n <Toaster label={label} />\n </BaseToast.Provider>\n );\n}\n\n/**\n * Hands `add`/`close` to `useToast()` without the toast list, so components that only\n * show toasts don't re-render every time one appears or goes away. They talk to the\n * provider's store directly, so a toast added from a mount effect isn't lost.\n */\nfunction ToastApiBridge({\n children,\n}: {\n children?: ReactNode;\n}): React.JSX.Element {\n const { add, close } = BaseToast.useToastManager<ToastData>();\n const api = useMemo(() => ({ add, close }), [add, close]);\n\n return (\n <ToastApiContext.Provider value={api}>{children}</ToastApiContext.Provider>\n );\n}\n\nfunction Toaster({ label }: { label: string }): React.JSX.Element {\n const { toasts } = BaseToast.useToastManager<ToastData>();\n\n return (\n <BaseToast.Portal>\n <BaseToast.Viewport\n data-slot=\"toast-viewport\"\n aria-label={label}\n className={cn(\n \"fixed inset-x-4 bottom-4 z-60 flex flex-col-reverse gap-2 outline-hidden\",\n \"md:inset-x-auto md:top-8 md:right-8 md:bottom-auto md:w-90 md:flex-col\",\n )}\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} />\n ))}\n </BaseToast.Viewport>\n </BaseToast.Portal>\n );\n}\n\nconst toastVariants = cva(\n [\n \"flex items-center gap-3 rounded-lg px-4 py-3 text-text-inverse shadow-lg select-none\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-swipe-movement-y))]\",\n \"transition-[opacity,transform] duration-200 ease-out-soft motion-reduce:transition-none\",\n \"data-starting-style:opacity-0 data-starting-style:[transform:translateY(100%)] md:data-starting-style:[transform:translateY(-100%)]\",\n \"data-ending-style:opacity-0\",\n \"data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))]\",\n \"data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))]\",\n \"data-limited:hidden focus-ring\",\n ],\n {\n variants: {\n status: {\n neutral: \"bg-surface-inverse\",\n // success-STRONG, not the status green: white on #34C759 is 2.22:1.\n success: \"bg-success-strong\",\n error: \"bg-error-strong\",\n },\n },\n defaultVariants: {\n status: \"neutral\",\n },\n },\n);\n\nconst statusIcons = {\n neutral: InfoCircleIcon,\n success: CheckCircleIcon,\n error: AlertCircleIcon,\n} as const;\n\nfunction isToastStatus(value: string | undefined): value is ToastStatus {\n return value === \"neutral\" || value === \"success\" || value === \"error\";\n}\n\nfunction ToastItem({\n toast,\n}: {\n toast: ToastObject<ToastData>;\n}): React.JSX.Element {\n const status = isToastStatus(toast.type) ? toast.type : \"neutral\";\n const Icon = statusIcons[status];\n\n return (\n <BaseToast.Root\n toast={toast}\n data-slot=\"toast\"\n className={toastVariants({ status })}\n >\n {toast.data?.icon !== false && <Icon size=\"md\" className=\"shrink-0\" />}\n <BaseToast.Title\n render={<p />}\n className=\"min-w-0 flex-1 text-body-small\"\n />\n {/* The 36px button overflows the 12px padding so the toast stays 48px tall. */}\n <BaseToast.Action\n render={\n <Button\n variant=\"inverse-ghost\"\n size=\"small\"\n className=\"-my-1.5 underline underline-offset-2\"\n />\n }\n />\n </BaseToast.Root>\n );\n}\n"],"names":["BaseToast"],"mappings":"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"}
@@ -79,7 +79,7 @@ function TooltipContent({
79
79
  "data-slot": "tooltip",
80
80
  className: cn(
81
81
  "max-w-60 rounded-md bg-surface-inverse px-2 py-1.5 text-label-small text-text-inverse",
82
- "origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out",
82
+ "origin-(--transform-origin) transition-[opacity,scale] duration-150 ease-out-soft",
83
83
  "data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:opacity-0",
84
84
  "data-instant:transition-none motion-reduce:transition-none",
85
85
  className
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip.js","sources":["../../../src/components/tooltip/tooltip.tsx"],"sourcesContent":["import { Tooltip as BaseTooltip } from \"@base-ui/react/tooltip\";\nimport {\n createContext,\n isValidElement,\n type ReactNode,\n useCallback,\n useContext,\n useId,\n useMemo,\n useState,\n} from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport type TooltipPlacement = \"top\" | \"bottom\";\n\ninterface TooltipContextValue {\n id: string;\n open: boolean;\n}\n\nconst TooltipContext = createContext<TooltipContextValue | null>(null);\n\nfunction useTooltipContext(part: string): TooltipContextValue {\n const context = useContext(TooltipContext);\n if (context === null) {\n throw new Error(`MediCore: <${part}> must be used inside <Tooltip>.`);\n }\n return context;\n}\n\nexport interface TooltipProviderProps extends BaseTooltip.Provider.Props {}\n\n/**\n * Optional, around a toolbar or the whole app: once one tooltip is showing, the\n * neighbouring ones open without waiting for the delay again.\n */\nexport function TooltipProvider(\n props: TooltipProviderProps,\n): React.JSX.Element {\n return <BaseTooltip.Provider {...props} />;\n}\n\nexport interface TooltipProps extends Omit<BaseTooltip.Root.Props, \"children\"> {\n /**\n * Shortcut: the tooltip text. With it, `children` is the trigger element and the\n * content is built for you: `<Tooltip label=\"Editar alergia\"><IconButton … /></Tooltip>`.\n */\n label?: string;\n /** Used with `label`. Top by default; it flips on its own when there's no room. */\n placement?: TooltipPlacement;\n children?: ReactNode;\n}\n\n/**\n * Floating label for controls without visible text — the edit, delete or download\n * IconButtons. Only to NAME, never to explain: there is no hover on mobile, so\n * anything the screen needs to be understood can't live here. Two or three words.\n *\n * It opens on hover and on keyboard focus, closes with Escape, and is linked to the\n * trigger with `aria-describedby`. The trigger still needs its own `aria-label`: the\n * tooltip doesn't replace it.\n */\nexport function Tooltip({\n label,\n placement,\n children,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n ...props\n}: TooltipProps): React.JSX.Element {\n const id = useId();\n // Mirrors Base UI's open state so the trigger only points at the popup while it exists.\n const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n const open = openProp ?? uncontrolledOpen;\n\n const handleOpenChange = useCallback<\n NonNullable<BaseTooltip.Root.Props[\"onOpenChange\"]>\n >(\n (nextOpen, eventDetails) => {\n onOpenChange?.(nextOpen, eventDetails);\n if (!eventDetails.isCanceled) {\n setUncontrolledOpen(nextOpen);\n }\n },\n [onOpenChange],\n );\n\n const context = useMemo(() => ({ id, open }), [id, open]);\n\n return (\n <TooltipContext.Provider value={context}>\n <BaseTooltip.Root\n open={openProp}\n defaultOpen={defaultOpen}\n onOpenChange={handleOpenChange}\n {...props}\n >\n {label !== undefined && isValidElement(children) ? (\n <>\n <TooltipTrigger render={children} />\n <TooltipContent placement={placement}>{label}</TooltipContent>\n </>\n ) : (\n children\n )}\n </BaseTooltip.Root>\n </TooltipContext.Provider>\n );\n}\n\nexport interface TooltipTriggerProps extends BaseTooltip.Trigger.Props {}\n\n/** The element the tooltip names. Pass it with `render={<IconButton aria-label=\"…\" />}`. */\nexport function TooltipTrigger({\n \"aria-describedby\": describedBy,\n ...props\n}: TooltipTriggerProps): React.JSX.Element {\n const { id, open } = useTooltipContext(\"TooltipTrigger\");\n const ids = [describedBy, open ? id : undefined].filter(Boolean).join(\" \");\n\n return (\n <BaseTooltip.Trigger\n data-slot=\"tooltip-trigger\"\n aria-describedby={ids === \"\" ? undefined : ids}\n {...props}\n />\n );\n}\n\nexport interface TooltipContentProps\n extends Omit<BaseTooltip.Popup.Props, \"className\"> {\n placement?: TooltipPlacement;\n className?: string;\n}\n\n/** The label: `surface-inverse` with white 12px text (16.27:1) and an arrow. */\nexport function TooltipContent({\n placement = \"top\",\n className,\n children,\n ...props\n}: TooltipContentProps): React.JSX.Element {\n const { id } = useTooltipContext(\"TooltipContent\");\n\n return (\n <BaseTooltip.Portal>\n <BaseTooltip.Positioner side={placement} sideOffset={8} className=\"z-70\">\n <BaseTooltip.Popup\n id={id}\n role=\"tooltip\"\n data-slot=\"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 className,\n )}\n {...props}\n >\n <BaseTooltip.Arrow\n className={cn(\n \"h-1.5 w-2.5 bg-surface-inverse\",\n \"data-[side=top]:top-full data-[side=top]:[clip-path:polygon(0_0,100%_0,50%_100%)]\",\n \"data-[side=bottom]:bottom-full data-[side=bottom]:[clip-path:polygon(50%_0,100%_100%,0_100%)]\",\n )}\n />\n {children}\n </BaseTooltip.Popup>\n </BaseTooltip.Positioner>\n </BaseTooltip.Portal>\n );\n}\n"],"names":["BaseTooltip"],"mappings":";;;;AAqBA,MAAM,iBAAiB,cAA0C,IAAI;AAErE,SAAS,kBAAkB,MAAmC;AAC5D,QAAM,UAAU,WAAW,cAAc;AACzC,MAAI,YAAY,MAAM;AACpB,UAAM,IAAI,MAAM,cAAc,IAAI,kCAAkC;AAAA,EACtE;AACA,SAAO;AACT;AAQO,SAAS,gBACd,OACmB;AACnB,SAAO,oBAACA,UAAY,UAAZ,EAAsB,GAAG,MAAA,CAAO;AAC1C;AAsBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,cAAc;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAoC;AAClC,QAAM,KAAK,MAAA;AAEX,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,WAAW;AACpE,QAAM,OAAO,YAAY;AAEzB,QAAM,mBAAmB;AAAA,IAGvB,CAAC,UAAU,iBAAiB;AAC1B,qBAAe,UAAU,YAAY;AACrC,UAAI,CAAC,aAAa,YAAY;AAC5B,4BAAoB,QAAQ;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,CAAC,YAAY;AAAA,EAAA;AAGf,QAAM,UAAU,QAAQ,OAAO,EAAE,IAAI,SAAS,CAAC,IAAI,IAAI,CAAC;AAExD,SACE,oBAAC,eAAe,UAAf,EAAwB,OAAO,SAC9B,UAAA;AAAA,IAACA,UAAY;AAAA,IAAZ;AAAA,MACC,MAAM;AAAA,MACN;AAAA,MACA,cAAc;AAAA,MACb,GAAG;AAAA,MAEH,UAAA,UAAU,UAAa,eAAe,QAAQ,IAC7C,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,gBAAA,EAAe,QAAQ,SAAA,CAAU;AAAA,QAClC,oBAAC,gBAAA,EAAe,WAAuB,UAAA,MAAA,CAAM;AAAA,MAAA,EAAA,CAC/C,IAEA;AAAA,IAAA;AAAA,EAAA,GAGN;AAEJ;AAKO,SAAS,eAAe;AAAA,EAC7B,oBAAoB;AAAA,EACpB,GAAG;AACL,GAA2C;AACzC,QAAM,EAAE,IAAI,SAAS,kBAAkB,gBAAgB;AACvD,QAAM,MAAM,CAAC,aAAa,OAAO,KAAK,MAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAEzE,SACE;AAAA,IAACA,UAAY;AAAA,IAAZ;AAAA,MACC,aAAU;AAAA,MACV,oBAAkB,QAAQ,KAAK,SAAY;AAAA,MAC1C,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AASO,SAAS,eAAe;AAAA,EAC7B,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA2C;AACzC,QAAM,EAAE,GAAA,IAAO,kBAAkB,gBAAgB;AAEjD,SACE,oBAACA,UAAY,QAAZ,EACC,UAAA,oBAACA,UAAY,YAAZ,EAAuB,MAAM,WAAW,YAAY,GAAG,WAAU,QAChE,UAAA;AAAA,IAACA,UAAY;AAAA,IAAZ;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA;AAAA,UAACA,UAAY;AAAA,UAAZ;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,YAAA;AAAA,UACF;AAAA,QAAA;AAAA,QAED;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEL,EAAA,CACF;AAEJ;"}
1
+ {"version":3,"file":"tooltip.js","sources":["../../../src/components/tooltip/tooltip.tsx"],"sourcesContent":["import { Tooltip as BaseTooltip } from \"@base-ui/react/tooltip\";\nimport {\n createContext,\n isValidElement,\n type ReactNode,\n useCallback,\n useContext,\n useId,\n useMemo,\n useState,\n} from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport type TooltipPlacement = \"top\" | \"bottom\";\n\ninterface TooltipContextValue {\n id: string;\n open: boolean;\n}\n\nconst TooltipContext = createContext<TooltipContextValue | null>(null);\n\nfunction useTooltipContext(part: string): TooltipContextValue {\n const context = useContext(TooltipContext);\n if (context === null) {\n throw new Error(`MediCore: <${part}> must be used inside <Tooltip>.`);\n }\n return context;\n}\n\nexport interface TooltipProviderProps extends BaseTooltip.Provider.Props {}\n\n/**\n * Optional, around a toolbar or the whole app: once one tooltip is showing, the\n * neighbouring ones open without waiting for the delay again.\n */\nexport function TooltipProvider(\n props: TooltipProviderProps,\n): React.JSX.Element {\n return <BaseTooltip.Provider {...props} />;\n}\n\nexport interface TooltipProps extends Omit<BaseTooltip.Root.Props, \"children\"> {\n /**\n * Shortcut: the tooltip text. With it, `children` is the trigger element and the\n * content is built for you: `<Tooltip label=\"Editar alergia\"><IconButton … /></Tooltip>`.\n */\n label?: string;\n /** Used with `label`. Top by default; it flips on its own when there's no room. */\n placement?: TooltipPlacement;\n children?: ReactNode;\n}\n\n/**\n * Floating label for controls without visible text — the edit, delete or download\n * IconButtons. Only to NAME, never to explain: there is no hover on mobile, so\n * anything the screen needs to be understood can't live here. Two or three words.\n *\n * It opens on hover and on keyboard focus, closes with Escape, and is linked to the\n * trigger with `aria-describedby`. The trigger still needs its own `aria-label`: the\n * tooltip doesn't replace it.\n */\nexport function Tooltip({\n label,\n placement,\n children,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n ...props\n}: TooltipProps): React.JSX.Element {\n const id = useId();\n // Mirrors Base UI's open state so the trigger only points at the popup while it exists.\n const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n const open = openProp ?? uncontrolledOpen;\n\n const handleOpenChange = useCallback<\n NonNullable<BaseTooltip.Root.Props[\"onOpenChange\"]>\n >(\n (nextOpen, eventDetails) => {\n onOpenChange?.(nextOpen, eventDetails);\n if (!eventDetails.isCanceled) {\n setUncontrolledOpen(nextOpen);\n }\n },\n [onOpenChange],\n );\n\n const context = useMemo(() => ({ id, open }), [id, open]);\n\n return (\n <TooltipContext.Provider value={context}>\n <BaseTooltip.Root\n open={openProp}\n defaultOpen={defaultOpen}\n onOpenChange={handleOpenChange}\n {...props}\n >\n {label !== undefined && isValidElement(children) ? (\n <>\n <TooltipTrigger render={children} />\n <TooltipContent placement={placement}>{label}</TooltipContent>\n </>\n ) : (\n children\n )}\n </BaseTooltip.Root>\n </TooltipContext.Provider>\n );\n}\n\nexport interface TooltipTriggerProps extends BaseTooltip.Trigger.Props {}\n\n/** The element the tooltip names. Pass it with `render={<IconButton aria-label=\"…\" />}`. */\nexport function TooltipTrigger({\n \"aria-describedby\": describedBy,\n ...props\n}: TooltipTriggerProps): React.JSX.Element {\n const { id, open } = useTooltipContext(\"TooltipTrigger\");\n const ids = [describedBy, open ? id : undefined].filter(Boolean).join(\" \");\n\n return (\n <BaseTooltip.Trigger\n data-slot=\"tooltip-trigger\"\n aria-describedby={ids === \"\" ? undefined : ids}\n {...props}\n />\n );\n}\n\nexport interface TooltipContentProps\n extends Omit<BaseTooltip.Popup.Props, \"className\"> {\n placement?: TooltipPlacement;\n className?: string;\n}\n\n/** The label: `surface-inverse` with white 12px text (16.27:1) and an arrow. */\nexport function TooltipContent({\n placement = \"top\",\n className,\n children,\n ...props\n}: TooltipContentProps): React.JSX.Element {\n const { id } = useTooltipContext(\"TooltipContent\");\n\n return (\n <BaseTooltip.Portal>\n <BaseTooltip.Positioner side={placement} sideOffset={8} className=\"z-70\">\n <BaseTooltip.Popup\n id={id}\n role=\"tooltip\"\n data-slot=\"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 className,\n )}\n {...props}\n >\n <BaseTooltip.Arrow\n className={cn(\n \"h-1.5 w-2.5 bg-surface-inverse\",\n \"data-[side=top]:top-full data-[side=top]:[clip-path:polygon(0_0,100%_0,50%_100%)]\",\n \"data-[side=bottom]:bottom-full data-[side=bottom]:[clip-path:polygon(50%_0,100%_100%,0_100%)]\",\n )}\n />\n {children}\n </BaseTooltip.Popup>\n </BaseTooltip.Positioner>\n </BaseTooltip.Portal>\n );\n}\n"],"names":["BaseTooltip"],"mappings":";;;;AAqBA,MAAM,iBAAiB,cAA0C,IAAI;AAErE,SAAS,kBAAkB,MAAmC;AAC5D,QAAM,UAAU,WAAW,cAAc;AACzC,MAAI,YAAY,MAAM;AACpB,UAAM,IAAI,MAAM,cAAc,IAAI,kCAAkC;AAAA,EACtE;AACA,SAAO;AACT;AAQO,SAAS,gBACd,OACmB;AACnB,SAAO,oBAACA,UAAY,UAAZ,EAAsB,GAAG,MAAA,CAAO;AAC1C;AAsBO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,cAAc;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAoC;AAClC,QAAM,KAAK,MAAA;AAEX,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,WAAW;AACpE,QAAM,OAAO,YAAY;AAEzB,QAAM,mBAAmB;AAAA,IAGvB,CAAC,UAAU,iBAAiB;AAC1B,qBAAe,UAAU,YAAY;AACrC,UAAI,CAAC,aAAa,YAAY;AAC5B,4BAAoB,QAAQ;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,CAAC,YAAY;AAAA,EAAA;AAGf,QAAM,UAAU,QAAQ,OAAO,EAAE,IAAI,SAAS,CAAC,IAAI,IAAI,CAAC;AAExD,SACE,oBAAC,eAAe,UAAf,EAAwB,OAAO,SAC9B,UAAA;AAAA,IAACA,UAAY;AAAA,IAAZ;AAAA,MACC,MAAM;AAAA,MACN;AAAA,MACA,cAAc;AAAA,MACb,GAAG;AAAA,MAEH,UAAA,UAAU,UAAa,eAAe,QAAQ,IAC7C,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,gBAAA,EAAe,QAAQ,SAAA,CAAU;AAAA,QAClC,oBAAC,gBAAA,EAAe,WAAuB,UAAA,MAAA,CAAM;AAAA,MAAA,EAAA,CAC/C,IAEA;AAAA,IAAA;AAAA,EAAA,GAGN;AAEJ;AAKO,SAAS,eAAe;AAAA,EAC7B,oBAAoB;AAAA,EACpB,GAAG;AACL,GAA2C;AACzC,QAAM,EAAE,IAAI,SAAS,kBAAkB,gBAAgB;AACvD,QAAM,MAAM,CAAC,aAAa,OAAO,KAAK,MAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAEzE,SACE;AAAA,IAACA,UAAY;AAAA,IAAZ;AAAA,MACC,aAAU;AAAA,MACV,oBAAkB,QAAQ,KAAK,SAAY;AAAA,MAC1C,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AASO,SAAS,eAAe;AAAA,EAC7B,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA2C;AACzC,QAAM,EAAE,GAAA,IAAO,kBAAkB,gBAAgB;AAEjD,SACE,oBAACA,UAAY,QAAZ,EACC,UAAA,oBAACA,UAAY,YAAZ,EAAuB,MAAM,WAAW,YAAY,GAAG,WAAU,QAChE,UAAA;AAAA,IAACA,UAAY;AAAA,IAAZ;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA;AAAA,UAACA,UAAY;AAAA,UAAZ;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,YAAA;AAAA,UACF;AAAA,QAAA;AAAA,QAED;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEL,EAAA,CACF;AAEJ;"}
@@ -13,7 +13,7 @@ import { useMediCoreLabels } from "../../provider/medicore-provider.js";
13
13
  const videoControlButtonVariants = cva(
14
14
  [
15
15
  "relative inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-xl",
16
- "text-text-inverse transition-colors focus-ring [&_svg]:size-icon-md",
16
+ "text-text-inverse interactive pressable focus-ring [&_svg]:size-icon-md",
17
17
  "data-disabled:cursor-not-allowed"
18
18
  ],
19
19
  {
@@ -1 +1 @@
1
- {"version":3,"file":"video-control-button.js","sources":["../../../src/components/video/video-control-button.tsx"],"sourcesContent":["import { Button as BaseButton } from \"@base-ui/react/button\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { cva } from \"class-variance-authority\";\nimport { type ComponentProps, useState } from \"react\";\n\nimport {\n MessageIcon,\n Microphone01Icon,\n MicrophoneOff01Icon,\n VideoRecorderIcon,\n VideoRecorderOffIcon,\n} from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\n\nexport type VideoControl = \"microphone\" | \"camera\" | \"chat\";\n\n/** `on`: the device works · `off`: muted microphone / camera turned off. */\nexport type VideoControlState = \"on\" | \"off\";\n\nexport const videoControlButtonVariants = cva(\n [\n \"relative inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-xl\",\n \"text-text-inverse transition-colors focus-ring [&_svg]:size-icon-md\",\n \"data-disabled:cursor-not-allowed\",\n ],\n {\n variants: {\n state: {\n on: \"border border-border-inverse bg-surface-inverse\",\n off: \"bg-error-strong\",\n },\n },\n defaultVariants: {\n state: \"on\",\n },\n },\n);\n\nexport interface VideoControlButtonProps\n extends Omit<ComponentProps<\"button\">, \"children\" | \"value\"> {\n control: VideoControl;\n /**\n * Microphone and camera. Controlled: pass it with `onStateChange`. `chat` has no\n * off state: it opens the chat.\n */\n state?: VideoControlState;\n /** Uncontrolled initial state. Default `on`. */\n defaultState?: VideoControlState;\n onStateChange?: (state: VideoControlState) => void;\n /** `chat` only: new messages. Shows the red dot and says it in the button's name. */\n unreadCount?: number;\n}\n\n/**\n * Button over the video: microphone, camera and (on mobile) chat. 44×44 drawn:\n * over the video there is no label next to it to extend the hit area.\n *\n * - On: `surface-inverse` fill. Off: `error-strong` fill AND the crossed-out icon —\n * colour is never the only cue. White icon in both, over any image (the button\n * is opaque).\n * - Microphone and camera are toggle buttons (`aria-pressed`, pressed = muted /\n * camera off) and their name says the action: \"Silenciar micrófono\" / \"Activar\n * micrófono\", \"Apagar cámara\" / \"Encender cámara\".\n * - Chat is a plain button: \"Abrir chat, 1 mensaje nuevo\".\n */\nexport function VideoControlButton({\n control,\n state: stateProp,\n defaultState = \"on\",\n onStateChange,\n unreadCount = 0,\n className,\n type = \"button\",\n ...props\n}: VideoControlButtonProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n if (control === \"chat\") {\n const unread = unreadCount > 0;\n return (\n <BaseButton\n data-slot=\"video-control-button\"\n data-control={control}\n type={type}\n aria-label={\n unread\n ? `${labels.videoOpenChat}, ${labels.chatNewMessages(unreadCount)}`\n : labels.videoOpenChat\n }\n className={cn(videoControlButtonVariants({ state: \"on\" }), className)}\n {...props}\n >\n <MessageIcon />\n {unread && (\n <span\n data-slot=\"video-control-button-unread\"\n className=\"absolute top-1 right-1 size-2.5 rounded-full bg-error ring-2 ring-text-inverse\"\n />\n )}\n </BaseButton>\n );\n }\n\n return (\n <VideoToggle\n control={control}\n state={stateProp}\n defaultState={defaultState}\n onStateChange={onStateChange}\n className={className}\n type={type}\n {...props}\n />\n );\n}\n\ninterface VideoToggleProps\n extends Omit<VideoControlButtonProps, \"control\" | \"unreadCount\"> {\n control: Exclude<VideoControl, \"chat\">;\n defaultState: VideoControlState;\n}\n\nfunction VideoToggle({\n control,\n state: stateProp,\n defaultState,\n onStateChange,\n className,\n ...props\n}: VideoToggleProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const controlled = stateProp !== undefined;\n const [uncontrolledState, setUncontrolledState] = useState(defaultState);\n const state = stateProp ?? uncontrolledState;\n const off = state === \"off\";\n\n const label =\n control === \"microphone\"\n ? off\n ? labels.videoUnmuteMicrophone\n : labels.videoMuteMicrophone\n : off\n ? labels.videoTurnOnCamera\n : labels.videoTurnOffCamera;\n\n const Icon =\n control === \"microphone\"\n ? off\n ? MicrophoneOff01Icon\n : Microphone01Icon\n : off\n ? VideoRecorderOffIcon\n : VideoRecorderIcon;\n\n return (\n <Toggle\n data-slot=\"video-control-button\"\n data-control={control}\n data-state={state}\n pressed={off}\n onPressedChange={(pressed) => {\n const next: VideoControlState = pressed ? \"off\" : \"on\";\n if (!controlled) {\n setUncontrolledState(next);\n }\n onStateChange?.(next);\n }}\n aria-label={label}\n className={cn(videoControlButtonVariants({ state }), className)}\n {...props}\n >\n <Icon />\n </Toggle>\n );\n}\n"],"names":["BaseButton"],"mappings":";;;;;;;;;;;;AAoBO,MAAM,6BAA6B;AAAA,EACxC;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,IAAI;AAAA,QACJ,KAAK;AAAA,MAAA;AAAA,IACP;AAAA,IAEF,iBAAiB;AAAA,MACf,OAAO;AAAA,IAAA;AAAA,EACT;AAEJ;AA6BO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA,OAAO;AAAA,EACP,eAAe;AAAA,EACf;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,OAAO;AAAA,EACP,GAAG;AACL,GAA+C;AAC7C,QAAM,SAAS,kBAAA;AAEf,MAAI,YAAY,QAAQ;AACtB,UAAM,SAAS,cAAc;AAC7B,WACE;AAAA,MAACA;AAAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,gBAAc;AAAA,QACd;AAAA,QACA,cACE,SACI,GAAG,OAAO,aAAa,KAAK,OAAO,gBAAgB,WAAW,CAAC,KAC/D,OAAO;AAAA,QAEb,WAAW,GAAG,2BAA2B,EAAE,OAAO,KAAA,CAAM,GAAG,SAAS;AAAA,QACnE,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAA,oBAAC,aAAA,EAAY;AAAA,UACZ,UACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACC,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAQA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwC;AACtC,QAAM,SAAS,kBAAA;AACf,QAAM,aAAa,cAAc;AACjC,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,YAAY;AACvE,QAAM,QAAQ,aAAa;AAC3B,QAAM,MAAM,UAAU;AAEtB,QAAM,QACJ,YAAY,eACR,MACE,OAAO,wBACP,OAAO,sBACT,MACE,OAAO,oBACP,OAAO;AAEf,QAAM,OACJ,YAAY,eACR,MACE,sBACA,mBACF,MACE,uBACA;AAER,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc;AAAA,MACd,cAAY;AAAA,MACZ,SAAS;AAAA,MACT,iBAAiB,CAAC,YAAY;AAC5B,cAAM,OAA0B,UAAU,QAAQ;AAClD,YAAI,CAAC,YAAY;AACf,+BAAqB,IAAI;AAAA,QAC3B;AACA,wBAAgB,IAAI;AAAA,MACtB;AAAA,MACA,cAAY;AAAA,MACZ,WAAW,GAAG,2BAA2B,EAAE,MAAA,CAAO,GAAG,SAAS;AAAA,MAC7D,GAAG;AAAA,MAEJ,8BAAC,MAAA,CAAA,CAAK;AAAA,IAAA;AAAA,EAAA;AAGZ;"}
1
+ {"version":3,"file":"video-control-button.js","sources":["../../../src/components/video/video-control-button.tsx"],"sourcesContent":["import { Button as BaseButton } from \"@base-ui/react/button\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { cva } from \"class-variance-authority\";\nimport { type ComponentProps, useState } from \"react\";\n\nimport {\n MessageIcon,\n Microphone01Icon,\n MicrophoneOff01Icon,\n VideoRecorderIcon,\n VideoRecorderOffIcon,\n} from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\n\nexport type VideoControl = \"microphone\" | \"camera\" | \"chat\";\n\n/** `on`: the device works · `off`: muted microphone / camera turned off. */\nexport type VideoControlState = \"on\" | \"off\";\n\nexport const videoControlButtonVariants = cva(\n [\n \"relative inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-xl\",\n \"text-text-inverse interactive pressable focus-ring [&_svg]:size-icon-md\",\n \"data-disabled:cursor-not-allowed\",\n ],\n {\n variants: {\n state: {\n on: \"border border-border-inverse bg-surface-inverse\",\n off: \"bg-error-strong\",\n },\n },\n defaultVariants: {\n state: \"on\",\n },\n },\n);\n\nexport interface VideoControlButtonProps\n extends Omit<ComponentProps<\"button\">, \"children\" | \"value\"> {\n control: VideoControl;\n /**\n * Microphone and camera. Controlled: pass it with `onStateChange`. `chat` has no\n * off state: it opens the chat.\n */\n state?: VideoControlState;\n /** Uncontrolled initial state. Default `on`. */\n defaultState?: VideoControlState;\n onStateChange?: (state: VideoControlState) => void;\n /** `chat` only: new messages. Shows the red dot and says it in the button's name. */\n unreadCount?: number;\n}\n\n/**\n * Button over the video: microphone, camera and (on mobile) chat. 44×44 drawn:\n * over the video there is no label next to it to extend the hit area.\n *\n * - On: `surface-inverse` fill. Off: `error-strong` fill AND the crossed-out icon —\n * colour is never the only cue. White icon in both, over any image (the button\n * is opaque).\n * - Microphone and camera are toggle buttons (`aria-pressed`, pressed = muted /\n * camera off) and their name says the action: \"Silenciar micrófono\" / \"Activar\n * micrófono\", \"Apagar cámara\" / \"Encender cámara\".\n * - Chat is a plain button: \"Abrir chat, 1 mensaje nuevo\".\n */\nexport function VideoControlButton({\n control,\n state: stateProp,\n defaultState = \"on\",\n onStateChange,\n unreadCount = 0,\n className,\n type = \"button\",\n ...props\n}: VideoControlButtonProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n if (control === \"chat\") {\n const unread = unreadCount > 0;\n return (\n <BaseButton\n data-slot=\"video-control-button\"\n data-control={control}\n type={type}\n aria-label={\n unread\n ? `${labels.videoOpenChat}, ${labels.chatNewMessages(unreadCount)}`\n : labels.videoOpenChat\n }\n className={cn(videoControlButtonVariants({ state: \"on\" }), className)}\n {...props}\n >\n <MessageIcon />\n {unread && (\n <span\n data-slot=\"video-control-button-unread\"\n className=\"absolute top-1 right-1 size-2.5 rounded-full bg-error ring-2 ring-text-inverse\"\n />\n )}\n </BaseButton>\n );\n }\n\n return (\n <VideoToggle\n control={control}\n state={stateProp}\n defaultState={defaultState}\n onStateChange={onStateChange}\n className={className}\n type={type}\n {...props}\n />\n );\n}\n\ninterface VideoToggleProps\n extends Omit<VideoControlButtonProps, \"control\" | \"unreadCount\"> {\n control: Exclude<VideoControl, \"chat\">;\n defaultState: VideoControlState;\n}\n\nfunction VideoToggle({\n control,\n state: stateProp,\n defaultState,\n onStateChange,\n className,\n ...props\n}: VideoToggleProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n const controlled = stateProp !== undefined;\n const [uncontrolledState, setUncontrolledState] = useState(defaultState);\n const state = stateProp ?? uncontrolledState;\n const off = state === \"off\";\n\n const label =\n control === \"microphone\"\n ? off\n ? labels.videoUnmuteMicrophone\n : labels.videoMuteMicrophone\n : off\n ? labels.videoTurnOnCamera\n : labels.videoTurnOffCamera;\n\n const Icon =\n control === \"microphone\"\n ? off\n ? MicrophoneOff01Icon\n : Microphone01Icon\n : off\n ? VideoRecorderOffIcon\n : VideoRecorderIcon;\n\n return (\n <Toggle\n data-slot=\"video-control-button\"\n data-control={control}\n data-state={state}\n pressed={off}\n onPressedChange={(pressed) => {\n const next: VideoControlState = pressed ? \"off\" : \"on\";\n if (!controlled) {\n setUncontrolledState(next);\n }\n onStateChange?.(next);\n }}\n aria-label={label}\n className={cn(videoControlButtonVariants({ state }), className)}\n {...props}\n >\n <Icon />\n </Toggle>\n );\n}\n"],"names":["BaseButton"],"mappings":";;;;;;;;;;;;AAoBO,MAAM,6BAA6B;AAAA,EACxC;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,IAAI;AAAA,QACJ,KAAK;AAAA,MAAA;AAAA,IACP;AAAA,IAEF,iBAAiB;AAAA,MACf,OAAO;AAAA,IAAA;AAAA,EACT;AAEJ;AA6BO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA,OAAO;AAAA,EACP,eAAe;AAAA,EACf;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,OAAO;AAAA,EACP,GAAG;AACL,GAA+C;AAC7C,QAAM,SAAS,kBAAA;AAEf,MAAI,YAAY,QAAQ;AACtB,UAAM,SAAS,cAAc;AAC7B,WACE;AAAA,MAACA;AAAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,gBAAc;AAAA,QACd;AAAA,QACA,cACE,SACI,GAAG,OAAO,aAAa,KAAK,OAAO,gBAAgB,WAAW,CAAC,KAC/D,OAAO;AAAA,QAEb,WAAW,GAAG,2BAA2B,EAAE,OAAO,KAAA,CAAM,GAAG,SAAS;AAAA,QACnE,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAA,oBAAC,aAAA,EAAY;AAAA,UACZ,UACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACC,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAQA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwC;AACtC,QAAM,SAAS,kBAAA;AACf,QAAM,aAAa,cAAc;AACjC,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,YAAY;AACvE,QAAM,QAAQ,aAAa;AAC3B,QAAM,MAAM,UAAU;AAEtB,QAAM,QACJ,YAAY,eACR,MACE,OAAO,wBACP,OAAO,sBACT,MACE,OAAO,oBACP,OAAO;AAEf,QAAM,OACJ,YAAY,eACR,MACE,sBACA,mBACF,MACE,uBACA;AAER,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc;AAAA,MACd,cAAY;AAAA,MACZ,SAAS;AAAA,MACT,iBAAiB,CAAC,YAAY;AAC5B,cAAM,OAA0B,UAAU,QAAQ;AAClD,YAAI,CAAC,YAAY;AACf,+BAAqB,IAAI;AAAA,QAC3B;AACA,wBAAgB,IAAI;AAAA,MACtB;AAAA,MACA,cAAY;AAAA,MACZ,WAAW,GAAG,2BAA2B,EAAE,MAAA,CAAO,GAAG,SAAS;AAAA,MAC7D,GAAG;AAAA,MAEJ,8BAAC,MAAA,CAAA,CAAK;AAAA,IAAA;AAAA,EAAA;AAGZ;"}
@@ -5,6 +5,7 @@ export interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, "children
5
5
  size?: IconSize;
6
6
  /** Accessible name. Without it the icon is decorative (aria-hidden). */
7
7
  title?: string;
8
+ hoverFill?: boolean;
8
9
  }
9
10
  /**
10
11
  * Builds a MediCore icon: 24×24 grid, 1.5 stroke (Figma `icon-stroke`) in
@@ -10,6 +10,7 @@ function createIcon(displayName, children) {
10
10
  function Icon({
11
11
  size = "lg",
12
12
  title,
13
+ hoverFill = false,
13
14
  className,
14
15
  ...props
15
16
  }) {
@@ -28,7 +29,12 @@ function createIcon(displayName, children) {
28
29
  "aria-label": labelled ? title : void 0,
29
30
  "aria-hidden": labelled ? void 0 : true,
30
31
  focusable: labelled ? void 0 : "false",
31
- className: cn("shrink-0", sizeClasses[size], className),
32
+ className: cn(
33
+ "shrink-0",
34
+ sizeClasses[size],
35
+ hoverFill && "hover-fill",
36
+ className
37
+ ),
32
38
  ...props,
33
39
  children: [
34
40
  labelled ? /* @__PURE__ */ jsx("title", { children: title }) : null,
@@ -1 +1 @@
1
- {"version":3,"file":"create-icon.js","sources":["../../src/icons/create-icon.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { cn } from \"../lib/cn\";\n\nexport type IconSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface IconProps\n extends Omit<React.SVGProps<SVGSVGElement>, \"children\"> {\n /** xs 12 · sm 16 · md 20 · lg 24. Default \"lg\" (the Figma grid). */\n size?: IconSize;\n /** Accessible name. Without it the icon is decorative (aria-hidden). */\n title?: string;\n}\n\nconst sizeClasses: Record<IconSize, string> = {\n xs: \"size-icon-xs\",\n sm: \"size-icon-sm\",\n md: \"size-icon-md\",\n lg: \"size-icon-lg\",\n};\n\n/**\n * Builds a MediCore icon: 24×24 grid, 1.5 stroke (Figma `icon-stroke`) in\n * `currentColor`, so it takes the text colour of its parent — recolour it with a\n * `text-*` token class, never with a hex.\n *\n * Figma: don't go below 16px (`sm`) for stroke icons, the line clogs; `xs` is only\n * for dense chrome. An icon is never the only cue for an action: an icon-only\n * control needs an accessible name (`title` here, or `aria-label` on the control).\n */\nexport function createIcon(\n displayName: string,\n children: ReactNode,\n): (props: IconProps) => React.JSX.Element {\n function Icon({\n size = \"lg\",\n title,\n className,\n ...props\n }: IconProps): React.JSX.Element {\n const labelled = Boolean(title);\n\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.5}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-slot=\"icon\"\n role={labelled ? \"img\" : undefined}\n aria-label={labelled ? title : undefined}\n aria-hidden={labelled ? undefined : true}\n focusable={labelled ? undefined : \"false\"}\n className={cn(\"shrink-0\", sizeClasses[size], className)}\n {...props}\n >\n {labelled ? <title>{title}</title> : null}\n {children}\n </svg>\n );\n }\n\n Icon.displayName = displayName;\n return Icon;\n}\n"],"names":[],"mappings":";;AAcA,MAAM,cAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,WACd,aACA,UACyC;AACzC,WAAS,KAAK;AAAA,IACZ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAC4B;AAC/B,UAAM,WAAW,QAAQ,KAAK;AAE9B,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,QAAO;AAAA,QACP,aAAa;AAAA,QACb,eAAc;AAAA,QACd,gBAAe;AAAA,QACf,aAAU;AAAA,QACV,MAAM,WAAW,QAAQ;AAAA,QACzB,cAAY,WAAW,QAAQ;AAAA,QAC/B,eAAa,WAAW,SAAY;AAAA,QACpC,WAAW,WAAW,SAAY;AAAA,QAClC,WAAW,GAAG,YAAY,YAAY,IAAI,GAAG,SAAS;AAAA,QACrD,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,WAAW,oBAAC,SAAA,EAAO,UAAA,MAAA,CAAM,IAAW;AAAA,UACpC;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,OAAK,cAAc;AACnB,SAAO;AACT;"}
1
+ {"version":3,"file":"create-icon.js","sources":["../../src/icons/create-icon.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { cn } from \"../lib/cn\";\n\nexport type IconSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface IconProps\n extends Omit<React.SVGProps<SVGSVGElement>, \"children\"> {\n /** xs 12 · sm 16 · md 20 · lg 24. Default \"lg\" (the Figma grid). */\n size?: IconSize;\n /** Accessible name. Without it the icon is decorative (aria-hidden). */\n title?: string;\n hoverFill?: boolean;\n}\n\nconst sizeClasses: Record<IconSize, string> = {\n xs: \"size-icon-xs\",\n sm: \"size-icon-sm\",\n md: \"size-icon-md\",\n lg: \"size-icon-lg\",\n};\n\n/**\n * Builds a MediCore icon: 24×24 grid, 1.5 stroke (Figma `icon-stroke`) in\n * `currentColor`, so it takes the text colour of its parent — recolour it with a\n * `text-*` token class, never with a hex.\n *\n * Figma: don't go below 16px (`sm`) for stroke icons, the line clogs; `xs` is only\n * for dense chrome. An icon is never the only cue for an action: an icon-only\n * control needs an accessible name (`title` here, or `aria-label` on the control).\n */\nexport function createIcon(\n displayName: string,\n children: ReactNode,\n): (props: IconProps) => React.JSX.Element {\n function Icon({\n size = \"lg\",\n title,\n hoverFill = false,\n className,\n ...props\n }: IconProps): React.JSX.Element {\n const labelled = Boolean(title);\n\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.5}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-slot=\"icon\"\n role={labelled ? \"img\" : undefined}\n aria-label={labelled ? title : undefined}\n aria-hidden={labelled ? undefined : true}\n focusable={labelled ? undefined : \"false\"}\n className={cn(\n \"shrink-0\",\n sizeClasses[size],\n hoverFill && \"hover-fill\",\n className,\n )}\n {...props}\n >\n {labelled ? <title>{title}</title> : null}\n {children}\n </svg>\n );\n }\n\n Icon.displayName = displayName;\n return Icon;\n}\n"],"names":[],"mappings":";;AAeA,MAAM,cAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,WACd,aACA,UACyC;AACzC,WAAS,KAAK;AAAA,IACZ,OAAO;AAAA,IACP;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA,GAAG;AAAA,EAAA,GAC4B;AAC/B,UAAM,WAAW,QAAQ,KAAK;AAE9B,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,QAAO;AAAA,QACP,aAAa;AAAA,QACb,eAAc;AAAA,QACd,gBAAe;AAAA,QACf,aAAU;AAAA,QACV,MAAM,WAAW,QAAQ;AAAA,QACzB,cAAY,WAAW,QAAQ;AAAA,QAC/B,eAAa,WAAW,SAAY;AAAA,QACpC,WAAW,WAAW,SAAY;AAAA,QAClC,WAAW;AAAA,UACT;AAAA,UACA,YAAY,IAAI;AAAA,UAChB,aAAa;AAAA,UACb;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,WAAW,oBAAC,SAAA,EAAO,UAAA,MAAA,CAAM,IAAW;AAAA,UACpC;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,OAAK,cAAc;AACnB,SAAO;AACT;"}
package/dist/index.d.ts CHANGED
@@ -34,6 +34,7 @@ export * from "./components/field";
34
34
  export * from "./components/footer";
35
35
  export * from "./components/health-record";
36
36
  export * from "./components/hero-greeting";
37
+ export * from "./components/hover-arrow";
37
38
  export * from "./components/icon-button";
38
39
  export * from "./components/icon-container";
39
40
  export * from "./components/included-item";
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ import { Field } from "./components/field/field.js";
47
47
  import { Footer, FooterLink } from "./components/footer/footer.js";
48
48
  import { HealthRecordHeader, HealthRecordRow, HealthRecordSection } from "./components/health-record/health-record.js";
49
49
  import { HeroGreeting } from "./components/hero-greeting/hero-greeting.js";
50
+ import { HoverArrow } from "./components/hover-arrow/hover-arrow.js";
50
51
  import { IconButton, iconButtonVariants } from "./components/icon-button/icon-button.js";
51
52
  import { IconContainer, iconContainerVariants } from "./components/icon-container/icon-container.js";
52
53
  import { IncludedItem } from "./components/included-item/included-item.js";
@@ -158,6 +159,7 @@ export {
158
159
  HealthRecordRow,
159
160
  HealthRecordSection,
160
161
  HeroGreeting,
162
+ HoverArrow,
161
163
  IconButton,
162
164
  IconContainer,
163
165
  IncludedItem,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -116,6 +116,9 @@
116
116
  /* Motion. Always behind motion-safe: in the components. */
117
117
  --animate-skeleton-shimmer: skeleton-shimmer 1.2s linear infinite;
118
118
 
119
+ --ease-out-soft: cubic-bezier(0.2, 0, 0, 1);
120
+ --duration-interactive: 150ms;
121
+
119
122
  @keyframes skeleton-shimmer {
120
123
  from {
121
124
  background-position: 100% 0;
@@ -35,3 +35,132 @@
35
35
  transform: translate(-50%, -50%);
36
36
  }
37
37
  }
38
+
39
+ @utility interactive {
40
+ transition-property:
41
+ color, background-color, border-color, outline-color,
42
+ text-decoration-color, box-shadow, opacity, filter, translate, scale;
43
+ transition-duration: var(--duration-interactive);
44
+ transition-timing-function: var(--ease-out-soft);
45
+
46
+ @media (prefers-reduced-motion: reduce) {
47
+ transition-property:
48
+ color, background-color, border-color, outline-color,
49
+ text-decoration-color, box-shadow, opacity, filter;
50
+ }
51
+ }
52
+
53
+ @utility pressable {
54
+ @media (prefers-reduced-motion: no-preference) {
55
+ &:active:not(:disabled, [data-disabled], [aria-disabled="true"]) {
56
+ scale: 0.97;
57
+ }
58
+ }
59
+ }
60
+
61
+ @utility hover-lift {
62
+ @media (hover: hover) {
63
+ &:has([data-slot="card-link"]):hover {
64
+ box-shadow: var(--shadow-lg);
65
+ }
66
+ }
67
+
68
+ @media (hover: hover) and (prefers-reduced-motion: no-preference) {
69
+ &:has([data-slot="card-link"]):hover {
70
+ translate: 0 -2px;
71
+ }
72
+ }
73
+ }
74
+
75
+ @utility hover-zoom-media {
76
+ & img {
77
+ transition: scale 500ms var(--ease-out-soft);
78
+ }
79
+
80
+ @media (hover: hover) and (prefers-reduced-motion: no-preference) {
81
+ &:has([data-slot="card-link"]):hover img {
82
+ scale: 1.05;
83
+ }
84
+ }
85
+ }
86
+
87
+ @custom-variant hover-triggered {
88
+ @media (hover: hover) {
89
+ :is(
90
+ .hover-trigger,
91
+ .hover-lift:has([data-slot="card-link"])
92
+ ):not([data-disabled], [aria-disabled="true"], :disabled):hover
93
+ & {
94
+ @slot;
95
+ }
96
+ }
97
+
98
+ :is(.hover-trigger, .hover-lift:has([data-slot="card-link"])):is(
99
+ :focus-visible,
100
+ :has(:focus-visible)
101
+ )
102
+ & {
103
+ @slot;
104
+ }
105
+ }
106
+
107
+ @utility hover-arrow {
108
+ display: inline-flex;
109
+ vertical-align: middle;
110
+ width: 0;
111
+ overflow: hidden;
112
+ margin-inline-start: calc(-1 * var(--hover-arrow-gap, 0px));
113
+ transition-property: width, margin-inline-start;
114
+ transition-duration: 300ms;
115
+ transition-timing-function: var(--ease-out-soft);
116
+
117
+ & > svg {
118
+ flex-shrink: 0;
119
+ opacity: 0;
120
+ translate: -0.375em 0;
121
+ transition-property: opacity, translate;
122
+ transition-duration: 300ms;
123
+ transition-timing-function: var(--ease-out-soft);
124
+ }
125
+
126
+ @variant hover-triggered {
127
+ width: var(--hover-arrow-size, 1em);
128
+ margin-inline-start: calc(
129
+ var(--hover-arrow-space, 0.25em) - var(--hover-arrow-gap, 0px)
130
+ );
131
+
132
+ & > svg {
133
+ opacity: 1;
134
+ translate: 0 0;
135
+ }
136
+ }
137
+
138
+ @media (prefers-reduced-motion: reduce) {
139
+ transition-property: none;
140
+
141
+ & > svg {
142
+ translate: 0 0;
143
+ transition-property: opacity;
144
+ }
145
+ }
146
+ }
147
+
148
+ @utility hover-fill {
149
+ fill: color-mix(in oklab, currentColor 0%, transparent);
150
+ transition: fill 300ms var(--ease-out-soft);
151
+
152
+ @variant hover-triggered {
153
+ fill: var(
154
+ --hover-fill-color,
155
+ color-mix(in oklab, currentColor 20%, transparent)
156
+ );
157
+ }
158
+ }
159
+
160
+ @utility hover-scale {
161
+ @media (hover: hover) and (prefers-reduced-motion: no-preference) {
162
+ &:has([data-slot="card-link"]):hover {
163
+ scale: 1.02;
164
+ }
165
+ }
166
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mediquo/medicore",
3
3
  "description": "MediCore — design system de las plataformas web de mediQuo",
4
- "version": "0.4.2",
4
+ "version": "0.5.1",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "sideEffects": [