@mediquo/medicore 0.1.1 → 0.3.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 (33) hide show
  1. package/dist/components/badge/badge.d.ts +7 -3
  2. package/dist/components/badge/badge.js +27 -4
  3. package/dist/components/badge/badge.js.map +1 -1
  4. package/dist/components/breadcrumb/breadcrumb.d.ts +12 -5
  5. package/dist/components/breadcrumb/breadcrumb.js +20 -4
  6. package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
  7. package/dist/components/button/button.js +9 -9
  8. package/dist/components/button/button.js.map +1 -1
  9. package/dist/components/chip/chip.d.ts +3 -0
  10. package/dist/components/chip/chip.js +41 -10
  11. package/dist/components/chip/chip.js.map +1 -1
  12. package/dist/components/detail-header/detail-header.d.ts +49 -0
  13. package/dist/components/detail-header/detail-header.js +92 -0
  14. package/dist/components/detail-header/detail-header.js.map +1 -0
  15. package/dist/components/detail-header/index.d.ts +1 -0
  16. package/dist/components/fact-card/fact-card.d.ts +34 -0
  17. package/dist/components/fact-card/fact-card.js +86 -0
  18. package/dist/components/fact-card/fact-card.js.map +1 -0
  19. package/dist/components/fact-card/index.d.ts +1 -0
  20. package/dist/components/input/control-styles.d.ts +1 -1
  21. package/dist/components/input/control-styles.js +4 -4
  22. package/dist/components/input/control-styles.js.map +1 -1
  23. package/dist/components/stat-card/index.d.ts +1 -0
  24. package/dist/components/stat-card/stat-card.d.ts +31 -0
  25. package/dist/components/stat-card/stat-card.js +85 -0
  26. package/dist/components/stat-card/stat-card.js.map +1 -0
  27. package/dist/components/table/table.d.ts +1 -1
  28. package/dist/components/table/table.js +18 -5
  29. package/dist/components/table/table.js.map +1 -1
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +9 -0
  32. package/dist/index.js.map +1 -1
  33. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  import type { ComponentProps } from "react";
