@jotis-me/react 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +77 -2
  2. package/dist/components/ActivityFeed.d.ts +29 -0
  3. package/dist/components/ActivityFeed.js +10 -0
  4. package/dist/components/Alert.d.ts +34 -0
  5. package/dist/components/Alert.js +27 -0
  6. package/dist/components/AppIcon.d.ts +27 -0
  7. package/dist/components/AppIcon.js +9 -0
  8. package/dist/components/Avatar.d.ts +20 -0
  9. package/dist/components/Avatar.js +27 -0
  10. package/dist/components/AvatarGroup.d.ts +10 -0
  11. package/dist/components/AvatarGroup.js +7 -0
  12. package/dist/components/AvatarLabel.d.ts +10 -0
  13. package/dist/components/AvatarLabel.js +7 -0
  14. package/dist/components/BackgroundPattern.d.ts +35 -0
  15. package/dist/components/BackgroundPattern.js +73 -0
  16. package/dist/components/Badge.d.ts +38 -0
  17. package/dist/components/Badge.js +82 -0
  18. package/dist/components/Banner.d.ts +48 -0
  19. package/dist/components/Banner.js +60 -0
  20. package/dist/components/BarChart.d.ts +25 -0
  21. package/dist/components/BarChart.js +30 -0
  22. package/dist/components/Breadcrumbs.d.ts +19 -0
  23. package/dist/components/Breadcrumbs.js +14 -0
  24. package/dist/components/BrowserFrame.d.ts +18 -0
  25. package/dist/components/BrowserFrame.js +15 -0
  26. package/dist/components/Button.d.ts +51 -0
  27. package/dist/components/Button.js +54 -0
  28. package/dist/components/ButtonGroup.d.ts +21 -0
  29. package/dist/components/ButtonGroup.js +14 -0
  30. package/dist/components/CalendarView.d.ts +30 -0
  31. package/dist/components/CalendarView.js +43 -0
  32. package/dist/components/CamFrame.d.ts +21 -0
  33. package/dist/components/CamFrame.js +11 -0
  34. package/dist/components/CardHeader.d.ts +32 -0
  35. package/dist/components/CardHeader.js +12 -0
  36. package/dist/components/ChannelBanner.d.ts +24 -0
  37. package/dist/components/ChannelBanner.js +13 -0
  38. package/dist/components/ChannelPanel.d.ts +36 -0
  39. package/dist/components/ChannelPanel.js +12 -0
  40. package/dist/components/ChartLegend.d.ts +12 -0
  41. package/dist/components/ChartLegend.js +7 -0
  42. package/dist/components/ChatBadge.d.ts +15 -0
  43. package/dist/components/ChatBadge.js +39 -0
  44. package/dist/components/ChatBox.d.ts +42 -0
  45. package/dist/components/ChatBox.js +35 -0
  46. package/dist/components/Checkbox.d.ts +17 -0
  47. package/dist/components/Checkbox.js +26 -0
  48. package/dist/components/CloseButton.d.ts +7 -0
  49. package/dist/components/CloseButton.js +8 -0
  50. package/dist/components/CodeSnippet.d.ts +23 -0
  51. package/dist/components/CodeSnippet.js +75 -0
  52. package/dist/components/ColorPicker.d.ts +17 -0
  53. package/dist/components/ColorPicker.js +42 -0
  54. package/dist/components/CommandMenu.d.ts +72 -0
  55. package/dist/components/CommandMenu.js +102 -0
  56. package/dist/components/ContentDivider.d.ts +17 -0
  57. package/dist/components/ContentDivider.js +13 -0
  58. package/dist/components/CreditCard.d.ts +28 -0
  59. package/dist/components/CreditCard.js +63 -0
  60. package/dist/components/DatePicker.d.ts +48 -0
  61. package/dist/components/DatePicker.js +105 -0
  62. package/dist/components/Dropdown.d.ts +39 -0
  63. package/dist/components/Dropdown.js +39 -0
  64. package/dist/components/EmptyState.d.ts +18 -0
  65. package/dist/components/EmptyState.js +7 -0
  66. package/dist/components/FeaturedIcon.d.ts +17 -0
  67. package/dist/components/FeaturedIcon.js +32 -0
  68. package/dist/components/FileUpload.d.ts +39 -0
  69. package/dist/components/FileUpload.js +50 -0
  70. package/dist/components/FilterBar.d.ts +46 -0
  71. package/dist/components/FilterBar.js +24 -0
  72. package/dist/components/Icon.d.ts +5 -0
  73. package/dist/components/Icon.js +9 -0
  74. package/dist/components/InlineCTA.d.ts +19 -0
  75. package/dist/components/InlineCTA.js +7 -0
  76. package/dist/components/Input.d.ts +16 -0
  77. package/dist/components/Input.js +10 -0
  78. package/dist/components/LineChart.d.ts +25 -0
  79. package/dist/components/LineChart.js +23 -0
  80. package/dist/components/Message.d.ts +22 -0
  81. package/dist/components/Message.js +10 -0
  82. package/dist/components/MessageInput.d.ts +17 -0
  83. package/dist/components/MessageInput.js +25 -0
  84. package/dist/components/MetricCard.d.ts +28 -0
  85. package/dist/components/MetricCard.js +11 -0
  86. package/dist/components/Modal.d.ts +45 -0
  87. package/dist/components/Modal.js +15 -0
  88. package/dist/components/Notification.d.ts +15 -0
  89. package/dist/components/Notification.js +33 -0
  90. package/dist/components/NowPlaying.d.ts +25 -0
  91. package/dist/components/NowPlaying.js +16 -0
  92. package/dist/components/PageHeader.d.ts +22 -0
  93. package/dist/components/PageHeader.js +8 -0
  94. package/dist/components/Pagination.d.ts +29 -0
  95. package/dist/components/Pagination.js +33 -0
  96. package/dist/components/PieChart.d.ts +32 -0
  97. package/dist/components/PieChart.js +30 -0
  98. package/dist/components/ProgressBar.d.ts +11 -0
  99. package/dist/components/ProgressBar.js +8 -0
  100. package/dist/components/Radio.d.ts +15 -0
  101. package/dist/components/Radio.js +10 -0
  102. package/dist/components/RadioGroup.d.ts +26 -0
  103. package/dist/components/RadioGroup.js +9 -0
  104. package/dist/components/SectionFooter.d.ts +12 -0
  105. package/dist/components/SectionFooter.js +7 -0
  106. package/dist/components/SectionHeader.d.ts +24 -0
  107. package/dist/components/SectionHeader.js +7 -0
  108. package/dist/components/Select.d.ts +22 -0
  109. package/dist/components/Select.js +14 -0
  110. package/dist/components/SidebarNav.d.ts +38 -0
  111. package/dist/components/SidebarNav.js +26 -0
  112. package/dist/components/Skeleton.d.ts +15 -0
  113. package/dist/components/Skeleton.js +10 -0
  114. package/dist/components/Slideout.d.ts +28 -0
  115. package/dist/components/Slideout.js +15 -0
  116. package/dist/components/Slider.d.ts +15 -0
  117. package/dist/components/Slider.js +17 -0
  118. package/dist/components/Spinner.d.ts +11 -0
  119. package/dist/components/Spinner.js +12 -0
  120. package/dist/components/StreamAlert.d.ts +34 -0
  121. package/dist/components/StreamAlert.js +10 -0
  122. package/dist/components/StreamCountdown.d.ts +30 -0
  123. package/dist/components/StreamCountdown.js +44 -0
  124. package/dist/components/StreamEvents.d.ts +40 -0
  125. package/dist/components/StreamEvents.js +20 -0
  126. package/dist/components/StreamGoal.d.ts +22 -0
  127. package/dist/components/StreamGoal.js +21 -0
  128. package/dist/components/StreamScene.d.ts +35 -0
  129. package/dist/components/StreamScene.js +33 -0
  130. package/dist/components/StreamStatus.d.ts +28 -0
  131. package/dist/components/StreamStatus.js +26 -0
  132. package/dist/components/Table.d.ts +36 -0
  133. package/dist/components/Table.js +15 -0
  134. package/dist/components/Tabs.d.ts +38 -0
  135. package/dist/components/Tabs.js +34 -0
  136. package/dist/components/Tag.d.ts +18 -0
  137. package/dist/components/Tag.js +15 -0
  138. package/dist/components/TextEditor.d.ts +37 -0
  139. package/dist/components/TextEditor.js +103 -0
  140. package/dist/components/Textarea.d.ts +6 -0
  141. package/dist/components/Textarea.js +10 -0
  142. package/dist/components/Toggle.d.ts +22 -0
  143. package/dist/components/Toggle.js +12 -0
  144. package/dist/components/Tooltip.d.ts +15 -0
  145. package/dist/components/Tooltip.js +27 -0
  146. package/dist/components/TreeView.d.ts +33 -0
  147. package/dist/components/TreeView.js +48 -0
  148. package/dist/components/VideoPlayer.d.ts +25 -0
  149. package/dist/components/VideoPlayer.js +52 -0
  150. package/dist/index.d.ts +80 -0
  151. package/dist/index.js +76 -0
  152. package/dist/internal/chart.d.ts +38 -0
  153. package/dist/internal/chart.js +33 -0
  154. package/dist/internal/choice.d.ts +18 -0
  155. package/dist/internal/choice.js +17 -0
  156. package/dist/internal/cx.d.ts +3 -0
  157. package/dist/internal/cx.js +26 -0
  158. package/dist/internal/field.d.ts +29 -0
  159. package/dist/internal/field.js +23 -0
  160. package/dist/internal/format.d.ts +12 -0
  161. package/dist/internal/format.js +40 -0
  162. package/dist/internal/kbd.d.ts +6 -0
  163. package/dist/internal/kbd.js +6 -0
  164. package/dist/internal/layer.d.ts +21 -0
  165. package/dist/internal/layer.js +27 -0
  166. package/dist/internal/line.d.ts +2 -0
  167. package/dist/internal/line.js +5 -0
  168. package/dist/internal/overlay.d.ts +2 -0
  169. package/dist/internal/overlay.js +4 -0
  170. package/dist/internal/stream-stage.d.ts +18 -0
  171. package/dist/internal/stream-stage.js +38 -0
  172. package/dist/internal/stream.d.ts +56 -0
  173. package/dist/internal/stream.js +112 -0
  174. package/dist/internal/strings.d.ts +101 -0
  175. package/dist/internal/strings.js +103 -0
  176. package/dist/internal/use-controllable.d.ts +5 -0
  177. package/dist/internal/use-controllable.js +17 -0
  178. package/package.json +66 -5
  179. package/styles.css +7 -0
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { controlClasses, describedBy, FieldShell, nativeClasses, } from "../internal/field.js";
4
+ import { Icon } from "./Icon.js";
5
+ /** Campo de texto de una línea con etiqueta, pista y error, en tres alturas. */
6
+ export function Input({ label, required, hint, error, size = "md", icon, trailing, id: givenId, className, disabled, ...rest }) {
7
+ const autoId = useId();
8
+ const id = givenId ?? autoId;
9
+ return (_jsx(FieldShell, { id: id, label: label, required: required, hint: hint, error: error, size: size, className: className, children: _jsxs("div", { className: controlClasses(size, error, disabled), children: [icon ? _jsx(Icon, { name: icon, size: 20 }) : null, _jsx("input", { id: id, required: required, disabled: disabled, ...describedBy(id, { hint, error }), ...rest, className: nativeClasses(size) }), error ? (_jsx("span", { className: "inline-flex text-fg-error-secondary", children: _jsx(Icon, { name: "alert-circle", size: 16 }) })) : trailing ? (_jsx("span", { className: "inline-flex", children: _jsx(Icon, { name: trailing, size: 16 }) })) : null] }) }));
10
+ }
@@ -0,0 +1,25 @@
1
+ import { type ChartSeries } from "../internal/chart.js";
2
+ export interface LineChartProps {
3
+ /** Etiquetas del eje horizontal, una por punto. */
4
+ labels: string[];
5
+ /** Hasta 4 series. Sin `color`, la paleta de series en orden. */
6
+ series: ChartSeries[];
7
+ /** Formato de los valores en el eje y en el título de cada punto, como `(v) => formatCurrency(v)`. */
8
+ format?: (v: number) => string;
9
+ /**
10
+ * Relleno suave bajo la primera serie.
11
+ * @default true
12
+ */
13
+ area?: boolean;
14
+ /** Leyenda encima. Por defecto, sí con dos series o más. */
15
+ legend?: boolean;
16
+ /** Proporción del dibujo (se escala al ancho del contenedor). @default 640 */
17
+ width?: number;
18
+ /** @default 240 */
19
+ height?: number;
20
+ /** Qué cuenta la gráfica, para lectores de pantalla. @default "Gráfica de líneas" */
21
+ "aria-label"?: string;
22
+ className?: string;
23
+ }
24
+ /** Gráfica de líneas para la evolución de una o varias series en el tiempo. La escala empieza en cero. */
25
+ export declare function LineChart({ labels, series, format, area, legend, width, height, className, ...rest }: LineChartProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Axes, nice, PAD, pointTitle, seriesColor } from "../internal/chart.js";
3
+ import { cx } from "../internal/cx.js";
4
+ import { ChartLegend } from "./ChartLegend.js";
5
+ /** Gráfica de líneas para la evolución de una o varias series en el tiempo. La escala empieza en cero. */
6
+ export function LineChart({ labels, series, format, area = true, legend, width = 640, height = 240, className, ...rest }) {
7
+ const plot = { w: width, h: height, pad: PAD };
8
+ const top = nice(Math.max(0, ...series.flatMap((s) => s.data)));
9
+ const iw = width - PAD.l - PAD.r;
10
+ const ih = height - PAD.t - PAD.b;
11
+ const n = labels.length;
12
+ const point = (v, i) => [PAD.l + iw * ((i + 0.5) / n), PAD.t + ih * (1 - v / top)];
13
+ return (_jsxs("figure", { className: cx("m-0 flex flex-col gap-4 font-body", className), children: [(legend ?? series.length > 1) ? _jsx(ChartLegend, { series: series }) : null, _jsxs("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": rest["aria-label"] ?? "Gráfica de líneas", className: "h-auto w-full overflow-visible", children: [_jsx(Axes, { plot: plot, top: top, labels: labels, format: format }), series.map((s, si) => {
14
+ const pts = s.data.map(point);
15
+ const c = seriesColor(si, s.color);
16
+ const d = pts
17
+ .map(([x, y], i) => `${i ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`)
18
+ .join(" ");
19
+ const first = pts[0];
20
+ const last = pts[pts.length - 1];
21
+ return (_jsxs("g", { children: [si === 0 && area && first && last ? (_jsx("path", { d: `${d} L${last[0]} ${PAD.t + ih} L${first[0]} ${PAD.t + ih} Z`, fill: c, opacity: 0.08 })) : null, _jsx("path", { d: d, fill: "none", stroke: c, strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round" }), pts.map(([x, y], i) => (_jsx("circle", { cx: x, cy: y, r: 3, fill: "var(--bg-primary)", stroke: c, strokeWidth: 2, children: _jsx("title", { children: pointTitle(s.name, labels[i] ?? "", format ? format(s.data[i] ?? 0) : (s.data[i] ?? 0)) }) }, i)))] }, si));
22
+ })] })] }));
23
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ import { type AvatarProps } from "./Avatar.js";
3
+ export interface MessageProps {
4
+ /** Quien escribe. En los enviados, por defecto "Tú". */
5
+ author?: ReactNode;
6
+ /** Avatar del recibido (32px). Por defecto, las dos primeras letras del autor. */
7
+ avatar?: AvatarProps;
8
+ time?: ReactNode;
9
+ /** Enviado por el usuario: a la derecha, sin avatar, burbuja `bg-primary` con la esquina superior derecha recta. */
10
+ sent?: boolean;
11
+ /** Puntos de "escribiendo" en lugar del contenido. */
12
+ typing?: boolean;
13
+ /** Punto de en línea en el avatar. */
14
+ online?: boolean;
15
+ /** Bajo la burbuja: reacciones, adjuntos, "Visto". */
16
+ footer?: ReactNode;
17
+ /** Texto del mensaje. Respuestas cortas, en párrafos. */
18
+ children?: ReactNode;
19
+ className?: string;
20
+ }
21
+ /** Mensaje de una conversación, recibido (con avatar, a la izquierda) o enviado (a la derecha). Es el chat de Emptor. */
22
+ export declare function Message({ author, avatar, time, sent, typing, online, footer, children, className, }: MessageProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../internal/cx.js";
3
+ import { defaultStrings } from "../internal/strings.js";
4
+ import { Avatar } from "./Avatar.js";
5
+ /** Mensaje de una conversación, recibido (con avatar, a la izquierda) o enviado (a la derecha). Es el chat de Emptor. */
6
+ export function Message({ author, avatar, time, sent, typing, online, footer, children, className, }) {
7
+ return (_jsxs("div", { className: cx("flex max-w-[480px] gap-3 font-body", sent && "ml-auto justify-end", className), children: [sent ? null : (_jsx(Avatar, { size: "sm", status: online ? "online" : undefined, ...(avatar ?? { initials: typeof author === "string" ? author.slice(0, 2) : "?" }) })), _jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "text-sm font-medium text-secondary", children: sent ? (author ?? defaultStrings.you) : author }), time ? _jsx("span", { className: "text-xs text-tertiary", children: time }) : null] }), _jsx("div", { className: cx("px-3 py-2 text-md wrap-anywhere whitespace-pre-wrap text-primary shadow-[inset_0_0_0_1px_var(--border-secondary)]", sent
8
+ ? "rounded-lg rounded-tr-none bg-primary"
9
+ : "rounded-lg rounded-tl-none bg-secondary"), children: typing ? (_jsxs("span", { className: "inline-flex gap-1 py-2", children: [_jsx("span", { className: "sr-only", children: defaultStrings.typing }), [0, 1, 2].map((i) => (_jsx("i", { "aria-hidden": "true", className: cx("size-1.5 rounded-full bg-fg-quaternary motion-safe:animate-[pulse_1s_infinite]", i === 1 && "[animation-delay:.2s]", i === 2 && "[animation-delay:.4s]") }, i)))] })) : (children) }), footer ?? null] })] }));
10
+ }
@@ -0,0 +1,17 @@
1
+ export interface MessageInputProps {
2
+ /** También es el nombre accesible del campo si no pasas `aria-label`. */
3
+ placeholder?: string;
4
+ /** Recibe el texto sin espacios a los lados. Enter envía y Mayús+Enter salta de línea. */
5
+ onSend?: (text: string) => void;
6
+ /** Con esta prop aparece el botón de adjuntar. */
7
+ onAttach?: () => void;
8
+ /** @default "Enviar" */
9
+ sendLabel?: string;
10
+ /** Nombre accesible del botón de adjuntar. */
11
+ attachLabel?: string;
12
+ disabled?: boolean;
13
+ "aria-label"?: string;
14
+ className?: string;
15
+ }
16
+ /** Caja para escribir y enviar un mensaje, con botón de adjuntar opcional. */
17
+ export declare function MessageInput({ placeholder, onSend, onAttach, sendLabel, attachLabel, disabled, "aria-label": ariaLabel, className, }: MessageInputProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { cx } from "../internal/cx.js";
5
+ import { defaultStrings } from "../internal/strings.js";
6
+ import { Button } from "./Button.js";
7
+ /** Caja para escribir y enviar un mensaje, con botón de adjuntar opcional. */
8
+ export function MessageInput({ placeholder = defaultStrings.writeMessage, onSend, onAttach, sendLabel = defaultStrings.send, attachLabel = defaultStrings.attach, disabled, "aria-label": ariaLabel, className, }) {
9
+ const [text, setText] = useState("");
10
+ const empty = !text.trim();
11
+ const send = () => {
12
+ if (disabled || empty)
13
+ return;
14
+ onSend?.(text.trim());
15
+ setText("");
16
+ };
17
+ const onKeyDown = (e) => {
18
+ // Con un método de entrada (IME) a medias, Enter confirma la palabra, no envía
19
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
20
+ e.preventDefault();
21
+ send();
22
+ }
23
+ };
24
+ return (_jsxs("div", { className: cx("flex flex-col gap-2 rounded-lg bg-primary pt-3 pr-3 pb-2 pl-3.5 font-body shadow-[var(--shadow-xs),inset_0_0_0_1px_var(--border-primary)] focus-within:shadow-[var(--shadow-xs),inset_0_0_0_2px_var(--border-brand)] has-[textarea:focus-visible]:shadow-[var(--shadow-xs),inset_0_0_0_2px_var(--border-brand),var(--focus-ring-shadow)]", disabled && "opacity-50", className), children: [_jsx("textarea", { rows: 2, value: text, placeholder: placeholder, "aria-label": ariaLabel ?? placeholder ?? defaultStrings.message, disabled: disabled, onChange: (e) => setText(e.target.value), onKeyDown: onKeyDown, className: "resize-none border-0 bg-transparent p-0.5 font-body text-md text-primary outline-none placeholder:text-placeholder disabled:cursor-not-allowed" }), _jsxs("div", { className: "flex items-center justify-end gap-1", children: [onAttach ? (_jsx(Button, { hierarchy: "tertiary", size: "sm", iconOnly: "paperclip", label: attachLabel, disabled: disabled, onClick: onAttach })) : null, _jsx(Button, { size: "sm", iconTrailing: "send-01", disabled: disabled || empty, onClick: send, children: sendLabel })] })] }));
25
+ }
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from "react";
2
+ export interface MetricCardProps {
3
+ label: ReactNode;
4
+ /** La cifra, en Bricolage Grotesque 36/44 semibold. */
5
+ value: ReactNode;
6
+ /** Cambio frente al periodo anterior, como "12%". */
7
+ change?: ReactNode;
8
+ /**
9
+ * Sube (`text-success-primary`, flecha arriba) o baja (`text-error-primary`, flecha abajo): siempre con flecha,
10
+ * nunca solo color.
11
+ * @default "up"
12
+ */
13
+ trend?: "up" | "down";
14
+ /** Periodo de comparación, como "vs. septiembre". */
15
+ period?: ReactNode;
16
+ /**
17
+ * Menú de la tarjeta: `true` pinta el botón de tres puntos (con `onMenu`); también admite un nodo, como un
18
+ * `Dropdown` con `trigger` (ampliación sobre la guía).
19
+ */
20
+ menu?: boolean | ReactNode;
21
+ /** Al pulsar el botón de tres puntos (ampliación sobre la guía). */
22
+ onMenu?: () => void;
23
+ /** Nombre del botón de tres puntos. @default "Más opciones" */
24
+ menuLabel?: string;
25
+ className?: string;
26
+ }
27
+ /** Tarjeta con una cifra clave, su tendencia y el periodo de comparación. */
28
+ export declare function MetricCard({ label, value, change, trend, period, menu, onMenu, menuLabel, className, }: MetricCardProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isValidElement } from "react";
3
+ import { cx } from "../internal/cx.js";
4
+ import { defaultStrings } from "../internal/strings.js";
5
+ import { Button } from "./Button.js";
6
+ import { Icon } from "./Icon.js";
7
+ /** Tarjeta con una cifra clave, su tendencia y el periodo de comparación. */
8
+ export function MetricCard({ label, value, change, trend = "up", period, menu, onMenu, menuLabel, className, }) {
9
+ const up = trend !== "down";
10
+ return (_jsxs("div", { className: cx("flex flex-col gap-5 rounded-xl bg-primary px-6 py-5 font-body shadow-[var(--shadow-xs),inset_0_0_0_1px_var(--border-secondary)]", className), children: [_jsxs("div", { className: "flex items-center justify-between text-md font-semibold text-primary", children: [label, menu === true ? (_jsx(Button, { hierarchy: "tertiary", size: "sm", iconOnly: "dots-vertical", label: menuLabel ?? defaultStrings.moreOptions, onClick: onMenu })) : isValidElement(menu) ? (menu) : null] }), _jsxs("div", { children: [_jsx("div", { className: "font-display text-display-md font-semibold text-primary", children: value }), change ? (_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-tertiary", children: [_jsxs("span", { className: cx("inline-flex items-center gap-1 font-medium", up ? "text-success-primary" : "text-error-primary"), children: [_jsx(Icon, { name: up ? "trend-up-01" : "trend-down-01", size: 20 }), _jsxs("span", { className: "sr-only", children: [up ? defaultStrings.rising : defaultStrings.falling, " "] }), change] }), period ?? null] })) : null] })] }));
11
+ }
@@ -0,0 +1,45 @@
1
+ import { type ReactNode } from "react";
2
+ import type { StatusColor } from "./Badge.js";
3
+ import { type FeaturedIconVariant } from "./FeaturedIcon.js";
4
+ import type { IconName } from "./Icon.js";
5
+ export interface ModalProps {
6
+ /**
7
+ * Visible. Con `false` no se pinta.
8
+ * @default true
9
+ */
10
+ open?: boolean;
11
+ /** Pregunta o tarea. El botón de confirmar repite su verbo ("Eliminar", no "Aceptar"). */
12
+ title: ReactNode;
13
+ description?: ReactNode;
14
+ /** Icono destacado sobre el título (`FeaturedIcon` de 48px). */
15
+ icon?: IconName;
16
+ /** Color del icono. Por defecto, `brand` (`error` si es `destructive`). */
17
+ color?: StatusColor;
18
+ /**
19
+ * Estilo del icono destacado.
20
+ * @default "light"
21
+ */
22
+ iconVariant?: FeaturedIconVariant;
23
+ /** Confirma una acción destructiva: icono en `error`. */
24
+ destructive?: boolean;
25
+ /**
26
+ * `stacked`: icono encima y botones a partes iguales. `horizontal`: icono a la izquierda y botones a la derecha.
27
+ * @default "stacked"
28
+ */
29
+ layout?: "stacked" | "horizontal";
30
+ /** Botones del pie: normalmente `Button` secundario + primario. Una sola acción principal. */
31
+ actions?: ReactNode;
32
+ /** Contenido, como un formulario corto. */
33
+ children?: ReactNode;
34
+ /** Cierra con la X, Escape o un clic fuera. Sin él no hay X y el modal no se puede descartar. */
35
+ onClose?: () => void;
36
+ /**
37
+ * Ancho máximo.
38
+ * @default 480
39
+ */
40
+ width?: number | string;
41
+ /** Se pinta dentro de su contenedor (posición relativa) en vez de sobre toda la página, sin atrapar el foco. */
42
+ contained?: boolean;
43
+ }
44
+ /** Diálogo sobre la página para confirmar o completar una tarea corta sin salir de ella. Un modal, una decisión. */
45
+ export declare function Modal({ open, title, description, icon, color, iconVariant, destructive, layout, actions, children, onClose, width, contained, }: ModalProps): import("react").JSX.Element | null;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { cx } from "../internal/cx.js";
5
+ import { Layer } from "../internal/layer.js";
6
+ import { CloseButton } from "./CloseButton.js";
7
+ import { FeaturedIcon } from "./FeaturedIcon.js";
8
+ /** Diálogo sobre la página para confirmar o completar una tarea corta sin salir de ella. Un modal, una decisión. */
9
+ export function Modal({ open = true, title, description, icon, color, iconVariant = "light", destructive, layout = "stacked", actions, children, onClose, width, contained, }) {
10
+ const id = useId();
11
+ if (!open)
12
+ return null;
13
+ const horizontal = layout === "horizontal";
14
+ return (_jsxs(Layer, { contained: contained, onClose: onClose, overlayClassName: "items-center justify-center", panelClassName: "relative w-full max-w-[var(--width-sm)] rounded-2xl bg-primary_alt font-body shadow-xl", panelStyle: width !== undefined ? { maxWidth: width } : undefined, labelledBy: `${id}-title`, describedBy: description ? `${id}-desc` : undefined, children: [_jsxs("div", { className: cx("flex gap-4 px-6 pt-6", horizontal ? "flex-row" : "flex-col"), children: [icon ? (_jsx(FeaturedIcon, { icon: icon, color: color ?? (destructive ? "error" : "brand"), size: "lg", variant: iconVariant })) : null, _jsxs("div", { children: [_jsx("h2", { id: `${id}-title`, className: "m-0 text-lg font-semibold text-primary", children: title }), description ? (_jsx("p", { id: `${id}-desc`, className: "mt-1 mb-0 text-sm text-tertiary", children: description })) : null] })] }), children ? _jsx("div", { className: "px-6 pt-5", children: children }) : null, actions ? (_jsx("div", { className: cx("flex gap-3 px-6 pt-8 pb-6", horizontal ? "justify-end [&>*]:flex-none" : "[&>*]:flex-1"), children: actions })) : null, onClose ? _jsx(CloseButton, { onClick: onClose, className: "absolute top-4 right-4" }) : null] }));
15
+ }
@@ -0,0 +1,15 @@
1
+ import { type AlertProps } from "./Alert.js";
2
+ export interface NotificationProps extends Omit<AlertProps, "variant"> {
3
+ /**
4
+ * Milisegundos hasta cerrarse sola (llama a `onClose`), o `false` para que no se cierre. Solo cuenta si hay
5
+ * `onClose` y no hay `actions`; el tiempo se para mientras el ratón está encima o el foco dentro.
6
+ * Ampliación sobre la guía.
7
+ * @default 6000
8
+ */
9
+ autoClose?: number | false;
10
+ }
11
+ /**
12
+ * Aviso flotante y temporal (toast) tras una acción del usuario, de 320px de ancho. La app lo coloca abajo a la
13
+ * derecha, apilado; se cierra solo a los 6 s salvo que tenga acciones.
14
+ */
15
+ export declare function Notification({ autoClose, className, ...props }: NotificationProps): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { cx } from "../internal/cx.js";
5
+ import { ALERT_BASE, alertBody } from "./Alert.js";
6
+ /**
7
+ * Aviso flotante y temporal (toast) tras una acción del usuario, de 320px de ancho. La app lo coloca abajo a la
8
+ * derecha, apilado; se cierra solo a los 6 s salvo que tenga acciones.
9
+ */
10
+ export function Notification({ autoClose = 6000, className, ...props }) {
11
+ const { color = "gray", onClose, actions } = props;
12
+ const enabled = autoClose !== false && !!onClose && !actions;
13
+ const [hovered, setHovered] = useState(false);
14
+ const [focused, setFocused] = useState(false);
15
+ const paused = hovered || focused;
16
+ const remaining = useRef(autoClose || 0);
17
+ const close = useRef(onClose);
18
+ close.current = onClose;
19
+ useEffect(() => {
20
+ if (!enabled || paused)
21
+ return;
22
+ const start = Date.now();
23
+ const timer = setTimeout(() => close.current?.(), remaining.current);
24
+ return () => {
25
+ clearTimeout(timer);
26
+ remaining.current = Math.max(0, remaining.current - (Date.now() - start));
27
+ };
28
+ }, [enabled, paused]);
29
+ return (_jsx("div", { role: color === "error" ? "alert" : "status", onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onFocus: () => setFocused(true), onBlur: (e) => {
30
+ if (!e.currentTarget.contains(e.relatedTarget))
31
+ setFocused(false);
32
+ }, className: cx(ALERT_BASE, "w-full max-w-[var(--width-xxs)] rounded-xl shadow-[var(--shadow-lg),inset_0_0_0_1px_var(--border-secondary_alt)]", className), children: alertBody(props) }));
33
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+ export interface NowPlayingLabels {
3
+ /** @default "Suena ahora" */
4
+ playing: string;
5
+ /** @default "En pausa" */
6
+ paused: string;
7
+ /** Nombre de la barra. @default "Progreso de la canción" */
8
+ progress: string;
9
+ }
10
+ export interface NowPlayingProps {
11
+ title: ReactNode;
12
+ artist?: ReactNode;
13
+ /** URL de la carátula; sin ella, icono de volumen. */
14
+ cover?: string;
15
+ /** De 0 a 1. Sin él no hay barra. Avanza cuando llega un valor nuevo: no se anima por su cuenta. */
16
+ progress?: number;
17
+ paused?: boolean;
18
+ /** @default 360 */
19
+ width?: number | string;
20
+ /** Sustituye los textos (ampliación sobre la guía). */
21
+ labels?: Partial<NowPlayingLabels>;
22
+ className?: string;
23
+ }
24
+ /** Tarjeta de "suena ahora" con carátula, título, artista y una línea fina de progreso, siempre en oscuro. */
25
+ export declare function NowPlaying({ title, artist, cover, progress, paused, width, labels, className, }: NowPlayingProps): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../internal/cx.js";
3
+ import { STREAM_ROOT } from "../internal/stream.js";
4
+ import { defaultStrings } from "../internal/strings.js";
5
+ import { Icon } from "./Icon.js";
6
+ /** Tarjeta de "suena ahora" con carátula, título, artista y una línea fina de progreso, siempre en oscuro. */
7
+ export function NowPlaying({ title, artist, cover, progress, paused, width, labels, className, }) {
8
+ const t = {
9
+ playing: defaultStrings.nowPlaying,
10
+ paused: defaultStrings.paused,
11
+ progress: defaultStrings.songProgress,
12
+ ...labels,
13
+ };
14
+ const pct = progress == null ? null : Math.min(100, Math.max(0, progress * 100));
15
+ return (_jsxs("div", { className: cx(STREAM_ROOT, "relative flex max-w-full items-center gap-3 overflow-hidden rounded-xl border border-secondary bg-primary_alt p-3 pb-[15px]", width == null && "w-[360px]", className), style: width == null ? undefined : { width }, children: [_jsx("div", { className: "inline-flex size-14 flex-none items-center justify-center overflow-hidden rounded-lg bg-tertiary text-fg-secondary", children: cover ? (_jsx("img", { src: cover, alt: "", className: "size-full object-cover" })) : (_jsx(Icon, { name: "volume-max", size: 24 })) }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "block text-sm font-medium text-tertiary", children: paused ? t.paused : t.playing }), _jsx("span", { className: "truncate text-md font-semibold text-primary", children: title }), artist ? _jsx("span", { className: "truncate text-sm text-tertiary", children: artist }) : null] }), pct != null ? (_jsx("div", { role: "progressbar", "aria-label": t.progress, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(pct), className: "absolute inset-x-0 bottom-0 h-[3px] bg-quaternary", children: _jsx("div", { className: "h-full bg-brand-solid", style: { width: `${pct}%` } }) })) : null] }));
16
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ import { type BreadcrumbItem } from "./Breadcrumbs.js";
3
+ export interface PageHeaderProps {
4
+ /** Título de la página (`<h1>`), en Bricolage Grotesque 30/38 semibold. */
5
+ title: ReactNode;
6
+ /** Una frase en `text-tertiary` bajo el título. */
7
+ description?: ReactNode;
8
+ /** Ruta encima del título (`Breadcrumbs`). */
9
+ breadcrumbs?: BreadcrumbItem[];
10
+ /** Botones a la derecha: la acción principal, la última. */
11
+ actions?: ReactNode;
12
+ /** Lo que va bajo el título, como unas `Tabs`. */
13
+ children?: ReactNode;
14
+ /**
15
+ * Línea inferior en `border-secondary`.
16
+ * @default true
17
+ */
18
+ divider?: boolean;
19
+ className?: string;
20
+ }
21
+ /** Cabecera de página de app: migas, título, descripción, acciones y línea inferior. */
22
+ export declare function PageHeader({ title, description, breadcrumbs, actions, children, divider, className, }: PageHeaderProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../internal/cx.js";
3
+ import { Line } from "../internal/line.js";
4
+ import { Breadcrumbs } from "./Breadcrumbs.js";
5
+ /** Cabecera de página de app: migas, título, descripción, acciones y línea inferior. */
6
+ export function PageHeader({ title, description, breadcrumbs, actions, children, divider = true, className, }) {
7
+ return (_jsxs("header", { className: cx("flex flex-col gap-5 font-body", className), children: [breadcrumbs ? _jsx(Breadcrumbs, { items: breadcrumbs }) : null, _jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "m-0 font-display text-display-sm font-semibold text-primary", children: title }), description ? _jsx("p", { className: "mt-1 mb-0 text-md text-tertiary", children: description }) : null] }), actions ? _jsx("div", { className: "flex flex-wrap items-center gap-3", children: actions }) : null] }), children ?? null, divider ? _jsx(Line, {}) : null] }));
8
+ }
@@ -0,0 +1,29 @@
1
+ export interface PaginationLabels {
2
+ /** @default "Anterior" */
3
+ previous: string;
4
+ /** @default "Siguiente" */
5
+ next: string;
6
+ /** Nombre de la navegación. @default "Paginación" */
7
+ nav: string;
8
+ /** Nombre de cada número. @default (n) => `Página ${n}` */
9
+ page: (n: number) => string;
10
+ }
11
+ export interface PaginationProps {
12
+ /** Número de páginas. */
13
+ total: number;
14
+ /** Página actual, desde 1 (controlado). */
15
+ page?: number;
16
+ /**
17
+ * Página al principio (no controlado).
18
+ * @default 1
19
+ */
20
+ defaultPage?: number;
21
+ onChange?: (n: number) => void;
22
+ /** Textos que trae el componente, para sustituirlos. */
23
+ labels?: Partial<PaginationLabels>;
24
+ className?: string;
25
+ }
26
+ /** Números que se muestran: todos hasta 7 páginas; con más, recorta con "…". */
27
+ export declare function pageList(current: number, total: number): (number | "…")[];
28
+ /** Paginación con Anterior/Siguiente y números, separada de la tabla por una línea `border-secondary`. */
29
+ export declare function Pagination({ total, page, defaultPage, onChange, labels, className }: PaginationProps): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cx } from "../internal/cx.js";
4
+ import { defaultStrings } from "../internal/strings.js";
5
+ import { useControllable } from "../internal/use-controllable.js";
6
+ import { Button } from "./Button.js";
7
+ /** Números que se muestran: todos hasta 7 páginas; con más, recorta con "…". */
8
+ export function pageList(current, total) {
9
+ if (total <= 7)
10
+ return Array.from({ length: total }, (_, i) => i + 1);
11
+ if (current <= 3 || current >= total - 2)
12
+ return [1, 2, 3, "…", total - 2, total - 1, total];
13
+ return [1, "…", current - 1, current, current + 1, "…", total];
14
+ }
15
+ /** Paginación con Anterior/Siguiente y números, separada de la tabla por una línea `border-secondary`. */
16
+ export function Pagination({ total, page, defaultPage = 1, onChange, labels, className }) {
17
+ const [current, setCurrent] = useControllable(page, defaultPage);
18
+ const pages = Math.max(1, total);
19
+ const text = {
20
+ previous: defaultStrings.previous,
21
+ next: defaultStrings.next,
22
+ nav: defaultStrings.pagination,
23
+ page: (n) => `${defaultStrings.page} ${n}`,
24
+ ...labels,
25
+ };
26
+ const go = (n) => {
27
+ if (n < 1 || n > pages || n === current)
28
+ return;
29
+ setCurrent(n);
30
+ onChange?.(n);
31
+ };
32
+ return (_jsxs("nav", { "aria-label": text.nav, className: cx("flex items-center justify-between gap-3 pt-3 font-body shadow-[inset_0_1px_0_var(--border-secondary)]", className), children: [_jsx(Button, { hierarchy: "secondary", size: "sm", iconLeading: "arrow-left", disabled: current <= 1, onClick: () => go(current - 1), children: text.previous }), _jsx("div", { className: "flex gap-0.5", children: pageList(current, pages).map((n, i) => n === "…" ? (_jsx("span", { "aria-hidden": "true", className: "w-10 text-center text-sm leading-10 text-quaternary", children: "\u2026" }, `gap-${i}`)) : (_jsx("button", { type: "button", "aria-label": text.page(n), "aria-current": n === current ? "page" : undefined, onClick: () => go(n), className: "size-10 cursor-pointer rounded-lg border-0 bg-transparent font-body text-sm font-medium text-quaternary transition-colors duration-100 hover:bg-primary_hover hover:text-secondary focus-visible:shadow-focus-ring focus-visible:outline-none aria-[current=page]:bg-primary_hover aria-[current=page]:text-secondary", children: n }, n))) }), _jsx(Button, { hierarchy: "secondary", size: "sm", iconTrailing: "arrow-right", disabled: current >= pages, onClick: () => go(current + 1), children: text.next })] }));
33
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from "react";
2
+ export interface PieChartSlice {
3
+ name: string;
4
+ value: number;
5
+ /** Sin `color`, la paleta de series en orden. */
6
+ color?: string;
7
+ }
8
+ export interface PieChartProps {
9
+ /** Cinco porciones como mucho: agrupa el resto en "Otros". */
10
+ data: PieChartSlice[];
11
+ /** Lado en px. @default 200 */
12
+ size?: number;
13
+ /**
14
+ * Hueco del dónut, de 0 (tarta) a 0,9.
15
+ * @default 0.6
16
+ */
17
+ hole?: number;
18
+ /** Texto en el hueco, como el total. */
19
+ center?: ReactNode;
20
+ /** Formato del valor en el título de cada porción. */
21
+ format?: (v: number) => string;
22
+ /**
23
+ * Leyenda a la derecha: el color solo no basta.
24
+ * @default true
25
+ */
26
+ legend?: boolean;
27
+ /** Qué cuenta la gráfica, para lectores de pantalla. @default "Gráfica circular" */
28
+ "aria-label"?: string;
29
+ className?: string;
30
+ }
31
+ /** Gráfica circular o de dónut para el reparto de un total. */
32
+ export declare function PieChart({ data, size, hole, center, format, legend, className, ...rest }: PieChartProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { seriesColor } from "../internal/chart.js";
3
+ import { cx } from "../internal/cx.js";
4
+ import { ChartLegend } from "./ChartLegend.js";
5
+ /** Gráfica circular o de dónut para el reparto de un total. */
6
+ export function PieChart({ data, size = 200, hole = 0.6, center, format, legend = true, className, ...rest }) {
7
+ const r = size / 2;
8
+ const total = data.reduce((a, d) => a + d.value, 0) || 1;
9
+ const arc = (a, b) => {
10
+ const ri = r * hole;
11
+ const large = b - a > Math.PI ? 1 : 0;
12
+ const { cos, sin } = Math;
13
+ const outer = `M${r + r * cos(a)} ${r + r * sin(a)} A${r} ${r} 0 ${large} 1 ${r + r * cos(b)} ${r + r * sin(b)}`;
14
+ return ri
15
+ ? `${outer} L${r + ri * cos(b)} ${r + ri * sin(b)} A${ri} ${ri} 0 ${large} 0 ${r + ri * cos(a)} ${r + ri * sin(a)} Z`
16
+ : `${outer} L${r} ${r} Z`;
17
+ };
18
+ let a0 = -Math.PI / 2;
19
+ return (_jsxs("figure", { className: cx("m-0 flex flex-row items-center gap-4 font-body", className), children: [_jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, role: "img", "aria-label": rest["aria-label"] ?? "Gráfica circular", className: "flex-none overflow-visible", children: [data.map((d, i) => {
20
+ const step = (2 * Math.PI * d.value) / total;
21
+ const a = a0;
22
+ // Un hueco de 0,02 rad entre porciones
23
+ const b = a0 + step - (data.length > 1 ? 0.02 : 0);
24
+ a0 += step;
25
+ const color = seriesColor(i, d.color);
26
+ if (data.length === 1)
27
+ return (_jsx("circle", { cx: r, cy: r, r: (r * (1 + hole)) / 2, fill: "none", stroke: color, strokeWidth: r * (1 - hole), children: _jsx("title", { children: `${d.name}: ${format ? format(d.value) : d.value}` }) }, d.name));
28
+ return (_jsx("path", { d: arc(a, b), fill: color, children: _jsx("title", { children: `${d.name}: ${format ? format(d.value) : d.value}` }) }, i));
29
+ }), center ? (_jsx("text", { x: r, y: r + 8, textAnchor: "middle", fontSize: 24, fontWeight: 600, className: "font-display", fill: "var(--text-primary)", children: center })) : null] }), legend ? _jsx(ChartLegend, { vertical: true, series: data }) : null] }));
30
+ }
@@ -0,0 +1,11 @@
1
+ export interface ProgressBarProps {
2
+ /** De 0 a 100. */
3
+ value: number;
4
+ /** Porcentaje a la derecha (por defecto), debajo o sin texto. */
5
+ label?: "right" | "bottom" | false;
6
+ /** Qué progresa: "Presupuesto usado", "Subida". */
7
+ "aria-label"?: string;
8
+ className?: string;
9
+ }
10
+ /** Barra de progreso de 8px con el porcentaje a la derecha o debajo. */
11
+ export declare function ProgressBar({ value, label, "aria-label": ariaLabel, className, }: ProgressBarProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../internal/cx.js";
3
+ /** Barra de progreso de 8px con el porcentaje a la derecha o debajo. */
4
+ export function ProgressBar({ value, label = "right", "aria-label": ariaLabel, className, }) {
5
+ const v = Math.max(0, Math.min(100, value || 0));
6
+ const bottom = label === "bottom";
7
+ return (_jsxs("div", { className: cx("flex items-center gap-3 font-body text-sm font-medium text-secondary", bottom && "flex-col items-end gap-2", className), children: [_jsx("div", { role: "progressbar", "aria-valuenow": v, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": ariaLabel, className: cx("h-2 w-full overflow-hidden rounded-full bg-quaternary", bottom ? "flex-none" : "flex-1"), children: _jsx("div", { className: "h-full rounded-full bg-fg-brand-primary", style: { width: `${Math.max(v, 3)}%` } }) }), label ? _jsxs("span", { children: [v, "%"] }) : null] }));
8
+ }
@@ -0,0 +1,15 @@
1
+ import { type InputHTMLAttributes, type ReactNode } from "react";
2
+ import { type ChoiceSize } from "../internal/choice.js";
3
+ export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
4
+ /** Etiqueta junto al botón. */
5
+ label?: ReactNode;
6
+ /** Texto de apoyo bajo la etiqueta. */
7
+ hint?: ReactNode;
8
+ /**
9
+ * Círculo de 16px (`sm`) o 20px (`md`).
10
+ * @default "sm"
11
+ */
12
+ size?: ChoiceSize;
13
+ }
14
+ /** Botón de opción suelto, con `<input type="radio">` nativo. En grupo, usa `RadioGroup`. */
15
+ export declare function Radio({ label, hint, size, id: givenId, className, ...rest }: RadioProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { ChoiceText, choiceBox, choiceLabel, hiddenInput, radioDot, } from "../internal/choice.js";
4
+ import { cx } from "../internal/cx.js";
5
+ /** Botón de opción suelto, con `<input type="radio">` nativo. En grupo, usa `RadioGroup`. */
6
+ export function Radio({ label, hint, size = "sm", id: givenId, className, ...rest }) {
7
+ const autoId = useId();
8
+ const id = givenId ?? autoId;
9
+ return (_jsxs("label", { htmlFor: id, className: cx(choiceLabel(size), className), children: [_jsx("input", { id: id, type: "radio", ...rest, className: hiddenInput }), _jsx("span", { "aria-hidden": "true", className: cx(choiceBox(size, true), "peer-checked:bg-brand-solid peer-checked:shadow-none peer-focus-visible:shadow-focus-ring peer-disabled:opacity-50"), children: _jsx("span", { className: cx(radioDot(size), "hidden group-has-[:checked]:block") }) }), _jsx(ChoiceText, { label: label, hint: hint, size: size, className: "peer-disabled:opacity-50" })] }));
10
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+ import { type ChoiceSize } from "../internal/choice.js";
3
+ export interface RadioGroupOption {
4
+ value: string;
5
+ label: ReactNode;
6
+ hint?: ReactNode;
7
+ }
8
+ export interface RadioGroupProps {
9
+ /** Hasta cinco opciones; con más, `Select`. */
10
+ options: RadioGroupOption[];
11
+ value?: string;
12
+ defaultValue?: string;
13
+ onChange?: (value: string) => void;
14
+ /**
15
+ * Círculos de 16px (`sm`) o 20px (`md`).
16
+ * @default "sm"
17
+ */
18
+ size?: ChoiceSize;
19
+ /** Nombre del campo al enviar el formulario. */
20
+ name?: string;
21
+ /** Qué se elige, si no hay una etiqueta visible. */
22
+ "aria-label"?: string;
23
+ className?: string;
24
+ }
25
+ /** Grupo de opciones excluyentes con flechas para moverse entre ellas. */
26
+ export declare function RadioGroup({ options, value, defaultValue, onChange, size, name, "aria-label": ariaLabel, className, }: RadioGroupProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Radio as AriaRadio, RadioGroup as AriaRadioGroup } from "react-aria-components";
4
+ import { ChoiceText, choiceBox, choiceLabel, radioDot } from "../internal/choice.js";
5
+ import { cx } from "../internal/cx.js";
6
+ /** Grupo de opciones excluyentes con flechas para moverse entre ellas. */
7
+ export function RadioGroup({ options, value, defaultValue, onChange, size = "sm", name, "aria-label": ariaLabel, className, }) {
8
+ return (_jsx(AriaRadioGroup, { value: value, defaultValue: defaultValue, onChange: onChange, name: name, "aria-label": ariaLabel, className: cx("flex flex-col gap-3", className), children: options.map((o) => (_jsxs(AriaRadio, { value: o.value, className: choiceLabel(size), children: [_jsx("span", { "aria-hidden": "true", className: cx(choiceBox(size, true), "group-data-selected:bg-brand-solid group-data-selected:shadow-none group-data-focus-visible:shadow-focus-ring group-data-disabled:opacity-50"), children: _jsx("span", { className: cx(radioDot(size), "hidden group-data-selected:block") }) }), _jsx(ChoiceText, { label: o.label, hint: o.hint, size: size, className: "group-data-disabled:opacity-50" })] }, o.value))) }));
9
+ }
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from "react";
2
+ export interface SectionFooterProps {
3
+ /** A la izquierda: un enlace, un `ButtonGroup`… */
4
+ start?: ReactNode;
5
+ /** Botones a la derecha: la acción principal, la última. */
6
+ actions?: ReactNode;
7
+ /** Añade el padding de tarjeta (24px a los lados, 16px abajo). */
8
+ card?: boolean;
9
+ className?: string;
10
+ }
11
+ /** Pie de sección o de tarjeta con línea superior: un enlace o grupo a la izquierda y las acciones a la derecha. */
12
+ export declare function SectionFooter({ start, actions, card, className }: SectionFooterProps): import("react").JSX.Element;