2
2
  export declare const badgeVariants: (props?: ({
3
- variant?: "error" | "brand" | "neutral" | "success" | "warning" | "info" | null | undefined;
3
+ variant?: "outline" | "error" | "brand" | "neutral" | "success" | "warning" | "info" | null | undefined;
4
4
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
5
- export type BadgeVariant = "neutral" | "brand" | "success" | "warning" | "error" | "info";
5
+ export type BadgeVariant = "neutral" | "outline" | "brand" | "success" | "warning" | "error" | "info";
6
6
  export interface BadgeProps extends ComponentProps<"span"> {
7
7
  variant?: BadgeVariant;
8
+ /** A coloured dot in front: for statuses ("Abierta", "Pagada"), not for categories. */
9
+ dot?: boolean;
8
10
  }
9
11
  /**
10
12
  * Category or status tag (documents: Receta, Informe, Orden médica; program cards).
@@ -12,9 +14,11 @@ export interface BadgeProps extends ComponentProps<"span"> {
12
14
  * it isn't a badge, it's text.
13
15
  *
14
16
  * - It is NOT a button: no click, no focus, no close icon. Removable → `Chip`.
17
+ * - `outline` is the quiet one for plain metadata (a clinic's name next to a
18
+ * professional); the tinted variants tell a state.
15
19
  * - On a photo use `neutral` (solid). Never a translucent pill: its contrast depends
16
20
  * on the photo.
17
21
  * - `warning` keeps its text in text-primary: the orange-on-orange pair doesn't reach
18
22
  * 4.5:1 with the current tokens.
19
23
  */
20
- export declare function Badge({ variant, className, ...props }: BadgeProps): React.JSX.Element;
24
+ export declare function Badge({ variant, dot, className, children, ...props }: BadgeProps): React.JSX.Element;
@@ -1,12 +1,13 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
3
  import { cn } from "../../lib/cn.js";
4
4
  const badgeVariants = cva(
5
- "inline-flex w-fit shrink-0 items-center whitespace-nowrap rounded-sm px-2 py-1 text-label-small",
5
+ "inline-flex h-6 w-fit shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 text-label-small font-medium [&_svg]:size-icon-xs",
6
6
  {
7
7
  variants: {
8
8
  variant: {
9
9
  neutral: "bg-surface-raised text-text-primary",
10
+ outline: "bg-background text-text-secondary ring-1 ring-border ring-inset",
10
11
  brand: "bg-brand-subtle text-text-brand",
11
12
  success: "bg-success-subtle text-text-success",
12
13
  // Token gap, not an aesthetic choice: warning-strong on warning-subtle is
@@ -21,17 +22,39 @@ const badgeVariants = cva(
21
22
  }
22
23
  }
23
24
  );
25
+ const badgeDotVariants = cva("size-1.5 shrink-0 rounded-full", {
26
+ variants: {
27
+ variant: {
28
+ neutral: "bg-text-secondary",
29
+ outline: "bg-text-secondary",
30
+ brand: "bg-brand",
31
+ success: "bg-success",
32
+ warning: "bg-warning",
33
+ error: "bg-error",
34
+ info: "bg-info"
35
+ }
36
+ },
37
+ defaultVariants: {
38
+ variant: "neutral"
39
+ }
40
+ });
24
41
  function Badge({
25
42
  variant,
43
+ dot = false,
26
44
  className,
45
+ children,
27
46
  ...props
28
47
  }) {
29
- return /* @__PURE__ */ jsx(
48
+ return /* @__PURE__ */ jsxs(
30
49
  "span",
31
50
  {
32
51
  "data-slot": "badge",
33
52
  className: cn(badgeVariants({ variant }), className),
34
- ...props
53
+ ...props,
54
+ children: [
55
+ dot && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: badgeDotVariants({ variant }) }),
56
+ children
57
+ ]
35
58
  }
36
59
  );
37
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"badge.js","sources":["../../../src/components/badge/badge.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport const badgeVariants = cva(\n \"inline-flex w-fit shrink-0 items-center whitespace-nowrap rounded-sm px-2 py-1 text-label-small\",\n {\n variants: {\n variant: {\n neutral: \"bg-surface-raised text-text-primary\",\n brand: \"bg-brand-subtle text-text-brand\",\n success: \"bg-success-subtle text-text-success\",\n // Token gap, not an aesthetic choice: warning-strong on warning-subtle is\n // 3.93:1, so the text stays in text-primary and the orange lives in the fill.\n warning: \"bg-warning-subtle text-text-primary\",\n error: \"bg-error-subtle text-text-error\",\n info: \"bg-info-subtle text-text-info\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n },\n },\n);\n\nexport type BadgeVariant =\n | \"neutral\"\n | \"brand\"\n | \"success\"\n | \"warning\"\n | \"error\"\n | \"info\";\n\nexport interface BadgeProps extends ComponentProps<\"span\"> {\n variant?: BadgeVariant;\n}\n\n/**\n * Category or status tag (documents: Receta, Informe, Orden médica; program cards).\n * 12px (Label/Small) because it is metadata: if people need to read it to decide,\n * it isn't a badge, it's text.\n *\n * - It is NOT a button: no click, no focus, no close icon. Removable → `Chip`.\n * - On a photo use `neutral` (solid). Never a translucent pill: its contrast depends\n * on the photo.\n * - `warning` keeps its text in text-primary: the orange-on-orange pair doesn't reach\n * 4.5:1 with the current tokens.\n */\nexport function Badge({\n variant,\n className,\n ...props\n}: BadgeProps): React.JSX.Element {\n return (\n <span\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n );\n}\n"],"names":[],"mappings":";;;AAKO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA;AAAA;AAAA,QAGT,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,MAAA;AAAA,IACR;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,IAAA;AAAA,EACX;AAEJ;AAyBO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkC;AAChC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,EAAE,QAAA,CAAS,GAAG,SAAS;AAAA,MAClD,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
1
+ {"version":3,"file":"badge.js","sources":["../../../src/components/badge/badge.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nexport const badgeVariants = cva(\n \"inline-flex h-6 w-fit shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 text-label-small font-medium [&_svg]:size-icon-xs\",\n {\n variants: {\n variant: {\n neutral: \"bg-surface-raised text-text-primary\",\n outline:\n \"bg-background text-text-secondary ring-1 ring-border ring-inset\",\n brand: \"bg-brand-subtle text-text-brand\",\n success: \"bg-success-subtle text-text-success\",\n // Token gap, not an aesthetic choice: warning-strong on warning-subtle is\n // 3.93:1, so the text stays in text-primary and the orange lives in the fill.\n warning: \"bg-warning-subtle text-text-primary\",\n error: \"bg-error-subtle text-text-error\",\n info: \"bg-info-subtle text-text-info\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n },\n },\n);\n\nconst badgeDotVariants = cva(\"size-1.5 shrink-0 rounded-full\", {\n variants: {\n variant: {\n neutral: \"bg-text-secondary\",\n outline: \"bg-text-secondary\",\n brand: \"bg-brand\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n error: \"bg-error\",\n info: \"bg-info\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n },\n});\n\nexport type BadgeVariant =\n | \"neutral\"\n | \"outline\"\n | \"brand\"\n | \"success\"\n | \"warning\"\n | \"error\"\n | \"info\";\n\nexport interface BadgeProps extends ComponentProps<\"span\"> {\n variant?: BadgeVariant;\n /** A coloured dot in front: for statuses (\"Abierta\", \"Pagada\"), not for categories. */\n dot?: boolean;\n}\n\n/**\n * Category or status tag (documents: Receta, Informe, Orden médica; program cards).\n * 12px (Label/Small) because it is metadata: if people need to read it to decide,\n * it isn't a badge, it's text.\n *\n * - It is NOT a button: no click, no focus, no close icon. Removable → `Chip`.\n * - `outline` is the quiet one for plain metadata (a clinic's name next to a\n * professional); the tinted variants tell a state.\n * - On a photo use `neutral` (solid). Never a translucent pill: its contrast depends\n * on the photo.\n * - `warning` keeps its text in text-primary: the orange-on-orange pair doesn't reach\n * 4.5:1 with the current tokens.\n */\nexport function Badge({\n variant,\n dot = false,\n className,\n children,\n ...props\n}: BadgeProps): React.JSX.Element {\n return (\n <span\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n >\n {dot && (\n <span aria-hidden=\"true\" className={badgeDotVariants({ variant })} />\n )}\n {children}\n </span>\n );\n}\n"],"names":[],"mappings":";;;AAKO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SACE;AAAA,QACF,OAAO;AAAA,QACP,SAAS;AAAA;AAAA;AAAA,QAGT,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM;AAAA,MAAA;AAAA,IACR;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,IAAA;AAAA,EACX;AAEJ;AAEA,MAAM,mBAAmB,IAAI,kCAAkC;AAAA,EAC7D,UAAU;AAAA,IACR,SAAS;AAAA,MACP,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,MACP,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,MACP,MAAM;AAAA,IAAA;AAAA,EACR;AAAA,EAEF,iBAAiB;AAAA,IACf,SAAS;AAAA,EAAA;AAEb,CAAC;AA8BM,SAAS,MAAM;AAAA,EACpB;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkC;AAChC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,EAAE,QAAA,CAAS,GAAG,SAAS;AAAA,MAClD,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA,OACC,oBAAC,UAAK,eAAY,QAAO,WAAW,iBAAiB,EAAE,QAAA,CAAS,GAAG;AAAA,QAEpE;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
@@ -1,5 +1,5 @@
1
+ import { useRender } from "@base-ui/react/use-render";
1
2
  import type { ComponentProps, ReactNode } from "react";
2
- import { type LinkProps } from "../link";
3
3
  export interface BreadcrumbProps extends ComponentProps<"nav"> {
4
4
  /** `BreadcrumbItem`s for the levels above, then one `BreadcrumbPage` for the current one. */
5
5
  children: ReactNode;
@@ -17,19 +17,26 @@ export interface BreadcrumbProps extends ComponentProps<"nav"> {
17
17
  * </Breadcrumb>
18
18
  * ```
19
19
  *
20
+ * The levels above are quiet (Label/Default in text-secondary, brand on hover), with
21
+ * a chevron in border-strong between them, so the trail never competes with the
22
+ * page title under it. The links are not underlined: inside the breadcrumb landmark
23
+ * and its list, their position already tells they are links (WCAG 1.4.1 applies to
24
+ * links in running text).
25
+ *
20
26
  * Below the `lg` breakpoint only the level right above shows, as a way back with an
21
27
  * arrow ("‹ Programas especializados").
22
28
  */
23
29
  export declare function Breadcrumb({ "aria-label": ariaLabel, className, children, ...props }: BreadcrumbProps): React.JSX.Element;
24
- export interface BreadcrumbItemProps extends Omit<LinkProps, "size"> {
30
+ export interface BreadcrumbItemProps extends useRender.ComponentProps<"a"> {
25
31
  /** The level's name, the same as the section or page it links to. */
26
32
  children: ReactNode;
27
33
  }
28
34
  /**
29
- * A level above the current page: a `Link` (pass the router's with `render`) followed
30
- * by a chevron. On mobile, the parent level gets a back arrow in front instead.
35
+ * A level above the current page: a link (`href`, or the router's with `render`)
36
+ * followed by a chevron. On mobile, the parent level gets a back arrow in front
37
+ * instead.
31
38
  */
32
- export declare function BreadcrumbItem({ className, children, ...props }: BreadcrumbItemProps): React.JSX.Element;
39
+ export declare function BreadcrumbItem({ render, ref, className, children, ...props }: BreadcrumbItemProps): React.JSX.Element;
33
40
  export interface BreadcrumbPageProps extends ComponentProps<"span"> {
34
41
  }
35
42
  /** The current level: text, not a link, with `aria-current="page"`. Hidden on mobile. */
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useRender } from "@base-ui/react/use-render";
2
3
  import { ChevronLeftIcon } from "../../icons/generated/chevron-left.js";
3
4
  import { ChevronRightIcon } from "../../icons/generated/chevron-right.js";
4
5
  import { cn } from "../../lib/cn.js";
5
6
  import { useMediCoreLabels } from "../../provider/medicore-provider.js";
6
- import { Link } from "../link/link.js";
7
7
  function Breadcrumb({
8
8
  "aria-label": ariaLabel,
9
9
  className,
@@ -33,18 +33,34 @@ function Breadcrumb({
33
33
  );
34
34
  }
35
35
  function BreadcrumbItem({
36
+ render,
37
+ ref,
36
38
  className,
37
39
  children,
38
40
  ...props
39
41
  }) {
42
+ const link = useRender({
43
+ render,
44
+ ref,
45
+ defaultTagName: "a",
46
+ props: {
47
+ "data-slot": "breadcrumb-link",
48
+ className: cn(
49
+ "focus-ring touch-target inline-flex items-center gap-1 rounded-xs text-label-default text-text-secondary transition-colors hover:text-text-brand",
50
+ className
51
+ ),
52
+ children,
53
+ ...props
54
+ }
55
+ });
40
56
  return /* @__PURE__ */ jsxs("li", { "data-slot": "breadcrumb-item", className: "flex items-center gap-2", children: [
41
57
  /* @__PURE__ */ jsx(ChevronLeftIcon, { size: "sm", className: "text-text-secondary lg:hidden" }),
42
- /* @__PURE__ */ jsx(Link, { standalone: true, className, ...props, children }),
58
+ link,
43
59
  /* @__PURE__ */ jsx(
44
60
  ChevronRightIcon,
45
61
  {
46
62
  size: "sm",
47
- className: "text-text-secondary max-lg:hidden"
63
+ className: "text-border-strong max-lg:hidden"
48
64
  }
49
65
  )
50
66
  ] });
@@ -57,7 +73,7 @@ function BreadcrumbPage({
57
73
  "span",
58
74
  {
59
75
  "aria-current": "page",
60
- className: cn("text-label-small text-text-primary", className),
76
+ className: cn("text-label-strong text-text-primary", className),
61
77
  ...props
62
78
  }
63
79
  ) });
@@ -1 +1 @@
1
- {"version":3,"file":"breadcrumb.js","sources":["../../../src/components/breadcrumb/breadcrumb.tsx"],"sourcesContent":["import type { ComponentProps, ReactNode } from \"react\";\n\nimport { ChevronLeftIcon, ChevronRightIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\nimport { Link, type LinkProps } from \"../link\";\n\nexport interface BreadcrumbProps extends ComponentProps<\"nav\"> {\n /** `BreadcrumbItem`s for the levels above, then one `BreadcrumbPage` for the current one. */\n children: ReactNode;\n}\n\n/**\n * Breadcrumb for pages two or three levels deep (a programme inside \"Programas\n * especializados\"). A `<nav>` named \"Migas de pan\" (`breadcrumb` label) with an\n * `<ol>`: the levels above are links, the last one is plain text with\n * `aria-current=\"page\"`.\n *\n * ```tsx\n * <Breadcrumb>\n * <BreadcrumbItem render={<Link to=\"/programas\" />}>Programas especializados</BreadcrumbItem>\n * <BreadcrumbPage>Pérdida de peso</BreadcrumbPage>\n * </Breadcrumb>\n * ```\n *\n * Below the `lg` breakpoint only the level right above shows, as a way back with an\n * arrow (\"‹ Programas especializados\").\n */\nexport function Breadcrumb({\n \"aria-label\": ariaLabel,\n className,\n children,\n ...props\n}: BreadcrumbProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n return (\n <nav\n data-slot=\"breadcrumb\"\n aria-label={ariaLabel ?? labels.breadcrumb}\n className={className}\n {...props}\n >\n <ol\n className={cn(\n \"flex flex-wrap items-center gap-2\",\n // Mobile: only the parent level (the second-to-last item) stays.\n \"max-lg:[&>li:not(:nth-last-child(2))]:hidden\",\n )}\n >\n {children}\n </ol>\n </nav>\n );\n}\n\nexport interface BreadcrumbItemProps extends Omit<LinkProps, \"size\"> {\n /** The level's name, the same as the section or page it links to. */\n children: ReactNode;\n}\n\n/**\n * A level above the current page: a `Link` (pass the router's with `render`) followed\n * by a chevron. On mobile, the parent level gets a back arrow in front instead.\n */\nexport function BreadcrumbItem({\n className,\n children,\n ...props\n}: BreadcrumbItemProps): React.JSX.Element {\n return (\n <li data-slot=\"breadcrumb-item\" className=\"flex items-center gap-2\">\n <ChevronLeftIcon size=\"sm\" className=\"text-text-secondary lg:hidden\" />\n <Link standalone className={className} {...props}>\n {children}\n </Link>\n <ChevronRightIcon\n size=\"sm\"\n className=\"text-text-secondary max-lg:hidden\"\n />\n </li>\n );\n}\n\nexport interface BreadcrumbPageProps extends ComponentProps<\"span\"> {}\n\n/** The current level: text, not a link, with `aria-current=\"page\"`. Hidden on mobile. */\nexport function BreadcrumbPage({\n className,\n ...props\n}: BreadcrumbPageProps): React.JSX.Element {\n return (\n <li data-slot=\"breadcrumb-page\" className=\"flex items-center\">\n <span\n aria-current=\"page\"\n className={cn(\"text-label-small text-text-primary\", className)}\n {...props}\n />\n </li>\n );\n}\n"],"names":[],"mappings":";;;;;;AA4BO,SAAS,WAAW;AAAA,EACzB,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuC;AACrC,QAAM,SAAS,kBAAA;AAEf,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,cAAY,aAAa,OAAO;AAAA,MAChC;AAAA,MACC,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA;AAAA,YAEA;AAAA,UAAA;AAAA,UAGD;AAAA,QAAA;AAAA,MAAA;AAAA,IACH;AAAA,EAAA;AAGN;AAWO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA2C;AACzC,SACE,qBAAC,MAAA,EAAG,aAAU,mBAAkB,WAAU,2BACxC,UAAA;AAAA,IAAA,oBAAC,iBAAA,EAAgB,MAAK,MAAK,WAAU,iCAAgC;AAAA,wBACpE,MAAA,EAAK,YAAU,MAAC,WAAuB,GAAG,OACxC,UACH;AAAA,IACA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,MAAA;AAAA,IAAA;AAAA,EACZ,GACF;AAEJ;AAKO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA,GAAG;AACL,GAA2C;AACzC,SACE,oBAAC,MAAA,EAAG,aAAU,mBAAkB,WAAU,qBACxC,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,gBAAa;AAAA,MACb,WAAW,GAAG,sCAAsC,SAAS;AAAA,MAC5D,GAAG;AAAA,IAAA;AAAA,EAAA,GAER;AAEJ;"}
1
+ {"version":3,"file":"breadcrumb.js","sources":["../../../src/components/breadcrumb/breadcrumb.tsx"],"sourcesContent":["import { useRender } from \"@base-ui/react/use-render\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { ChevronLeftIcon, ChevronRightIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\n\nexport interface BreadcrumbProps extends ComponentProps<\"nav\"> {\n /** `BreadcrumbItem`s for the levels above, then one `BreadcrumbPage` for the current one. */\n children: ReactNode;\n}\n\n/**\n * Breadcrumb for pages two or three levels deep (a programme inside \"Programas\n * especializados\"). A `<nav>` named \"Migas de pan\" (`breadcrumb` label) with an\n * `<ol>`: the levels above are links, the last one is plain text with\n * `aria-current=\"page\"`.\n *\n * ```tsx\n * <Breadcrumb>\n * <BreadcrumbItem render={<Link to=\"/programas\" />}>Programas especializados</BreadcrumbItem>\n * <BreadcrumbPage>Pérdida de peso</BreadcrumbPage>\n * </Breadcrumb>\n * ```\n *\n * The levels above are quiet (Label/Default in text-secondary, brand on hover), with\n * a chevron in border-strong between them, so the trail never competes with the\n * page title under it. The links are not underlined: inside the breadcrumb landmark\n * and its list, their position already tells they are links (WCAG 1.4.1 applies to\n * links in running text).\n *\n * Below the `lg` breakpoint only the level right above shows, as a way back with an\n * arrow (\"‹ Programas especializados\").\n */\nexport function Breadcrumb({\n \"aria-label\": ariaLabel,\n className,\n children,\n ...props\n}: BreadcrumbProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n return (\n <nav\n data-slot=\"breadcrumb\"\n aria-label={ariaLabel ?? labels.breadcrumb}\n className={className}\n {...props}\n >\n <ol\n className={cn(\n \"flex flex-wrap items-center gap-2\",\n // Mobile: only the parent level (the second-to-last item) stays.\n \"max-lg:[&>li:not(:nth-last-child(2))]:hidden\",\n )}\n >\n {children}\n </ol>\n </nav>\n );\n}\n\nexport interface BreadcrumbItemProps extends useRender.ComponentProps<\"a\"> {\n /** The level's name, the same as the section or page it links to. */\n children: ReactNode;\n}\n\n/**\n * A level above the current page: a link (`href`, or the router's with `render`)\n * followed by a chevron. On mobile, the parent level gets a back arrow in front\n * instead.\n */\nexport function BreadcrumbItem({\n render,\n ref,\n className,\n children,\n ...props\n}: BreadcrumbItemProps): React.JSX.Element {\n const link = useRender({\n render,\n ref,\n defaultTagName: \"a\",\n props: {\n \"data-slot\": \"breadcrumb-link\",\n className: cn(\n \"focus-ring touch-target inline-flex items-center gap-1 rounded-xs text-label-default text-text-secondary transition-colors hover:text-text-brand\",\n className,\n ),\n children,\n ...props,\n },\n });\n\n return (\n <li data-slot=\"breadcrumb-item\" className=\"flex items-center gap-2\">\n <ChevronLeftIcon size=\"sm\" className=\"text-text-secondary lg:hidden\" />\n {link}\n <ChevronRightIcon\n size=\"sm\"\n className=\"text-border-strong max-lg:hidden\"\n />\n </li>\n );\n}\n\nexport interface BreadcrumbPageProps extends ComponentProps<\"span\"> {}\n\n/** The current level: text, not a link, with `aria-current=\"page\"`. Hidden on mobile. */\nexport function BreadcrumbPage({\n className,\n ...props\n}: BreadcrumbPageProps): React.JSX.Element {\n return (\n <li data-slot=\"breadcrumb-page\" className=\"flex items-center\">\n <span\n aria-current=\"page\"\n className={cn(\"text-label-strong text-text-primary\", className)}\n {...props}\n />\n </li>\n );\n}\n"],"names":[],"mappings":";;;;;;AAkCO,SAAS,WAAW;AAAA,EACzB,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuC;AACrC,QAAM,SAAS,kBAAA;AAEf,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,cAAY,aAAa,OAAO;AAAA,MAChC;AAAA,MACC,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA;AAAA,YAEA;AAAA,UAAA;AAAA,UAGD;AAAA,QAAA;AAAA,MAAA;AAAA,IACH;AAAA,EAAA;AAGN;AAYO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA2C;AACzC,QAAM,OAAO,UAAU;AAAA,IACrB;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB,OAAO;AAAA,MACL,aAAa;AAAA,MACb,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAEF;AAAA,MACA,GAAG;AAAA,IAAA;AAAA,EACL,CACD;AAED,SACE,qBAAC,MAAA,EAAG,aAAU,mBAAkB,WAAU,2BACxC,UAAA;AAAA,IAAA,oBAAC,iBAAA,EAAgB,MAAK,MAAK,WAAU,iCAAgC;AAAA,IACpE;AAAA,IACD;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,MAAA;AAAA,IAAA;AAAA,EACZ,GACF;AAEJ;AAKO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA,GAAG;AACL,GAA2C;AACzC,SACE,oBAAC,MAAA,EAAG,aAAU,mBAAkB,WAAU,qBACxC,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,gBAAa;AAAA,MACb,WAAW,GAAG,uCAAuC,SAAS;AAAA,MAC7D,GAAG;AAAA,IAAA;AAAA,EAAA,GAER;AAEJ;"}
@@ -6,20 +6,20 @@ import { Spinner } from "../spinner/spinner.js";
6
6
  const buttonVariants = cva(
7
7
  [
8
8
  "inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap",
9
- "rounded-md border border-transparent text-label-strong transition-colors",
9
+ "rounded-md border border-transparent text-label-strong transition-[color,background-color,border-color,box-shadow]",
10
10
  "focus-ring touch-target",
11
11
  "[&_svg]:shrink-0 [&_svg]:size-icon-sm",
12
- "data-disabled:cursor-not-allowed aria-busy:cursor-progress"
12
+ "data-disabled:cursor-not-allowed data-disabled:shadow-none aria-busy:cursor-progress"
13
13
  ],
14
14
  {
15
15
  variants: {
16
16
  variant: {
17
- primary: "bg-brand text-text-inverse not-data-disabled:hover:bg-brand-strong",
18
- secondary: "border-text-brand bg-brand-subtle text-text-brand not-data-disabled:hover:bg-surface",
19
- outline: "border-border-input bg-background text-text-primary not-data-disabled:hover:bg-surface",
17
+ primary: "bg-brand text-text-inverse shadow-xs not-data-disabled:hover:bg-brand-strong",
18
+ secondary: "border-text-brand bg-brand-subtle text-text-brand not-data-disabled:hover:bg-background",
19
+ outline: "border-border bg-background text-text-primary shadow-xs not-data-disabled:hover:border-border-strong not-data-disabled:hover:bg-surface",
20
20
  ghost: "bg-transparent text-text-primary not-data-disabled:hover:bg-surface",
21
- destructive: "bg-error-strong text-text-inverse",
22
- inverse: "bg-background text-text-brand not-data-disabled:hover:bg-surface",
21
+ destructive: "bg-error-strong text-text-inverse shadow-xs",
22
+ inverse: "bg-background text-text-brand shadow-xs not-data-disabled:hover:bg-surface",
23
23
  "inverse-ghost": "bg-transparent text-text-inverse not-data-disabled:hover:bg-overlay-light"
24
24
  },
25
25
  size: {
@@ -37,12 +37,12 @@ const buttonVariants = cva(
37
37
  {
38
38
  variant: ["primary", "destructive"],
39
39
  disabled: true,
40
- className: "bg-surface text-text-disabled"
40
+ className: "bg-surface-raised text-text-disabled"
41
41
  },
42
42
  {
43
43
  variant: ["secondary", "outline"],
44
44
  disabled: true,
45
- className: "border-border bg-surface text-text-disabled"
45
+ className: "border-border-subtle bg-surface text-text-disabled"
46
46
  },
47
47
  {
48
48
  variant: "ghost",
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","sources":["../../../src/components/button/button.tsx"],"sourcesContent":["import { Button as BaseButton } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Spinner } from \"../spinner\";\n\nexport const buttonVariants = cva(\n [\n \"inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap\",\n \"rounded-md border border-transparent text-label-strong transition-colors\",\n \"focus-ring touch-target\",\n \"[&_svg]:shrink-0 [&_svg]:size-icon-sm\",\n \"data-disabled:cursor-not-allowed aria-busy:cursor-progress\",\n ],\n {\n variants: {\n variant: {\n primary:\n \"bg-brand text-text-inverse not-data-disabled:hover:bg-brand-strong\",\n secondary:\n \"border-text-brand bg-brand-subtle text-text-brand not-data-disabled:hover:bg-surface\",\n outline:\n \"border-border-input bg-background text-text-primary not-data-disabled:hover:bg-surface\",\n ghost:\n \"bg-transparent text-text-primary not-data-disabled:hover:bg-surface\",\n destructive: \"bg-error-strong text-text-inverse\",\n inverse:\n \"bg-background text-text-brand not-data-disabled:hover:bg-surface\",\n \"inverse-ghost\":\n \"bg-transparent text-text-inverse not-data-disabled:hover:bg-overlay-light\",\n },\n size: {\n default: \"h-control-md px-4\",\n small: \"h-control-sm px-3\",\n },\n disabled: {\n true: \"\",\n false: \"\",\n },\n },\n // Disabled uses explicit colours, not opacity: an opacity can't be a token and it\n // also dims the border.\n compoundVariants: [\n {\n variant: [\"primary\", \"destructive\"],\n disabled: true,\n className: \"bg-surface text-text-disabled\",\n },\n {\n variant: [\"secondary\", \"outline\"],\n disabled: true,\n className: \"border-border bg-surface text-text-disabled\",\n },\n {\n variant: \"ghost\",\n disabled: true,\n className: \"text-text-disabled\",\n },\n {\n variant: \"inverse\",\n disabled: true,\n className: \"bg-border-inverse text-text-inverse-secondary\",\n },\n {\n variant: \"inverse-ghost\",\n disabled: true,\n className: \"text-text-inverse-secondary\",\n },\n ],\n defaultVariants: {\n variant: \"primary\",\n size: \"default\",\n disabled: false,\n },\n },\n);\n\ntype ButtonVariantProps = Omit<VariantProps<typeof buttonVariants>, \"disabled\">;\n\nexport interface ButtonProps\n extends Omit<BaseButton.Props, \"className\">,\n ButtonVariantProps {\n /** Keeps the variant colour, shows a spinner and ignores clicks without losing focus. */\n loading?: boolean;\n leadingIcon?: ReactNode;\n trailingIcon?: ReactNode;\n className?: string;\n}\n\n/**\n * Action button. A button ACTS; to navigate use `Link` (or `render={<a href />}` when\n * it has to look like a button).\n *\n * Hierarchy, from most to least weight: primary → secondary → outline → ghost.\n * `destructive` is a warning, not a level. `inverse` and `inverse-ghost` are only\n * for dark surfaces (`bg-brand-dark`). One primary action per block.\n *\n * The drawing is 40px (36 small) but the hit area is always 44px. Full width: pass\n * `className=\"w-full\"`.\n */\nexport function Button({\n variant,\n size,\n disabled = false,\n loading = false,\n leadingIcon,\n trailingIcon,\n className,\n children,\n type = \"button\",\n ...props\n}: ButtonProps): React.JSX.Element {\n return (\n <BaseButton\n data-slot=\"button\"\n type={type}\n disabled={disabled || loading}\n focusableWhenDisabled={loading}\n aria-busy={loading || undefined}\n className={cn(buttonVariants({ variant, size, disabled }), className)}\n {...props}\n >\n {loading ? (\n <Spinner size={16} label={null} className=\"text-current\" />\n ) : (\n leadingIcon\n )}\n {children}\n {!loading && trailingIcon}\n </BaseButton>\n );\n}\n"],"names":["BaseButton"],"mappings":";;;;;AAOO,MAAM,iBAAiB;AAAA,EAC5B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SACE;AAAA,QACF,WACE;AAAA,QACF,SACE;AAAA,QACF,OACE;AAAA,QACF,aAAa;AAAA,QACb,SACE;AAAA,QACF,iBACE;AAAA,MAAA;AAAA,MAEJ,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,MAET,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA;AAAA;AAAA,IAIF,kBAAkB;AAAA,MAChB;AAAA,QACE,SAAS,CAAC,WAAW,aAAa;AAAA,QAClC,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS,CAAC,aAAa,SAAS;AAAA,QAChC,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,MACN,UAAU;AAAA,IAAA;AAAA,EACZ;AAEJ;AAyBO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,GAAG;AACL,GAAmC;AACjC,SACE;AAAA,IAACA;AAAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,UAAU,YAAY;AAAA,MACtB,uBAAuB;AAAA,MACvB,aAAW,WAAW;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,SAAA,CAAU,GAAG,SAAS;AAAA,MACnE,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA,UACC,oBAAC,WAAQ,MAAM,IAAI,OAAO,MAAM,WAAU,gBAAe,IAEzD;AAAA,QAED;AAAA,QACA,CAAC,WAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGnB;"}
1
+ {"version":3,"file":"button.js","sources":["../../../src/components/button/button.tsx"],"sourcesContent":["import { Button as BaseButton } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Spinner } from \"../spinner\";\n\nexport const buttonVariants = cva(\n [\n \"inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap\",\n \"rounded-md border border-transparent text-label-strong transition-[color,background-color,border-color,box-shadow]\",\n \"focus-ring touch-target\",\n \"[&_svg]:shrink-0 [&_svg]:size-icon-sm\",\n \"data-disabled:cursor-not-allowed data-disabled:shadow-none aria-busy:cursor-progress\",\n ],\n {\n variants: {\n variant: {\n primary:\n \"bg-brand text-text-inverse shadow-xs not-data-disabled:hover:bg-brand-strong\",\n secondary:\n \"border-text-brand bg-brand-subtle text-text-brand not-data-disabled:hover:bg-background\",\n outline:\n \"border-border bg-background text-text-primary shadow-xs not-data-disabled:hover:border-border-strong not-data-disabled:hover:bg-surface\",\n ghost:\n \"bg-transparent text-text-primary not-data-disabled:hover:bg-surface\",\n destructive: \"bg-error-strong text-text-inverse shadow-xs\",\n inverse:\n \"bg-background text-text-brand shadow-xs not-data-disabled:hover:bg-surface\",\n \"inverse-ghost\":\n \"bg-transparent text-text-inverse not-data-disabled:hover:bg-overlay-light\",\n },\n size: {\n default: \"h-control-md px-4\",\n small: \"h-control-sm px-3\",\n },\n disabled: {\n true: \"\",\n false: \"\",\n },\n },\n // Disabled uses explicit colours, not opacity: an opacity can't be a token and it\n // also dims the border.\n compoundVariants: [\n {\n variant: [\"primary\", \"destructive\"],\n disabled: true,\n className: \"bg-surface-raised text-text-disabled\",\n },\n {\n variant: [\"secondary\", \"outline\"],\n disabled: true,\n className: \"border-border-subtle bg-surface text-text-disabled\",\n },\n {\n variant: \"ghost\",\n disabled: true,\n className: \"text-text-disabled\",\n },\n {\n variant: \"inverse\",\n disabled: true,\n className: \"bg-border-inverse text-text-inverse-secondary\",\n },\n {\n variant: \"inverse-ghost\",\n disabled: true,\n className: \"text-text-inverse-secondary\",\n },\n ],\n defaultVariants: {\n variant: \"primary\",\n size: \"default\",\n disabled: false,\n },\n },\n);\n\ntype ButtonVariantProps = Omit<VariantProps<typeof buttonVariants>, \"disabled\">;\n\nexport interface ButtonProps\n extends Omit<BaseButton.Props, \"className\">,\n ButtonVariantProps {\n /** Keeps the variant colour, shows a spinner and ignores clicks without losing focus. */\n loading?: boolean;\n leadingIcon?: ReactNode;\n trailingIcon?: ReactNode;\n className?: string;\n}\n\n/**\n * Action button. A button ACTS; to navigate use `Link` (or `render={<a href />}` when\n * it has to look like a button).\n *\n * Hierarchy, from most to least weight: primary → secondary → outline → ghost.\n * `destructive` is a warning, not a level. `inverse` and `inverse-ghost` are only\n * for dark surfaces (`bg-brand-dark`). One primary action per block.\n *\n * The drawing is 40px (36 small) but the hit area is always 44px. Full width: pass\n * `className=\"w-full\"`.\n */\nexport function Button({\n variant,\n size,\n disabled = false,\n loading = false,\n leadingIcon,\n trailingIcon,\n className,\n children,\n type = \"button\",\n ...props\n}: ButtonProps): React.JSX.Element {\n return (\n <BaseButton\n data-slot=\"button\"\n type={type}\n disabled={disabled || loading}\n focusableWhenDisabled={loading}\n aria-busy={loading || undefined}\n className={cn(buttonVariants({ variant, size, disabled }), className)}\n {...props}\n >\n {loading ? (\n <Spinner size={16} label={null} className=\"text-current\" />\n ) : (\n leadingIcon\n )}\n {children}\n {!loading && trailingIcon}\n </BaseButton>\n );\n}\n"],"names":["BaseButton"],"mappings":";;;;;AAOO,MAAM,iBAAiB;AAAA,EAC5B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SACE;AAAA,QACF,WACE;AAAA,QACF,SACE;AAAA,QACF,OACE;AAAA,QACF,aAAa;AAAA,QACb,SACE;AAAA,QACF,iBACE;AAAA,MAAA;AAAA,MAEJ,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,MAET,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA;AAAA;AAAA,IAIF,kBAAkB;AAAA,MAChB;AAAA,QACE,SAAS,CAAC,WAAW,aAAa;AAAA,QAClC,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS,CAAC,aAAa,SAAS;AAAA,QAChC,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,MAEb;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,MACN,UAAU;AAAA,IAAA;AAAA,EACZ;AAEJ;AAyBO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,GAAG;AACL,GAAmC;AACjC,SACE;AAAA,IAACA;AAAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,UAAU,YAAY;AAAA,MACtB,uBAAuB;AAAA,MACvB,aAAW,WAAW;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,SAAA,CAAU,GAAG,SAAS;AAAA,MACnE,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA,UACC,oBAAC,WAAQ,MAAM,IAAI,OAAO,MAAM,WAAU,gBAAe,IAEzD;AAAA,QAED;AAAA,QACA,CAAC,WAAW;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGnB;"}
@@ -14,6 +14,8 @@ interface ChipOwnProps {
14
14
  size?: ChipSize;
15
15
  /** A MediCore icon before the label. While selected the check takes its place. */
16
16
  leadingIcon?: ReactNode;
17
+ /** Counter after the label ("Abiertas 5"): how many items the filter would show. */
18
+ count?: ReactNode;
17
19
  className?: string;
18
20
  }
19
21
  export interface ChipToggleProps extends Omit<Toggle.Props<string>, "className">, ChipOwnProps {
@@ -40,6 +42,7 @@ export type ChipProps = ChipToggleProps | ChipRemovableProps;
40
42
  * - It is a `<button aria-pressed>` (Base UI Toggle): `pressed` / `defaultPressed` /
41
43
  * `onPressedChange`, or `value` inside a `ChipGroup`.
42
44
  * - With `onRemove` it is a remove button for an applied filter instead.
45
+ * - `count` adds a counter pill at the end, brand-coloured while selected.
43
46
  */
44
47
  export declare function Chip(props: ChipProps): React.JSX.Element;
45
48
  export interface ChipGroupProps<Value extends string = string> extends Omit<ToggleGroup.Props<Value>, "className"> {
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { Button } from "@base-ui/react/button";
3
3
  import { Toggle } from "@base-ui/react/toggle";
4
4
  import { ToggleGroup } from "@base-ui/react/toggle-group";
@@ -10,15 +10,14 @@ import { useMediCoreLabels } from "../../provider/medicore-provider.js";
10
10
  const chipVariants = cva(
11
11
  [
12
12
  "group/chip inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap",
13
- // border-input, not border: a chip is a control and its edge needs 3:1 (1.4.11).
14
- "rounded-full border border-border-input bg-surface text-text-primary transition-colors",
13
+ "rounded-full border border-border bg-background text-text-primary shadow-xs transition-[color,background-color,border-color,box-shadow]",
15
14
  // 32 (24 small) drawn, 44 hit area.
16
15
  "focus-ring touch-target",
17
16
  "[&_svg]:shrink-0",
18
- "not-data-disabled:not-data-pressed:hover:bg-surface-raised",
19
- // Selected is not just a colour: check in front, 1.5px border, Medium label.
20
- "not-data-disabled:data-pressed:border-[1.5px] not-data-disabled:data-pressed:border-text-brand not-data-disabled:data-pressed:bg-brand-subtle not-data-disabled:data-pressed:text-text-brand",
21
- "data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:text-text-disabled"
17
+ "not-data-disabled:not-data-pressed:hover:border-border-strong not-data-disabled:not-data-pressed:hover:bg-surface",
18
+ // Selected is not just a colour: check in front, brand border, Medium label.
19
+ "not-data-disabled:data-pressed:border-brand not-data-disabled:data-pressed:bg-brand-subtle not-data-disabled:data-pressed:text-text-brand not-data-disabled:data-pressed:shadow-none",
20
+ "data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:bg-surface data-disabled:text-text-disabled data-disabled:shadow-none"
22
21
  ],
23
22
  {
24
23
  variants: {
@@ -32,10 +31,40 @@ const chipVariants = cva(
32
31
  }
33
32
  }
34
33
  );
34
+ const chipCountVariants = cva(
35
+ [
36
+ "inline-flex shrink-0 items-center justify-center rounded-full bg-surface-raised text-caption-default text-text-secondary tabular-nums transition-colors",
37
+ "group-data-pressed/chip:bg-brand group-data-pressed/chip:text-text-inverse",
38
+ "group-data-disabled/chip:bg-transparent group-data-disabled/chip:text-text-disabled"
39
+ ],
40
+ {
41
+ variants: {
42
+ size: {
43
+ default: "-me-1 h-5 min-w-5 px-1.5",
44
+ small: "-me-0.5 h-4 min-w-4 px-1"
45
+ }
46
+ },
47
+ defaultVariants: {
48
+ size: "default"
49
+ }
50
+ }
51
+ );
52
+ function ChipCount({
53
+ size,
54
+ count
55
+ }) {
56
+ if (count === void 0 || count === null) {
57
+ return null;
58
+ }
59
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
60
+ " ",
61
+ /* @__PURE__ */ jsx("span", { "data-slot": "chip-count", className: chipCountVariants({ size }), children: count })
62
+ ] });
63
+ }
35
64
  function Chip(props) {
36
65
  const labels = useMediCoreLabels();
37
66
  if (props.onRemove) {
38
- const { size: size2, leadingIcon: leadingIcon2, className: className2, children: children2, onRemove: onRemove2, ...rest2 } = props;
67
+ const { size: size2, leadingIcon: leadingIcon2, count: count2, className: className2, children: children2, onRemove: onRemove2, ...rest2 } = props;
39
68
  return /* @__PURE__ */ jsxs(
40
69
  Button,
41
70
  {
@@ -50,12 +79,13 @@ function Chip(props) {
50
79
  " "
51
80
  ] }),
52
81
  children2,
82
+ /* @__PURE__ */ jsx(ChipCount, { size: size2, count: count2 }),
53
83
  /* @__PURE__ */ jsx(XCloseIcon, {})
54
84
  ]
55
85
  }
56
86
  );
57
87
  }
58
- const { size, leadingIcon, className, children, onRemove, ...rest } = props;
88
+ const { size, leadingIcon, count, className, children, onRemove, ...rest } = props;
59
89
  return /* @__PURE__ */ jsxs(
60
90
  Toggle,
61
91
  {
@@ -65,7 +95,8 @@ function Chip(props) {
65
95
  children: [
66
96
  /* @__PURE__ */ jsx(CheckIcon, { className: "hidden group-data-pressed/chip:block" }),
67
97
  leadingIcon && /* @__PURE__ */ jsx("span", { className: "contents group-data-pressed/chip:hidden", children: leadingIcon }),
68
- children
98
+ children,
99
+ /* @__PURE__ */ jsx(ChipCount, { size, count })
69
100
  ]
70
101
  }
71
102
  );
@@ -1 +1 @@
1
- {"version":3,"file":"chip.js","sources":["../../../src/components/chip/chip.tsx"],"sourcesContent":["import { Button as BaseButton } from \"@base-ui/react/button\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { ToggleGroup } from \"@base-ui/react/toggle-group\";\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\n\nimport { CheckIcon, XCloseIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\n\nexport const chipVariants = cva(\n [\n \"group/chip inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap\",\n // border-input, not border: a chip is a control and its edge needs 3:1 (1.4.11).\n \"rounded-full border border-border-input bg-surface text-text-primary transition-colors\",\n // 32 (24 small) drawn, 44 hit area.\n \"focus-ring touch-target\",\n \"[&_svg]:shrink-0\",\n \"not-data-disabled:not-data-pressed:hover:bg-surface-raised\",\n // Selected is not just a colour: check in front, 1.5px border, Medium label.\n \"not-data-disabled:data-pressed:border-[1.5px] not-data-disabled:data-pressed:border-text-brand not-data-disabled:data-pressed:bg-brand-subtle not-data-disabled:data-pressed:text-text-brand\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:text-text-disabled\",\n ],\n {\n variants: {\n size: {\n default:\n \"h-8 gap-1.5 px-3 text-label-default data-pressed:text-label-strong [&_svg]:size-icon-sm\",\n small: \"h-6 gap-1 px-2 text-label-small [&_svg]:size-icon-xs\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n },\n);\n\nexport type ChipSize = \"default\" | \"small\";\n\ninterface ChipOwnProps {\n /**\n * `small` only for dense metadata, never as the main filter: at 24px it relies on\n * the invisible hit area to reach 44.\n */\n size?: ChipSize;\n /** A MediCore icon before the label. While selected the check takes its place. */\n leadingIcon?: ReactNode;\n className?: string;\n}\n\nexport interface ChipToggleProps\n extends Omit<Toggle.Props<string>, \"className\">,\n ChipOwnProps {\n onRemove?: undefined;\n}\n\nexport interface ChipRemovableProps\n extends Omit<BaseButton.Props, \"className\" | \"onClick\">,\n ChipOwnProps {\n /**\n * Turns the chip into a remove button for an applied filter: pressing anywhere on\n * it calls this. Its accessible name becomes \"Quitar <label>\" (`remove` label).\n * A removable chip is not a toggle.\n */\n onRemove: () => void;\n pressed?: undefined;\n defaultPressed?: undefined;\n onPressedChange?: undefined;\n}\n\nexport type ChipProps = ChipToggleProps | ChipRemovableProps;\n\n/**\n * Pressable filter (\"Dermatología\"). The filter row of lists and the admin.\n *\n * - **A Chip is not a Badge.** A Badge tells a state and can't be pressed\n * (\"Receta\", \"Cancelada\"); a Chip filters and is pressed. If it doesn't respond to\n * a tap, it's a Badge.\n * - It is a `<button aria-pressed>` (Base UI Toggle): `pressed` / `defaultPressed` /\n * `onPressedChange`, or `value` inside a `ChipGroup`.\n * - With `onRemove` it is a remove button for an applied filter instead.\n */\nexport function Chip(props: ChipProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n if (props.onRemove) {\n const { size, leadingIcon, className, children, onRemove, ...rest } = props;\n\n return (\n <BaseButton\n data-slot=\"chip\"\n className={cn(chipVariants({ size }), className)}\n onClick={onRemove}\n {...rest}\n >\n {leadingIcon}\n <span className=\"sr-only\">{labels.remove} </span>\n {children}\n <XCloseIcon />\n </BaseButton>\n );\n }\n\n const { size, leadingIcon, className, children, onRemove, ...rest } = props;\n\n return (\n <Toggle\n data-slot=\"chip\"\n className={cn(chipVariants({ size }), className)}\n {...rest}\n >\n <CheckIcon className=\"hidden group-data-pressed/chip:block\" />\n {leadingIcon && (\n <span className=\"contents group-data-pressed/chip:hidden\">\n {leadingIcon}\n </span>\n )}\n {children}\n </Toggle>\n );\n}\n\nexport interface ChipGroupProps<Value extends string = string>\n extends Omit<ToggleGroup.Props<Value>, \"className\"> {\n /**\n * `wrap` (default) breaks into lines; `scroll` keeps one line that scrolls\n * sideways — the usual filter row on mobile.\n */\n overflow?: \"wrap\" | \"scroll\";\n className?: string;\n}\n\n/**\n * A row of filter chips with shared state: `value` is the array of pressed chips.\n * Multi-select by default (`multiple={false}` for one at a time — but an exclusive\n * choice between 2–3 options is a `SegmentedControl`). One tab stop; ←/→ move\n * between chips. Name it with `aria-label` (\"Filtrar por especialidad\").\n */\nexport function ChipGroup<Value extends string>({\n overflow = \"wrap\",\n multiple = true,\n className,\n ...props\n}: ChipGroupProps<Value>): React.JSX.Element {\n return (\n <ToggleGroup\n data-slot=\"chip-group\"\n multiple={multiple}\n className={cn(\n \"flex gap-2\",\n overflow === \"wrap\"\n ? \"flex-wrap\"\n : // Room for the hit area and the focus ring inside the scroll box.\n \"-m-1.5 overflow-x-auto p-1.5 [scrollbar-width:none]\",\n className,\n )}\n {...props}\n />\n );\n}\n"],"names":["size","leadingIcon","className","children","onRemove","rest","BaseButton"],"mappings":";;;;;;;;;AAUO,MAAM,eAAe;AAAA,EAC1B;AAAA,IACE;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,SACE;AAAA,QACF,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AA+CO,SAAS,KAAK,OAAqC;AACxD,QAAM,SAAS,kBAAA;AAEf,MAAI,MAAM,UAAU;AAClB,UAAM,EAAE,MAAAA,OAAM,aAAAC,cAAa,WAAAC,YAAW,UAAAC,WAAU,UAAAC,WAAU,GAAGC,UAAS;AAEtE,WACE;AAAA,MAACC;AAAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW,GAAG,aAAa,EAAE,MAAAN,MAAAA,CAAM,GAAGE,UAAS;AAAA,QAC/C,SAASE;AAAAA,QACR,GAAGC;AAAAA,QAEH,UAAA;AAAA,UAAAJ;AAAAA,UACD,qBAAC,QAAA,EAAK,WAAU,WAAW,UAAA;AAAA,YAAA,OAAO;AAAA,YAAO;AAAA,UAAA,GAAC;AAAA,UACzCE;AAAAA,8BACA,YAAA,CAAA,CAAW;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGlB;AAEA,QAAM,EAAE,MAAM,aAAa,WAAW,UAAU,UAAU,GAAG,SAAS;AAEtE,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,aAAa,EAAE,KAAA,CAAM,GAAG,SAAS;AAAA,MAC9C,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA,oBAAC,WAAA,EAAU,WAAU,uCAAA,CAAuC;AAAA,QAC3D,eACC,oBAAC,QAAA,EAAK,WAAU,2CACb,UAAA,aACH;AAAA,QAED;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGP;AAkBO,SAAS,UAAgC;AAAA,EAC9C,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAA6C;AAC3C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA,aAAa,SACT;AAAA;AAAA,UAEA;AAAA;AAAA,QACJ;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
1
+ {"version":3,"file":"chip.js","sources":["../../../src/components/chip/chip.tsx"],"sourcesContent":["import { Button as BaseButton } from \"@base-ui/react/button\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { ToggleGroup } from \"@base-ui/react/toggle-group\";\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\n\nimport { CheckIcon, XCloseIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { useMediCoreLabels } from \"../../provider\";\n\nexport const chipVariants = cva(\n [\n \"group/chip inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap\",\n \"rounded-full border border-border bg-background text-text-primary shadow-xs transition-[color,background-color,border-color,box-shadow]\",\n // 32 (24 small) drawn, 44 hit area.\n \"focus-ring touch-target\",\n \"[&_svg]:shrink-0\",\n \"not-data-disabled:not-data-pressed:hover:border-border-strong not-data-disabled:not-data-pressed:hover:bg-surface\",\n // Selected is not just a colour: check in front, brand border, Medium label.\n \"not-data-disabled:data-pressed:border-brand not-data-disabled:data-pressed:bg-brand-subtle not-data-disabled:data-pressed:text-text-brand not-data-disabled:data-pressed:shadow-none\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:bg-surface data-disabled:text-text-disabled data-disabled:shadow-none\",\n ],\n {\n variants: {\n size: {\n default:\n \"h-8 gap-1.5 px-3 text-label-default data-pressed:text-label-strong [&_svg]:size-icon-sm\",\n small: \"h-6 gap-1 px-2 text-label-small [&_svg]:size-icon-xs\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n },\n);\n\nconst chipCountVariants = cva(\n [\n \"inline-flex shrink-0 items-center justify-center rounded-full bg-surface-raised text-caption-default text-text-secondary tabular-nums transition-colors\",\n \"group-data-pressed/chip:bg-brand group-data-pressed/chip:text-text-inverse\",\n \"group-data-disabled/chip:bg-transparent group-data-disabled/chip:text-text-disabled\",\n ],\n {\n variants: {\n size: {\n default: \"-me-1 h-5 min-w-5 px-1.5\",\n small: \"-me-0.5 h-4 min-w-4 px-1\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n },\n);\n\nexport type ChipSize = \"default\" | \"small\";\n\ninterface ChipOwnProps {\n /**\n * `small` only for dense metadata, never as the main filter: at 24px it relies on\n * the invisible hit area to reach 44.\n */\n size?: ChipSize;\n /** A MediCore icon before the label. While selected the check takes its place. */\n leadingIcon?: ReactNode;\n /** Counter after the label (\"Abiertas 5\"): how many items the filter would show. */\n count?: ReactNode;\n className?: string;\n}\n\nexport interface ChipToggleProps\n extends Omit<Toggle.Props<string>, \"className\">,\n ChipOwnProps {\n onRemove?: undefined;\n}\n\nexport interface ChipRemovableProps\n extends Omit<BaseButton.Props, \"className\" | \"onClick\">,\n ChipOwnProps {\n /**\n * Turns the chip into a remove button for an applied filter: pressing anywhere on\n * it calls this. Its accessible name becomes \"Quitar <label>\" (`remove` label).\n * A removable chip is not a toggle.\n */\n onRemove: () => void;\n pressed?: undefined;\n defaultPressed?: undefined;\n onPressedChange?: undefined;\n}\n\nexport type ChipProps = ChipToggleProps | ChipRemovableProps;\n\nfunction ChipCount({\n size,\n count,\n}: {\n size: ChipSize | undefined;\n count: ReactNode;\n}): React.JSX.Element | null {\n if (count === undefined || count === null) {\n return null;\n }\n return (\n <>\n {\" \"}\n <span data-slot=\"chip-count\" className={chipCountVariants({ size })}>\n {count}\n </span>\n </>\n );\n}\n\n/**\n * Pressable filter (\"Dermatología\"). The filter row of lists and the admin.\n *\n * - **A Chip is not a Badge.** A Badge tells a state and can't be pressed\n * (\"Receta\", \"Cancelada\"); a Chip filters and is pressed. If it doesn't respond to\n * a tap, it's a Badge.\n * - It is a `<button aria-pressed>` (Base UI Toggle): `pressed` / `defaultPressed` /\n * `onPressedChange`, or `value` inside a `ChipGroup`.\n * - With `onRemove` it is a remove button for an applied filter instead.\n * - `count` adds a counter pill at the end, brand-coloured while selected.\n */\nexport function Chip(props: ChipProps): React.JSX.Element {\n const labels = useMediCoreLabels();\n\n if (props.onRemove) {\n const { size, leadingIcon, count, className, children, onRemove, ...rest } =\n props;\n\n return (\n <BaseButton\n data-slot=\"chip\"\n className={cn(chipVariants({ size }), className)}\n onClick={onRemove}\n {...rest}\n >\n {leadingIcon}\n <span className=\"sr-only\">{labels.remove} </span>\n {children}\n <ChipCount size={size} count={count} />\n <XCloseIcon />\n </BaseButton>\n );\n }\n\n const { size, leadingIcon, count, className, children, onRemove, ...rest } =\n props;\n\n return (\n <Toggle\n data-slot=\"chip\"\n className={cn(chipVariants({ size }), className)}\n {...rest}\n >\n <CheckIcon className=\"hidden group-data-pressed/chip:block\" />\n {leadingIcon && (\n <span className=\"contents group-data-pressed/chip:hidden\">\n {leadingIcon}\n </span>\n )}\n {children}\n <ChipCount size={size} count={count} />\n </Toggle>\n );\n}\n\nexport interface ChipGroupProps<Value extends string = string>\n extends Omit<ToggleGroup.Props<Value>, \"className\"> {\n /**\n * `wrap` (default) breaks into lines; `scroll` keeps one line that scrolls\n * sideways — the usual filter row on mobile.\n */\n overflow?: \"wrap\" | \"scroll\";\n className?: string;\n}\n\n/**\n * A row of filter chips with shared state: `value` is the array of pressed chips.\n * Multi-select by default (`multiple={false}` for one at a time — but an exclusive\n * choice between 2–3 options is a `SegmentedControl`). One tab stop; ←/→ move\n * between chips. Name it with `aria-label` (\"Filtrar por especialidad\").\n */\nexport function ChipGroup<Value extends string>({\n overflow = \"wrap\",\n multiple = true,\n className,\n ...props\n}: ChipGroupProps<Value>): React.JSX.Element {\n return (\n <ToggleGroup\n data-slot=\"chip-group\"\n multiple={multiple}\n className={cn(\n \"flex gap-2\",\n overflow === \"wrap\"\n ? \"flex-wrap\"\n : // Room for the hit area and the focus ring inside the scroll box.\n \"-m-1.5 overflow-x-auto p-1.5 [scrollbar-width:none]\",\n className,\n )}\n {...props}\n />\n );\n}\n"],"names":["size","leadingIcon","count","className","children","onRemove","rest","BaseButton"],"mappings":";;;;;;;;;AAUO,MAAM,eAAe;AAAA,EAC1B;AAAA,IACE;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,SACE;AAAA,QACF,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAEA,MAAM,oBAAoB;AAAA,EACxB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAuCA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AACF,GAG6B;AAC3B,MAAI,UAAU,UAAa,UAAU,MAAM;AACzC,WAAO;AAAA,EACT;AACA,SACE,qBAAA,UAAA,EACG,UAAA;AAAA,IAAA;AAAA,IACD,oBAAC,QAAA,EAAK,aAAU,cAAa,WAAW,kBAAkB,EAAE,KAAA,CAAM,GAC/D,UAAA,MAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAaO,SAAS,KAAK,OAAqC;AACxD,QAAM,SAAS,kBAAA;AAEf,MAAI,MAAM,UAAU;AAClB,UAAM,EAAE,MAAAA,OAAM,aAAAC,cAAa,OAAAC,QAAO,WAAAC,YAAW,UAAAC,WAAU,UAAAC,WAAU,GAAGC,UAClE;AAEF,WACE;AAAA,MAACC;AAAAA,MAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW,GAAG,aAAa,EAAE,MAAAP,MAAAA,CAAM,GAAGG,UAAS;AAAA,QAC/C,SAASE;AAAAA,QACR,GAAGC;AAAAA,QAEH,UAAA;AAAA,UAAAL;AAAAA,UACD,qBAAC,QAAA,EAAK,WAAU,WAAW,UAAA;AAAA,YAAA,OAAO;AAAA,YAAO;AAAA,UAAA,GAAC;AAAA,UACzCG;AAAAA,UACD,oBAAC,WAAA,EAAU,MAAMJ,OAAM,OAAOE,QAAO;AAAA,8BACpC,YAAA,CAAA,CAAW;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGlB;AAEA,QAAM,EAAE,MAAM,aAAa,OAAO,WAAW,UAAU,UAAU,GAAG,KAAA,IAClE;AAEF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,aAAa,EAAE,KAAA,CAAM,GAAG,SAAS;AAAA,MAC9C,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA,oBAAC,WAAA,EAAU,WAAU,uCAAA,CAAuC;AAAA,QAC3D,eACC,oBAAC,QAAA,EAAK,WAAU,2CACb,UAAA,aACH;AAAA,QAED;AAAA,QACD,oBAAC,WAAA,EAAU,MAAY,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG3C;AAkBO,SAAS,UAAgC;AAAA,EAC9C,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAA6C;AAC3C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA,aAAa,SACT;AAAA;AAAA,UAEA;AAAA;AAAA,QACJ;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
@@ -0,0 +1,49 @@
1
+ import { useRender } from "@base-ui/react/use-render";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+ export type DetailHeaderHeadingLevel = 1 | 2;
4
+ export interface DetailHeaderProps extends Omit<ComponentProps<"header">, "title"> {
5
+ /** The record's name: "Laura Gómez", "Clínica Dental Sonrisas". */
6
+ title: ReactNode;
7
+ /** The scope of what is shown, next to the title in text-secondary: "Septiembre 2026". */
8
+ subtitle?: ReactNode;
9
+ /** Its state, a `Badge` right after the title. */
10
+ badge?: ReactNode;
11
+ /** The way back to the list: a `DetailHeaderBack`. */
12
+ back?: ReactNode;
13
+ /** An `Avatar` or `IconContainer` before the title. */
14
+ leading?: ReactNode;
15
+ /** One line of small print under the title: "Cerrada el 2 de octubre por Víctor". */
16
+ meta?: ReactNode;
17
+ /** The record's `Button`s, primary last. They wrap under the title on narrow screens. */
18
+ actions?: ReactNode;
19
+ /**
20
+ * Level of the title. Default 1: the detail page's `<h1>`. Use 2 when the page
21
+ * already has its `<h1>` (a `MobileHeader` with the section's name, for example).
22
+ */
23
+ headingLevel?: DetailHeaderHeadingLevel;
24
+ /** The figures row: `StatCard`s and `FactCard`s, laid out by the screen. */
25
+ children?: ReactNode;
26
+ }
27
+ /**
28
+ * The header of a record's page (a professional's settlement, a patient, a clinic):
29
+ * the way back, who or what it is, its state, its actions and its key figures, in one
30
+ * raised card so the rest of the page reads as the record's content.
31
+ *
32
+ * - The title looks like Heading/h2 in text-brand, like `PageHeader`: it is the
33
+ * identity of the screen. It IS the page heading (`headingLevel` 1) unless the
34
+ * page already has one.
35
+ * - `actions` sit to the right of the title from `lg` up and wrap under it below.
36
+ * Primary last, `outline` for the rest.
37
+ * - The figures go in `children`: `StatCard`s for amounts and counts, a `FactCard`
38
+ * for the facts that decide what can be done (invoicing data, missing consent).
39
+ * The screen owns their grid.
40
+ */
41
+ export declare function DetailHeader({ title, subtitle, badge, back, leading, meta, actions, headingLevel, className, children, ...props }: DetailHeaderProps): React.JSX.Element;
42
+ export interface DetailHeaderBackProps extends useRender.ComponentProps<"a"> {
43
+ }
44
+ /**
45
+ * The link back to the list: "Liquidaciones". A real link (`href`, or
46
+ * `render={<Link to="…" />}` for the router) drawn as a ghost small `Button` with
47
+ * an arrow, aligned with the title's left edge.
48
+ */
49
+ export declare function DetailHeaderBack({ render, ref, className, children, ...props }: DetailHeaderBackProps): React.JSX.Element;
@@ -0,0 +1,92 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { ArrowNarrowLeftIcon } from "../../icons/generated/arrow-narrow-left.js";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { buttonVariants } from "../button/button.js";
6
+ function DetailHeader({
7
+ title,
8
+ subtitle,
9
+ badge,
10
+ back,
11
+ leading,
12
+ meta,
13
+ actions,
14
+ headingLevel = 1,
15
+ className,
16
+ children,
17
+ ...props
18
+ }) {
19
+ const Heading = `h${headingLevel}`;
20
+ return /* @__PURE__ */ jsxs(
21
+ "header",
22
+ {
23
+ "data-slot": "detail-header",
24
+ className: cn(
25
+ "flex flex-col gap-5 rounded-2xl bg-background p-5 shadow-s lg:p-6",
26
+ className
27
+ ),
28
+ ...props,
29
+ children: [
30
+ back,
31
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between lg:gap-6", children: [
32
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-4", children: [
33
+ leading,
34
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
35
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [
36
+ /* @__PURE__ */ jsxs(Heading, { className: "min-w-0 text-heading-h2 text-text-brand", children: [
37
+ title,
38
+ subtitle && /* @__PURE__ */ jsxs("span", { className: "text-text-secondary", children: [
39
+ " · ",
40
+ subtitle
41
+ ] })
42
+ ] }),
43
+ badge
44
+ ] }),
45
+ meta && /* @__PURE__ */ jsx("p", { className: "text-body-small text-text-secondary", children: meta })
46
+ ] })
47
+ ] }),
48
+ actions && /* @__PURE__ */ jsx(
49
+ "div",
50
+ {
51
+ "data-slot": "detail-header-actions",
52
+ className: "flex flex-wrap items-center gap-3 lg:shrink-0 lg:justify-end",
53
+ children: actions
54
+ }
55
+ )
56
+ ] }),
57
+ children && /* @__PURE__ */ jsx("div", { "data-slot": "detail-header-figures", className: "flex flex-col gap-3", children })
58
+ ]
59
+ }
60
+ );
61
+ }
62
+ function DetailHeaderBack({
63
+ render,
64
+ ref,
65
+ className,
66
+ children,
67
+ ...props
68
+ }) {
69
+ return useRender({
70
+ render,
71
+ ref,
72
+ defaultTagName: "a",
73
+ props: {
74
+ "data-slot": "detail-header-back",
75
+ className: cn(
76
+ buttonVariants({ variant: "ghost", size: "small" }),
77
+ "-ml-3 self-start",
78
+ className
79
+ ),
80
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
81
+ /* @__PURE__ */ jsx(ArrowNarrowLeftIcon, {}),
82
+ children
83
+ ] }),
84
+ ...props
85
+ }
86
+ });
87
+ }
88
+ export {
89
+ DetailHeader,
90
+ DetailHeaderBack
91
+ };
92
+ //# sourceMappingURL=detail-header.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-header.js","sources":["../../../src/components/detail-header/detail-header.tsx"],"sourcesContent":["import { useRender } from \"@base-ui/react/use-render\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { ArrowNarrowLeftIcon } from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\nimport { buttonVariants } from \"../button\";\n\nexport type DetailHeaderHeadingLevel = 1 | 2;\n\nexport interface DetailHeaderProps\n extends Omit<ComponentProps<\"header\">, \"title\"> {\n /** The record's name: \"Laura Gómez\", \"Clínica Dental Sonrisas\". */\n title: ReactNode;\n /** The scope of what is shown, next to the title in text-secondary: \"Septiembre 2026\". */\n subtitle?: ReactNode;\n /** Its state, a `Badge` right after the title. */\n badge?: ReactNode;\n /** The way back to the list: a `DetailHeaderBack`. */\n back?: ReactNode;\n /** An `Avatar` or `IconContainer` before the title. */\n leading?: ReactNode;\n /** One line of small print under the title: \"Cerrada el 2 de octubre por Víctor\". */\n meta?: ReactNode;\n /** The record's `Button`s, primary last. They wrap under the title on narrow screens. */\n actions?: ReactNode;\n /**\n * Level of the title. Default 1: the detail page's `<h1>`. Use 2 when the page\n * already has its `<h1>` (a `MobileHeader` with the section's name, for example).\n */\n headingLevel?: DetailHeaderHeadingLevel;\n /** The figures row: `StatCard`s and `FactCard`s, laid out by the screen. */\n children?: ReactNode;\n}\n\n/**\n * The header of a record's page (a professional's settlement, a patient, a clinic):\n * the way back, who or what it is, its state, its actions and its key figures, in one\n * raised card so the rest of the page reads as the record's content.\n *\n * - The title looks like Heading/h2 in text-brand, like `PageHeader`: it is the\n * identity of the screen. It IS the page heading (`headingLevel` 1) unless the\n * page already has one.\n * - `actions` sit to the right of the title from `lg` up and wrap under it below.\n * Primary last, `outline` for the rest.\n * - The figures go in `children`: `StatCard`s for amounts and counts, a `FactCard`\n * for the facts that decide what can be done (invoicing data, missing consent).\n * The screen owns their grid.\n */\nexport function DetailHeader({\n title,\n subtitle,\n badge,\n back,\n leading,\n meta,\n actions,\n headingLevel = 1,\n className,\n children,\n ...props\n}: DetailHeaderProps): React.JSX.Element {\n const Heading = `h${headingLevel}` as const;\n\n return (\n <header\n data-slot=\"detail-header\"\n className={cn(\n \"flex flex-col gap-5 rounded-2xl bg-background p-5 shadow-s lg:p-6\",\n className,\n )}\n {...props}\n >\n {back}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between lg:gap-6\">\n <div className=\"flex min-w-0 items-center gap-4\">\n {leading}\n <div className=\"flex min-w-0 flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <Heading className=\"min-w-0 text-heading-h2 text-text-brand\">\n {title}\n {subtitle && (\n <span className=\"text-text-secondary\"> · {subtitle}</span>\n )}\n </Heading>\n {badge}\n </div>\n {meta && (\n <p className=\"text-body-small text-text-secondary\">{meta}</p>\n )}\n </div>\n </div>\n {actions && (\n <div\n data-slot=\"detail-header-actions\"\n className=\"flex flex-wrap items-center gap-3 lg:shrink-0 lg:justify-end\"\n >\n {actions}\n </div>\n )}\n </div>\n {children && (\n <div data-slot=\"detail-header-figures\" className=\"flex flex-col gap-3\">\n {children}\n </div>\n )}\n </header>\n );\n}\n\nexport interface DetailHeaderBackProps extends useRender.ComponentProps<\"a\"> {}\n\n/**\n * The link back to the list: \"Liquidaciones\". A real link (`href`, or\n * `render={<Link to=\"…\" />}` for the router) drawn as a ghost small `Button` with\n * an arrow, aligned with the title's left edge.\n */\nexport function DetailHeaderBack({\n render,\n ref,\n className,\n children,\n ...props\n}: DetailHeaderBackProps): React.JSX.Element {\n return useRender({\n render,\n ref,\n defaultTagName: \"a\",\n props: {\n \"data-slot\": \"detail-header-back\",\n className: cn(\n buttonVariants({ variant: \"ghost\", size: \"small\" }),\n \"-ml-3 self-start\",\n className,\n ),\n children: (\n <>\n <ArrowNarrowLeftIcon />\n {children}\n </>\n ),\n ...props,\n },\n });\n}\n"],"names":[],"mappings":";;;;;AAgDO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyC;AACvC,QAAM,UAAU,IAAI,YAAY;AAEhC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA;AAAA,QACD,qBAAC,OAAA,EAAI,WAAU,8EACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,mCACZ,UAAA;AAAA,YAAA;AAAA,YACD,qBAAC,OAAA,EAAI,WAAU,+BACb,UAAA;AAAA,cAAA,qBAAC,OAAA,EAAI,WAAU,+CACb,UAAA;AAAA,gBAAA,qBAAC,SAAA,EAAQ,WAAU,2CAChB,UAAA;AAAA,kBAAA;AAAA,kBACA,YACC,qBAAC,QAAA,EAAK,WAAU,uBAAsB,UAAA;AAAA,oBAAA;AAAA,oBAAI;AAAA,kBAAA,EAAA,CAAS;AAAA,gBAAA,GAEvD;AAAA,gBACC;AAAA,cAAA,GACH;AAAA,cACC,QACC,oBAAC,KAAA,EAAE,WAAU,uCAAuC,UAAA,KAAA,CAAK;AAAA,YAAA,EAAA,CAE7D;AAAA,UAAA,GACF;AAAA,UACC,WACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,cAET,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,GAEJ;AAAA,QACC,YACC,oBAAC,OAAA,EAAI,aAAU,yBAAwB,WAAU,uBAC9C,SAAA,CACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAIR;AASO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA6C;AAC3C,SAAO,UAAU;AAAA,IACf;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB,OAAO;AAAA,MACL,aAAa;AAAA,MACb,WAAW;AAAA,QACT,eAAe,EAAE,SAAS,SAAS,MAAM,SAAS;AAAA,QAClD;AAAA,QACA;AAAA,MAAA;AAAA,MAEF,UACE,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,qBAAA,EAAoB;AAAA,QACpB;AAAA,MAAA,GACH;AAAA,MAEF,GAAG;AAAA,IAAA;AAAA,EACL,CACD;AACH;"}
@@ -0,0 +1 @@
1
+ export { DetailHeader, DetailHeaderBack, type DetailHeaderBackProps, type DetailHeaderHeadingLevel, type DetailHeaderProps, } from "./detail-header";
@@ -0,0 +1,34 @@
1
+ import { type VariantProps } from "class-variance-authority";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+ export declare const factCardVariants: (props?: ({
4
+ tone?: "error" | "neutral" | "success" | "warning" | null | undefined;
5
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
+ export type FactCardTone = "neutral" | "success" | "warning" | "error";
7
+ export interface FactCardProps extends Omit<ComponentProps<"div">, "title">, VariantProps<typeof factCardVariants> {
8
+ /**
9
+ * The fact, in a few words: "Datos de facturación completos", "Falta el NIF".
10
+ * On `error`, say what it blocks: "Falta el NIF: no se puede cerrar el mes".
11
+ */
12
+ title: ReactNode;
13
+ /** The detail: "Laura Gómez Ruiz · NIF 12345678Z · IBAN ···· 1332". */
14
+ description?: ReactNode;
15
+ /** One way to act on it, a `Link` or a small `Button`. */
16
+ action?: ReactNode;
17
+ /** Replaces the tone's icon. */
18
+ icon?: ReactNode;
19
+ /**
20
+ * `success` when the fact is settled, `warning` when it limits something,
21
+ * `error` when it blocks the main action, `neutral` for plain information.
22
+ */
23
+ tone?: FactCardTone;
24
+ }
25
+ /**
26
+ * A fact about the record that decides what can be done with it, next to its figures
27
+ * in a `DetailHeader`: the invoicing data before a close, a missing consent, a pending
28
+ * document. It stays as long as the fact does.
29
+ *
30
+ * Unlike `Banner`, it is a card among cards, not a notice across the page, and it is
31
+ * never dismissible. Text in text-primary on every tone; the icon carries the tone,
32
+ * so the meaning is in the words. `error` is announced with `role="status"`.
33
+ */
34
+ export declare function FactCard({ title, description, action, icon, tone, className, ...props }: FactCardProps): React.JSX.Element;
@@ -0,0 +1,86 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { AlertCircleIcon } from "../../icons/generated/alert-circle.js";
4
+ import { AlertTriangleIcon } from "../../icons/generated/alert-triangle.js";
5
+ import { CheckCircleIcon } from "../../icons/generated/check-circle.js";
6
+ import { InfoCircleIcon } from "../../icons/generated/info-circle.js";
7
+ import { cn } from "../../lib/cn.js";
8
+ const factCardVariants = cva(
9
+ "flex min-w-0 items-start gap-3 rounded-xl border p-4",
10
+ {
11
+ variants: {
12
+ tone: {
13
+ neutral: "border-border-subtle bg-background",
14
+ success: "border-success bg-success-subtle",
15
+ warning: "border-warning bg-warning-subtle",
16
+ error: "border-error bg-error-subtle"
17
+ }
18
+ },
19
+ defaultVariants: {
20
+ tone: "neutral"
21
+ }
22
+ }
23
+ );
24
+ const iconClassName = {
25
+ neutral: "text-text-secondary",
26
+ success: "text-text-success",
27
+ warning: "text-text-warning",
28
+ error: "text-text-error"
29
+ };
30
+ const defaultIcon = {
31
+ neutral: /* @__PURE__ */ jsx(InfoCircleIcon, {}),
32
+ success: /* @__PURE__ */ jsx(CheckCircleIcon, {}),
33
+ warning: /* @__PURE__ */ jsx(AlertTriangleIcon, {}),
34
+ error: /* @__PURE__ */ jsx(AlertCircleIcon, {})
35
+ };
36
+ function FactCard({
37
+ title,
38
+ description,
39
+ action,
40
+ icon,
41
+ tone = "neutral",
42
+ className,
43
+ ...props
44
+ }) {
45
+ return /* @__PURE__ */ jsxs(
46
+ "div",
47
+ {
48
+ "data-slot": "fact-card",
49
+ role: tone === "error" ? "status" : void 0,
50
+ className: cn(factCardVariants({ tone }), className),
51
+ ...props,
52
+ children: [
53
+ /* @__PURE__ */ jsx(
54
+ "span",
55
+ {
56
+ "aria-hidden": "true",
57
+ className: cn(
58
+ "mt-0.5 flex shrink-0 [&_svg]:size-icon-md",
59
+ iconClassName[tone]
60
+ ),
61
+ children: icon ?? defaultIcon[tone]
62
+ }
63
+ ),
64
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
65
+ /* @__PURE__ */ jsx("p", { className: "text-label-strong text-text-primary", children: title }),
66
+ description && /* @__PURE__ */ jsx(
67
+ "p",
68
+ {
69
+ className: cn(
70
+ "text-body-small",
71
+ tone === "neutral" ? "text-text-secondary" : "text-text-primary"
72
+ ),
73
+ children: description
74
+ }
75
+ ),
76
+ action && /* @__PURE__ */ jsx("div", { className: "pt-1", children: action })
77
+ ] })
78
+ ]
79
+ }
80
+ );
81
+ }
82
+ export {
83
+ FactCard,
84
+ factCardVariants
85
+ };
86
+ //# sourceMappingURL=fact-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact-card.js","sources":["../../../src/components/fact-card/fact-card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport {\n AlertCircleIcon,\n AlertTriangleIcon,\n CheckCircleIcon,\n InfoCircleIcon,\n} from \"../../icons\";\nimport { cn } from \"../../lib/cn\";\n\nexport const factCardVariants = cva(\n \"flex min-w-0 items-start gap-3 rounded-xl border p-4\",\n {\n variants: {\n tone: {\n neutral: \"border-border-subtle bg-background\",\n success: \"border-success bg-success-subtle\",\n warning: \"border-warning bg-warning-subtle\",\n error: \"border-error bg-error-subtle\",\n },\n },\n defaultVariants: {\n tone: \"neutral\",\n },\n },\n);\n\nconst iconClassName: Record<FactCardTone, string> = {\n neutral: \"text-text-secondary\",\n success: \"text-text-success\",\n warning: \"text-text-warning\",\n error: \"text-text-error\",\n};\n\nconst defaultIcon: Record<FactCardTone, ReactNode> = {\n neutral: <InfoCircleIcon />,\n success: <CheckCircleIcon />,\n warning: <AlertTriangleIcon />,\n error: <AlertCircleIcon />,\n};\n\nexport type FactCardTone = \"neutral\" | \"success\" | \"warning\" | \"error\";\n\nexport interface FactCardProps\n extends Omit<ComponentProps<\"div\">, \"title\">,\n VariantProps<typeof factCardVariants> {\n /**\n * The fact, in a few words: \"Datos de facturación completos\", \"Falta el NIF\".\n * On `error`, say what it blocks: \"Falta el NIF: no se puede cerrar el mes\".\n */\n title: ReactNode;\n /** The detail: \"Laura Gómez Ruiz · NIF 12345678Z · IBAN ···· 1332\". */\n description?: ReactNode;\n /** One way to act on it, a `Link` or a small `Button`. */\n action?: ReactNode;\n /** Replaces the tone's icon. */\n icon?: ReactNode;\n /**\n * `success` when the fact is settled, `warning` when it limits something,\n * `error` when it blocks the main action, `neutral` for plain information.\n */\n tone?: FactCardTone;\n}\n\n/**\n * A fact about the record that decides what can be done with it, next to its figures\n * in a `DetailHeader`: the invoicing data before a close, a missing consent, a pending\n * document. It stays as long as the fact does.\n *\n * Unlike `Banner`, it is a card among cards, not a notice across the page, and it is\n * never dismissible. Text in text-primary on every tone; the icon carries the tone,\n * so the meaning is in the words. `error` is announced with `role=\"status\"`.\n */\nexport function FactCard({\n title,\n description,\n action,\n icon,\n tone = \"neutral\",\n className,\n ...props\n}: FactCardProps): React.JSX.Element {\n return (\n <div\n data-slot=\"fact-card\"\n role={tone === \"error\" ? \"status\" : undefined}\n className={cn(factCardVariants({ tone }), className)}\n {...props}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n \"mt-0.5 flex shrink-0 [&_svg]:size-icon-md\",\n iconClassName[tone],\n )}\n >\n {icon ?? defaultIcon[tone]}\n </span>\n <div className=\"flex min-w-0 flex-col gap-1\">\n <p className=\"text-label-strong text-text-primary\">{title}</p>\n {description && (\n <p\n className={cn(\n \"text-body-small\",\n tone === \"neutral\" ? \"text-text-secondary\" : \"text-text-primary\",\n )}\n >\n {description}\n </p>\n )}\n {action && <div className=\"pt-1\">{action}</div>}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;AAWO,MAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAEA,MAAM,gBAA8C;AAAA,EAClD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEA,MAAM,cAA+C;AAAA,EACnD,6BAAU,gBAAA,EAAe;AAAA,EACzB,6BAAU,iBAAA,EAAgB;AAAA,EAC1B,6BAAU,mBAAA,EAAkB;AAAA,EAC5B,2BAAQ,iBAAA,CAAA,CAAgB;AAC1B;AAkCO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAqC;AACnC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAM,SAAS,UAAU,WAAW;AAAA,MACpC,WAAW,GAAG,iBAAiB,EAAE,KAAA,CAAM,GAAG,SAAS;AAAA,MAClD,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,cAAc,IAAI;AAAA,YAAA;AAAA,YAGnB,UAAA,QAAQ,YAAY,IAAI;AAAA,UAAA;AAAA,QAAA;AAAA,QAE3B,qBAAC,OAAA,EAAI,WAAU,+BACb,UAAA;AAAA,UAAA,oBAAC,KAAA,EAAE,WAAU,uCAAuC,UAAA,OAAM;AAAA,UACzD,eACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,SAAS,YAAY,wBAAwB;AAAA,cAAA;AAAA,cAG9C,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGJ,UAAU,oBAAC,OAAA,EAAI,WAAU,QAAQ,UAAA,OAAA,CAAO;AAAA,QAAA,EAAA,CAC3C;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1 @@
1
+ export { FactCard, type FactCardProps, type FactCardTone, factCardVariants, } from "./fact-card";
@@ -1,5 +1,5 @@
1
1
  /** Border, radius, fill and text colour of the box. */
2
- export declare const controlBox = "w-full min-w-0 rounded-md border border-border-input bg-background text-text-primary ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]";
2
+ export declare const controlBox = "w-full min-w-0 rounded-md border border-border bg-background text-text-primary shadow-xs ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]";
3
3
  /** One-line content: 12px side padding, 8px between icon and text, Label/Default (14). */
4
4
  export declare const controlRow = "flex items-center gap-2 px-3 text-label-default";
5
5
  /** control-md (40) and control-sm (36): the same tokens as Button, so they line up. */
@@ -1,4 +1,4 @@
1
- const controlBox = "w-full min-w-0 rounded-md border border-border-input bg-background text-text-primary ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]";
1
+ const controlBox = "w-full min-w-0 rounded-md border border-border bg-background text-text-primary shadow-xs ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]";
2
2
  const controlRow = "flex items-center gap-2 px-3 text-label-default";
3
3
  const controlHeight = {
4
4
  default: "h-control-md",
@@ -7,17 +7,17 @@ const controlHeight = {
7
7
  const wrappedControlStates = [
8
8
  "has-[[aria-invalid=true]]:border-error has-[[aria-invalid=true]]:ring-error",
9
9
  "has-[:focus:not(button)]:border-border-focus has-[:focus:not(button)]:ring-border-focus",
10
- "has-[:is(input,select):disabled]:cursor-not-allowed has-[:is(input,select):disabled]:border-border has-[:is(input,select):disabled]:bg-surface has-[:is(input,select):disabled]:text-text-disabled"
10
+ "has-[:is(input,select):disabled]:cursor-not-allowed has-[:is(input,select):disabled]:border-border-subtle has-[:is(input,select):disabled]:bg-surface has-[:is(input,select):disabled]:text-text-disabled has-[:is(input,select):disabled]:shadow-none"
11
11
  ].join(" ");
12
12
  const selfControlStates = [
13
13
  "outline-none focus:border-border-focus focus:ring-border-focus",
14
14
  "aria-invalid:not-focus:border-error aria-invalid:not-focus:ring-error",
15
- "disabled:cursor-not-allowed disabled:border-border disabled:bg-surface disabled:text-text-disabled"
15
+ "disabled:cursor-not-allowed disabled:border-border-subtle disabled:bg-surface disabled:text-text-disabled disabled:shadow-none"
16
16
  ].join(" ");
17
17
  const triggerControlStates = [
18
18
  "outline-none focus:border-border-focus focus:ring-border-focus data-popup-open:border-border-focus data-popup-open:ring-border-focus",
19
19
  "aria-invalid:not-focus:not-data-popup-open:border-error aria-invalid:not-focus:not-data-popup-open:ring-error",
20
- "data-disabled:cursor-not-allowed data-disabled:border-border data-disabled:bg-surface data-disabled:text-text-disabled"
20
+ "data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:bg-surface data-disabled:text-text-disabled data-disabled:shadow-none"
21
21
  ].join(" ");
22
22
  const controlPlaceholder = "placeholder:text-text-secondary disabled:placeholder:text-text-disabled";
23
23
  const controlIcon = "flex shrink-0 text-text-secondary [&_svg]:size-icon-sm group-has-[:is(input,select):disabled]/control:text-text-disabled group-data-disabled/control:text-text-disabled";
@@ -1 +1 @@
1
- {"version":3,"file":"control-styles.js","sources":["../../../src/components/input/control-styles.ts"],"sourcesContent":["/*\n * The box shared by Input, Textarea, Select, NativeSelect and PhoneInput. Figma draws\n * them as the same object on purpose: same radius, padding, border and states, so a\n * field and a dropdown in the same row only differ by the chevron.\n *\n * Focus and Error are a 2px border (border-thick), not just a colour change. The\n * second pixel is an inset ring, so the content never moves when the state changes.\n * Focus wins over Error: the message under the field keeps saying what is wrong.\n */\n\n/** Border, radius, fill and text colour of the box. */\nexport const controlBox =\n \"w-full min-w-0 rounded-md border border-border-input bg-background text-text-primary ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]\";\n\n/** One-line content: 12px side padding, 8px between icon and text, Label/Default (14). */\nexport const controlRow = \"flex items-center gap-2 px-3 text-label-default\";\n\n/** control-md (40) and control-sm (36): the same tokens as Button, so they line up. */\nexport const controlHeight = {\n default: \"h-control-md\",\n small: \"h-control-sm\",\n} as const;\n\nexport type ControlSize = keyof typeof controlHeight;\n\n/**\n * States for a box that WRAPS the real control (Input, NativeSelect, the phone\n * prefix). Read from the control inside: `aria-invalid` (set by `invalid` or by\n * Field), `:focus` and `:disabled` (of the input/select only: a native select's\n * placeholder `<option disabled>` must not grey the box out). Buttons inside the box\n * (reveal password, clear) keep their own focus ring and don't light the box up.\n */\nexport const wrappedControlStates = [\n \"has-[[aria-invalid=true]]:border-error has-[[aria-invalid=true]]:ring-error\",\n \"has-[:focus:not(button)]:border-border-focus has-[:focus:not(button)]:ring-border-focus\",\n \"has-[:is(input,select):disabled]:cursor-not-allowed has-[:is(input,select):disabled]:border-border has-[:is(input,select):disabled]:bg-surface has-[:is(input,select):disabled]:text-text-disabled\",\n].join(\" \");\n\n/** States for a box that IS the control (Textarea). */\nexport const selfControlStates = [\n \"outline-none focus:border-border-focus focus:ring-border-focus\",\n \"aria-invalid:not-focus:border-error aria-invalid:not-focus:ring-error\",\n \"disabled:cursor-not-allowed disabled:border-border disabled:bg-surface disabled:text-text-disabled\",\n].join(\" \");\n\n/**\n * States for a button box (Select trigger). It keeps the focus border while its popup\n * is open, because focus is inside the list then.\n */\nexport const triggerControlStates = [\n \"outline-none focus:border-border-focus focus:ring-border-focus data-popup-open:border-border-focus data-popup-open:ring-border-focus\",\n \"aria-invalid:not-focus:not-data-popup-open:border-error aria-invalid:not-focus:not-data-popup-open:ring-error\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border data-disabled:bg-surface data-disabled:text-text-disabled\",\n].join(\" \");\n\n/** Placeholder: the same Value node in text-secondary, never a variant. */\nexport const controlPlaceholder =\n \"placeholder:text-text-secondary disabled:placeholder:text-text-disabled\";\n\n/**\n * Leading / trailing icon slot: 16px in text-secondary (5.07:1), disabled with the\n * box. The box carries `group/control`.\n */\nexport const controlIcon =\n \"flex shrink-0 text-text-secondary [&_svg]:size-icon-sm group-has-[:is(input,select):disabled]/control:text-text-disabled group-data-disabled/control:text-text-disabled\";\n\n/** Small icon button inside the box (reveal password, clear). 44px hit area. */\nexport const controlAction =\n \"focus-ring touch-target -me-1 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-sm text-text-secondary transition-colors hover:text-text-primary disabled:cursor-not-allowed disabled:text-text-disabled [&_svg]:size-icon-sm\";\n"],"names":[],"mappings":"AAWO,MAAM,aACX;AAGK,MAAM,aAAa;AAGnB,MAAM,gBAAgB;AAAA,EAC3B,SAAS;AAAA,EACT,OAAO;AACT;AAWO,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAGH,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAMH,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAGH,MAAM,qBACX;AAMK,MAAM,cACX;AAGK,MAAM,gBACX;"}
1
+ {"version":3,"file":"control-styles.js","sources":["../../../src/components/input/control-styles.ts"],"sourcesContent":["/*\n * The box shared by Input, Textarea, Select, NativeSelect and PhoneInput. Figma draws\n * them as the same object on purpose: same radius, padding, border and states, so a\n * field and a dropdown in the same row only differ by the chevron.\n *\n * Focus and Error are a 2px border (border-thick), not just a colour change. The\n * second pixel is an inset ring, so the content never moves when the state changes.\n * Focus wins over Error: the message under the field keeps saying what is wrong.\n */\n\n/** Border, radius, fill and text colour of the box. */\nexport const controlBox =\n \"w-full min-w-0 rounded-md border border-border bg-background text-text-primary shadow-xs ring-1 ring-transparent ring-inset transition-[border-color,box-shadow]\";\n\n/** One-line content: 12px side padding, 8px between icon and text, Label/Default (14). */\nexport const controlRow = \"flex items-center gap-2 px-3 text-label-default\";\n\n/** control-md (40) and control-sm (36): the same tokens as Button, so they line up. */\nexport const controlHeight = {\n default: \"h-control-md\",\n small: \"h-control-sm\",\n} as const;\n\nexport type ControlSize = keyof typeof controlHeight;\n\n/**\n * States for a box that WRAPS the real control (Input, NativeSelect, the phone\n * prefix). Read from the control inside: `aria-invalid` (set by `invalid` or by\n * Field), `:focus` and `:disabled` (of the input/select only: a native select's\n * placeholder `<option disabled>` must not grey the box out). Buttons inside the box\n * (reveal password, clear) keep their own focus ring and don't light the box up.\n */\nexport const wrappedControlStates = [\n \"has-[[aria-invalid=true]]:border-error has-[[aria-invalid=true]]:ring-error\",\n \"has-[:focus:not(button)]:border-border-focus has-[:focus:not(button)]:ring-border-focus\",\n \"has-[:is(input,select):disabled]:cursor-not-allowed has-[:is(input,select):disabled]:border-border-subtle has-[:is(input,select):disabled]:bg-surface has-[:is(input,select):disabled]:text-text-disabled has-[:is(input,select):disabled]:shadow-none\",\n].join(\" \");\n\n/** States for a box that IS the control (Textarea). */\nexport const selfControlStates = [\n \"outline-none focus:border-border-focus focus:ring-border-focus\",\n \"aria-invalid:not-focus:border-error aria-invalid:not-focus:ring-error\",\n \"disabled:cursor-not-allowed disabled:border-border-subtle disabled:bg-surface disabled:text-text-disabled disabled:shadow-none\",\n].join(\" \");\n\n/**\n * States for a button box (Select trigger). It keeps the focus border while its popup\n * is open, because focus is inside the list then.\n */\nexport const triggerControlStates = [\n \"outline-none focus:border-border-focus focus:ring-border-focus data-popup-open:border-border-focus data-popup-open:ring-border-focus\",\n \"aria-invalid:not-focus:not-data-popup-open:border-error aria-invalid:not-focus:not-data-popup-open:ring-error\",\n \"data-disabled:cursor-not-allowed data-disabled:border-border-subtle data-disabled:bg-surface data-disabled:text-text-disabled data-disabled:shadow-none\",\n].join(\" \");\n\n/** Placeholder: the same Value node in text-secondary, never a variant. */\nexport const controlPlaceholder =\n \"placeholder:text-text-secondary disabled:placeholder:text-text-disabled\";\n\n/**\n * Leading / trailing icon slot: 16px in text-secondary (5.07:1), disabled with the\n * box. The box carries `group/control`.\n */\nexport const controlIcon =\n \"flex shrink-0 text-text-secondary [&_svg]:size-icon-sm group-has-[:is(input,select):disabled]/control:text-text-disabled group-data-disabled/control:text-text-disabled\";\n\n/** Small icon button inside the box (reveal password, clear). 44px hit area. */\nexport const controlAction =\n \"focus-ring touch-target -me-1 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-sm text-text-secondary transition-colors hover:text-text-primary disabled:cursor-not-allowed disabled:text-text-disabled [&_svg]:size-icon-sm\";\n"],"names":[],"mappings":"AAWO,MAAM,aACX;AAGK,MAAM,aAAa;AAGnB,MAAM,gBAAgB;AAAA,EAC3B,SAAS;AAAA,EACT,OAAO;AACT;AAWO,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAGH,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAMH,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAGH,MAAM,qBACX;AAMK,MAAM,cACX;AAGK,MAAM,gBACX;"}
@@ -0,0 +1 @@
1
+ export { StatCard, type StatCardProps, statCardVariants } from "./stat-card";
@@ -0,0 +1,31 @@
1
+ import { type VariantProps } from "class-variance-authority";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+ import { type IconContainerTone } from "../icon-container";
4
+ export declare const statCardVariants: (props?: ({
5
+ highlighted?: boolean | null | undefined;
6
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
+ export interface StatCardProps extends Omit<ComponentProps<"div">, "children">, VariantProps<typeof statCardVariants> {
8
+ /** What the figure is: "A pagar este mes", "Operaciones". */
9
+ label: ReactNode;
10
+ /** The figure, already formatted by the app: "4.215,00 €", "58". */
11
+ value: ReactNode;
12
+ /** Small print under the figure: "58 operaciones", "Tienen operaciones sin tarifa". */
13
+ detail?: ReactNode;
14
+ /** A MediCore icon, drawn in an `IconContainer`. */
15
+ icon: ReactNode;
16
+ /** Tone of the icon container. Semantic: `error` only when the figure is a problem. */
17
+ tone?: IconContainerTone;
18
+ /** The one figure the screen is about ("Total a pagar"): brand fill and brand border. */
19
+ highlighted?: boolean;
20
+ }
21
+ /**
22
+ * A key figure of an admin screen: icon, label and value, with optional small print.
23
+ * Four in a row sum up a list; three in a `DetailHeader` sum up a record.
24
+ *
25
+ * - The text is a `<dl>`: the label is the term and the figure its value.
26
+ * - The app formats the value (es-ES, symbol after: "4.215,00 €"); the card doesn't.
27
+ * - Text stays in text-primary on the highlighted fill (text-secondary is 4.44:1 there).
28
+ * - Only one card per row is `highlighted`, the one that answers the screen's question.
29
+ * - Not for marketing figures: those are `StatTile`.
30
+ */
31
+ export declare function StatCard({ label, value, detail, icon, tone, highlighted, className, ...props }: StatCardProps): React.JSX.Element;
@@ -0,0 +1,85 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "../../lib/cn.js";
4
+ import { IconContainer } from "../icon-container/icon-container.js";
5
+ const statCardVariants = cva(
6
+ "flex min-w-0 items-start gap-3 rounded-xl p-4",
7
+ {
8
+ variants: {
9
+ highlighted: {
10
+ true: "border border-brand bg-brand-subtle",
11
+ false: "bg-background shadow-s"
12
+ }
13
+ },
14
+ defaultVariants: {
15
+ highlighted: false
16
+ }
17
+ }
18
+ );
19
+ function StatCard({
20
+ label,
21
+ value,
22
+ detail,
23
+ icon,
24
+ tone = "neutral",
25
+ highlighted = false,
26
+ className,
27
+ ...props
28
+ }) {
29
+ return /* @__PURE__ */ jsxs(
30
+ "div",
31
+ {
32
+ "data-slot": "stat-card",
33
+ className: cn(statCardVariants({ highlighted }), className),
34
+ ...props,
35
+ children: [
36
+ /* @__PURE__ */ jsx(
37
+ IconContainer,
38
+ {
39
+ icon,
40
+ tone: highlighted ? "brand" : tone,
41
+ size: 40,
42
+ className: "shrink-0"
43
+ }
44
+ ),
45
+ /* @__PURE__ */ jsxs("dl", { className: "flex min-w-0 flex-col gap-0.5", children: [
46
+ /* @__PURE__ */ jsx(
47
+ "dt",
48
+ {
49
+ className: cn(
50
+ "text-body-small",
51
+ highlighted ? "text-text-primary" : "text-text-secondary"
52
+ ),
53
+ children: label
54
+ }
55
+ ),
56
+ /* @__PURE__ */ jsx(
57
+ "dd",
58
+ {
59
+ className: cn(
60
+ "text-heading-h3 tabular-nums",
61
+ highlighted ? "text-text-brand" : "text-text-primary"
62
+ ),
63
+ children: value
64
+ }
65
+ ),
66
+ detail && /* @__PURE__ */ jsx(
67
+ "dd",
68
+ {
69
+ className: cn(
70
+ "text-body-small",
71
+ highlighted ? "text-text-primary" : "text-text-secondary"
72
+ ),
73
+ children: detail
74
+ }
75
+ )
76
+ ] })
77
+ ]
78
+ }
79
+ );
80
+ }
81
+ export {
82
+ StatCard,
83
+ statCardVariants
84
+ };
85
+ //# sourceMappingURL=stat-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stat-card.js","sources":["../../../src/components/stat-card/stat-card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { IconContainer, type IconContainerTone } from \"../icon-container\";\n\nexport const statCardVariants = cva(\n \"flex min-w-0 items-start gap-3 rounded-xl p-4\",\n {\n variants: {\n highlighted: {\n true: \"border border-brand bg-brand-subtle\",\n false: \"bg-background shadow-s\",\n },\n },\n defaultVariants: {\n highlighted: false,\n },\n },\n);\n\nexport interface StatCardProps\n extends Omit<ComponentProps<\"div\">, \"children\">,\n VariantProps<typeof statCardVariants> {\n /** What the figure is: \"A pagar este mes\", \"Operaciones\". */\n label: ReactNode;\n /** The figure, already formatted by the app: \"4.215,00 €\", \"58\". */\n value: ReactNode;\n /** Small print under the figure: \"58 operaciones\", \"Tienen operaciones sin tarifa\". */\n detail?: ReactNode;\n /** A MediCore icon, drawn in an `IconContainer`. */\n icon: ReactNode;\n /** Tone of the icon container. Semantic: `error` only when the figure is a problem. */\n tone?: IconContainerTone;\n /** The one figure the screen is about (\"Total a pagar\"): brand fill and brand border. */\n highlighted?: boolean;\n}\n\n/**\n * A key figure of an admin screen: icon, label and value, with optional small print.\n * Four in a row sum up a list; three in a `DetailHeader` sum up a record.\n *\n * - The text is a `<dl>`: the label is the term and the figure its value.\n * - The app formats the value (es-ES, symbol after: \"4.215,00 €\"); the card doesn't.\n * - Text stays in text-primary on the highlighted fill (text-secondary is 4.44:1 there).\n * - Only one card per row is `highlighted`, the one that answers the screen's question.\n * - Not for marketing figures: those are `StatTile`.\n */\nexport function StatCard({\n label,\n value,\n detail,\n icon,\n tone = \"neutral\",\n highlighted = false,\n className,\n ...props\n}: StatCardProps): React.JSX.Element {\n return (\n <div\n data-slot=\"stat-card\"\n className={cn(statCardVariants({ highlighted }), className)}\n {...props}\n >\n <IconContainer\n icon={icon}\n tone={highlighted ? \"brand\" : tone}\n size={40}\n className=\"shrink-0\"\n />\n <dl className=\"flex min-w-0 flex-col gap-0.5\">\n <dt\n className={cn(\n \"text-body-small\",\n highlighted ? \"text-text-primary\" : \"text-text-secondary\",\n )}\n >\n {label}\n </dt>\n <dd\n className={cn(\n \"text-heading-h3 tabular-nums\",\n highlighted ? \"text-text-brand\" : \"text-text-primary\",\n )}\n >\n {value}\n </dd>\n {detail && (\n <dd\n className={cn(\n \"text-body-small\",\n highlighted ? \"text-text-primary\" : \"text-text-secondary\",\n )}\n >\n {detail}\n </dd>\n )}\n </dl>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;AAMO,MAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,aAAa;AAAA,QACX,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,aAAa;AAAA,IAAA;AAAA,EACf;AAEJ;AA6BO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,cAAc;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAqC;AACnC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iBAAiB,EAAE,YAAA,CAAa,GAAG,SAAS;AAAA,MACzD,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA,MAAM,cAAc,UAAU;AAAA,YAC9B,MAAM;AAAA,YACN,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,QAEZ,qBAAC,MAAA,EAAG,WAAU,iCACZ,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,cAAc,sBAAsB;AAAA,cAAA;AAAA,cAGrC,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAEH;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,cAAc,oBAAoB;AAAA,cAAA;AAAA,cAGnC,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAEF,UACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,cAAc,sBAAsB;AAAA,cAAA;AAAA,cAGrC,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,EAAA,CAEJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -47,7 +47,7 @@ export interface TableRowProps extends ComponentProps<"tr"> {
47
47
  * A row. Figma has no Table/Row: a row is just cells as wide as their headers. If a
48
48
  * row opens a detail, put a link in its first cell — don't make the `<tr>` clickable.
49
49
  */
50
- export declare function TableRow(props: TableRowProps): React.JSX.Element;
50
+ export declare function TableRow({ className, ...props }: TableRowProps): React.JSX.Element;
51
51
  export declare const tableHeadVariants: (props?: ({
52
52
  type?: "text" | "numeric" | "action" | "badge" | null | undefined;
53
53
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -43,7 +43,7 @@ function Table({
43
43
  "data-slot": "table-container",
44
44
  ...scrollRegion,
45
45
  className: cn(
46
- "relative w-full overflow-x-auto focus-ring",
46
+ "relative w-full overflow-x-auto rounded-xl border border-border-subtle bg-background focus-ring",
47
47
  containerClassName
48
48
  ),
49
49
  children: /* @__PURE__ */ jsx(
@@ -68,8 +68,21 @@ function TableHeader(props) {
68
68
  function TableBody(props) {
69
69
  return /* @__PURE__ */ jsx("tbody", { "data-slot": "table-body", ...props });
70
70
  }
71
- function TableRow(props) {
72
- return /* @__PURE__ */ jsx("tr", { "data-slot": "table-row", ...props });
71
+ function TableRow({
72
+ className,
73
+ ...props
74
+ }) {
75
+ return /* @__PURE__ */ jsx(
76
+ "tr",
77
+ {
78
+ "data-slot": "table-row",
79
+ className: cn(
80
+ "transition-colors [tbody_&:last-child>td]:border-b-0 [tbody_&:hover>td]:bg-surface",
81
+ className
82
+ ),
83
+ ...props
84
+ }
85
+ );
73
86
  }
74
87
  const alignment = {
75
88
  text: "text-left",
@@ -79,7 +92,7 @@ const alignment = {
79
92
  action: "w-0 whitespace-nowrap text-center"
80
93
  };
81
94
  const tableHeadVariants = cva(
82
- "h-12 border-b border-border-subtle bg-surface px-4 align-middle text-label-strong text-text-secondary data-sorted:text-text-primary",
95
+ "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",
83
96
  { variants: { type: alignment }, defaultVariants: { type: "text" } }
84
97
  );
85
98
  function TableHead({
@@ -135,7 +148,7 @@ function TableHead({
135
148
  );
136
149
  }
137
150
  const tableCellVariants = cva(
138
- "h-16 border-b border-border-subtle bg-background px-4 align-middle text-body-small text-text-primary",
151
+ "h-14 border-b border-border-subtle bg-background px-4 align-middle text-body-small text-text-primary transition-colors",
139
152
  {
140
153
  variants: {
141
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 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(props: TableRowProps): React.JSX.Element {\n return <tr data-slot=\"table-row\" {...props} />;\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-12 border-b border-border-subtle bg-surface px-4 align-middle text-label-strong text-text-secondary 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-16 border-b border-border-subtle bg-background px-4 align-middle text-body-small text-text-primary\",\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 \"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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"}
package/dist/index.d.ts CHANGED
@@ -25,8 +25,10 @@ export * from "./components/checkbox";
25
25
  export * from "./components/chip";
26
26
  export * from "./components/date-badge";
27
27
  export * from "./components/day-cell";
28
+ export * from "./components/detail-header";
28
29
  export * from "./components/divider";
29
30
  export * from "./components/empty-state";
31
+ export * from "./components/fact-card";
30
32
  export * from "./components/field";
31
33
  export * from "./components/footer";
32
34
  export * from "./components/health-record";
@@ -58,6 +60,7 @@ export * from "./components/sidebar";
58
60
  export * from "./components/skeleton";
59
61
  export * from "./components/social-button";
60
62
  export * from "./components/spinner";
63
+ export * from "./components/stat-card";
61
64
  export * from "./components/stat-tile";
62
65
  export * from "./components/step-indicator";
63
66
  export * from "./components/store-badge";
package/dist/index.js CHANGED
@@ -38,8 +38,10 @@ import { Chip, ChipGroup, chipVariants } from "./components/chip/chip.js";
38
38
  import { ConfirmModal } from "./components/modal/confirm-modal.js";
39
39
  import { DateBadge } from "./components/date-badge/date-badge.js";
40
40
  import { DayCell, DayCellGroup } from "./components/day-cell/day-cell.js";
41
+ import { DetailHeader, DetailHeaderBack } from "./components/detail-header/detail-header.js";
41
42
  import { Divider, dividerVariants } from "./components/divider/divider.js";
42
43
  import { EmptyState, emptyStateVariants } from "./components/empty-state/empty-state.js";
44
+ import { FactCard, factCardVariants } from "./components/fact-card/fact-card.js";
43
45
  import { Field } from "./components/field/field.js";
44
46
  import { Footer, FooterLink } from "./components/footer/footer.js";
45
47
  import { HealthRecordHeader, HealthRecordRow, HealthRecordSection } from "./components/health-record/health-record.js";
@@ -73,6 +75,7 @@ import { Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Sidebar
73
75
  import { Skeleton, skeletonVariants } from "./components/skeleton/skeleton.js";
74
76
  import { SocialButton } from "./components/social-button/social-button.js";
75
77
  import { Spinner } from "./components/spinner/spinner.js";
78
+ import { StatCard, statCardVariants } from "./components/stat-card/stat-card.js";
76
79
  import { StatTile } from "./components/stat-tile/stat-tile.js";
77
80
  import { StepIndicator } from "./components/step-indicator/step-indicator.js";
78
81
  import { StoreBadge } from "./components/store-badge/store-badge.js";
@@ -141,8 +144,11 @@ export {
141
144
  DateBadge,
142
145
  DayCell,
143
146
  DayCellGroup,
147
+ DetailHeader,
148
+ DetailHeaderBack,
144
149
  Divider,
145
150
  EmptyState,
151
+ FactCard,
146
152
  Field,
147
153
  Footer,
148
154
  FooterLink,
@@ -203,6 +209,7 @@ export {
203
209
  Skeleton,
204
210
  SocialButton,
205
211
  Spinner,
212
+ StatCard,
206
213
  StatTile,
207
214
  StepIndicator,
208
215
  StoreBadge,
@@ -243,6 +250,7 @@ export {
243
250
  defaultLabels,
244
251
  dividerVariants,
245
252
  emptyStateVariants,
253
+ factCardVariants,
246
254
  getDefaultPhonePrefixes,
247
255
  iconButtonVariants,
248
256
  iconContainerVariants,
@@ -251,6 +259,7 @@ export {
251
259
  radioVariants,
252
260
  severityIndicatorVariants,
253
261
  skeletonVariants,
262
+ statCardVariants,
254
263
  tableCellVariants,
255
264
  tableHeadVariants,
256
265
  tableSortFrom,
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
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.1.1",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "sideEffects": [