@jotis-me/react 0.0.0-stage → 0.1.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 +13 -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 +18 -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 +15 -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 +100 -0
  175. package/dist/internal/strings.js +102 -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,60 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { cx } from "../internal/cx.js";
5
+ import { defaultStrings } from "../internal/strings.js";
6
+ import { Button } from "./Button.js";
7
+ import { CloseButton } from "./CloseButton.js";
8
+ import { FeaturedIcon } from "./FeaturedIcon.js";
9
+ import { Input } from "./Input.js";
10
+ const pad2 = (n) => String(n).padStart(2, "0");
11
+ const UNITS = ["días", "horas", "min", "seg"];
12
+ function Countdown({ to, now, brand }) {
13
+ const [tick, setTick] = useState(() => Date.now());
14
+ useEffect(() => {
15
+ if (now)
16
+ return;
17
+ const t = setInterval(() => setTick(Date.now()), 1000);
18
+ return () => clearInterval(t);
19
+ }, [now]);
20
+ const s = Math.max(0, Math.floor((to.getTime() - (now ? now.getTime() : tick)) / 1000));
21
+ const parts = [
22
+ Math.floor(s / 86400),
23
+ Math.floor((s % 86400) / 3600),
24
+ Math.floor((s % 3600) / 60),
25
+ s % 60,
26
+ ];
27
+ return (_jsx("div", { role: "timer", "aria-label": parts.map((n, i) => `${n} ${UNITS[i]}`).join(", "), className: "flex shrink-0 gap-2", children: parts.map((n, i) => (_jsxs("div", { "aria-hidden": "true", className: cx("flex min-w-12 flex-col items-center rounded-lg px-1.5 py-1", brand
28
+ ? "bg-fg-white/10 shadow-[inset_0_0_0_1px_var(--border-brand_alt)]"
29
+ : "bg-primary shadow-[inset_0_0_0_1px_var(--border-secondary)]"), children: [_jsx("span", { className: cx("text-lg font-semibold tabular-nums", brand ? "text-primary_on-brand" : "text-primary"), children: pad2(n) }), _jsx("span", { className: cx("text-[11px] leading-4 font-medium",
30
+ // En brand, blanco y no tertiary_on-brand: sobre la celda este daba 4,4:1 (decisión del usuario)
31
+ brand ? "text-primary_on-brand" : "text-tertiary"), children: UNITS[i] })] }, UNITS[i]))) }));
32
+ }
33
+ /**
34
+ * Aviso de ancho de página o flotante para anunciar algo a toda la app: novedades, mantenimiento, captación de
35
+ * correo, cuenta atrás. Uno a la vez. En móvil se apila y oculta el icono.
36
+ */
37
+ export function Banner({ title, description, icon, actions, email, countdown, now, theme = "default", layout = "floating", slim, onClose, closeLabel, className, ...rest }) {
38
+ const brand = theme === "brand";
39
+ const [value, setValue] = useState("");
40
+ const mail = email
41
+ ? { placeholder: defaultStrings.yourEmail, buttonLabel: defaultStrings.subscribe, ...email }
42
+ : null;
43
+ return (_jsx("section", { "aria-label": rest["aria-label"] ?? (typeof title === "string" ? title : defaultStrings.notice), className: cx("font-body", brand ? "bg-brand-section_subtle" : "bg-secondary", layout === "floating"
44
+ ? cx("rounded-xl", brand
45
+ ? "shadow-[inset_0_0_0_1px_var(--border-brand_alt),var(--shadow-lg)]"
46
+ : "shadow-[inset_0_0_0_1px_var(--border-secondary_alt),var(--shadow-lg)]")
47
+ : brand
48
+ ? "shadow-[inset_0_-1px_0_var(--border-brand_alt)]"
49
+ : "shadow-[inset_0_-1px_0_var(--border-secondary_alt)]", className), children: _jsxs("div", { className: cx("relative flex items-center gap-4 max-[720px]:flex-wrap",
50
+ // En móvil el banner se apila (el estrecho conserva su relleno y su X centrada, como en la guía)
51
+ !slim && "max-[720px]:p-4", layout === "full"
52
+ ? "mx-auto max-w-[var(--container-max-width-desktop)] px-8 py-3"
53
+ : "p-3", slim && "justify-center px-4 py-3"), children: [icon && !slim ? (_jsx(FeaturedIcon, { icon: icon, variant: brand ? "light" : "modern", size: "lg", color: brand ? "brand" : "gray", className: "max-[720px]:hidden" })) : null, _jsxs("div", { className: cx("flex min-w-0 flex-wrap gap-x-1.5 text-sm max-[720px]:basis-[calc(100%-48px)]", slim ? "flex-initial" : "flex-1"), children: [_jsx("span", { className: cx("font-semibold", brand ? "text-primary_on-brand" : "text-secondary"), children: title }), description ? (_jsx("span", { className: brand ? "text-tertiary_on-brand" : "text-tertiary", children: description })) : null] }), countdown ? _jsx(Countdown, { to: countdown, now: now, brand: brand }) : null, mail ? (_jsxs("form", { className: "flex w-[360px] max-w-full shrink-0 flex-wrap gap-3 max-[720px]:w-full", onSubmit: (e) => {
54
+ e.preventDefault();
55
+ mail.onSubmit?.(value);
56
+ }, children: [_jsx(Input, { type: "email", required: true, size: "sm", placeholder: mail.placeholder, "aria-label": mail.placeholder, value: value, onChange: (e) => setValue(e.target.value), className: "min-w-0 flex-1" }), _jsx(Button, { type: "submit", size: "sm", hierarchy: brand ? "secondary" : "primary", children: mail.buttonLabel }), mail.hint ? (_jsx("p", { className: cx("m-0 w-full text-xs", brand ? "text-tertiary_on-brand" : "text-tertiary"), children: mail.hint })) : null] })) : null, actions ? _jsx("div", { className: "flex shrink-0 gap-3 max-[720px]:w-full", children: actions }) : null, onClose ? (_jsx(CloseButton, { label: closeLabel ?? defaultStrings.closeNotice, onClick: onClose, className: cx("shrink-0", slim
57
+ ? "absolute top-1/2 right-2 -translate-y-1/2"
58
+ : "max-[720px]:absolute max-[720px]:top-2 max-[720px]:right-2", brand &&
59
+ "text-tertiary_on-brand hover:bg-fg-white/10 hover:text-primary_on-brand") })) : null] }) }));
60
+ }
@@ -0,0 +1,25 @@
1
+ import { type ChartSeries } from "../internal/chart.js";
2
+ export interface BarChartProps {
3
+ /** Etiquetas del eje horizontal, una por barra. */
4
+ labels: string[];
5
+ /** Sin `color`, la paleta de series en orden (como `LineChart`). */
6
+ series: ChartSeries[];
7
+ /**
8
+ * Barras apiladas; `false` para agrupadas.
9
+ * @default true
10
+ */
11
+ stacked?: boolean;
12
+ /** Formato de los valores en el eje y en el título de cada barra. */
13
+ format?: (v: number) => string;
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 barras" */
21
+ "aria-label"?: string;
22
+ className?: string;
23
+ }
24
+ /** Gráfica de barras, apiladas por defecto, para comparar cantidades por periodo o categoría. */
25
+ export declare function BarChart({ labels, series, stacked, format, legend, width, height, className, ...rest }: BarChartProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
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 barras, apiladas por defecto, para comparar cantidades por periodo o categoría. */
6
+ export function BarChart({ labels, series, stacked = true, format, legend, width = 640, height = 240, className, ...rest }) {
7
+ const plot = { w: width, h: height, pad: PAD };
8
+ const totals = labels.map((_, i) => stacked
9
+ ? series.reduce((a, s) => a + (s.data[i] ?? 0), 0)
10
+ : Math.max(0, ...series.map((s) => s.data[i] ?? 0)));
11
+ const top = nice(Math.max(0, ...totals));
12
+ const iw = width - PAD.l - PAD.r;
13
+ const ih = height - PAD.t - PAD.b;
14
+ const slot = iw / labels.length;
15
+ const bw = Math.min(32, slot * 0.6);
16
+ 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 barras", className: "h-auto w-full overflow-visible", children: [_jsx(Axes, { plot: plot, top: top, labels: labels, format: format }), labels.map((label, i) => {
17
+ let acc = 0;
18
+ const x0 = PAD.l + slot * i + (slot - bw) / 2;
19
+ return (_jsx("g", { children: series.map((s, si) => {
20
+ const v = s.data[i] ?? 0;
21
+ const bh = (ih * v) / top;
22
+ const x = stacked ? x0 : x0 + si * (bw / series.length);
23
+ const w = stacked ? bw : bw / series.length - 2;
24
+ const y = PAD.t + ih - (stacked ? acc + bh : bh);
25
+ if (stacked)
26
+ acc += bh;
27
+ return (_jsx("rect", { x: x, y: y, width: w, height: Math.max(bh, 0), rx: stacked && si < series.length - 1 ? 0 : 4, fill: seriesColor(si, s.color), children: _jsx("title", { children: pointTitle(s.name, label, format ? format(v) : v) }) }, si));
28
+ }) }, i));
29
+ })] })] }));
30
+ }
@@ -0,0 +1,19 @@
1
+ import { type IconName } from "./Icon.js";
2
+ export interface BreadcrumbItem {
3
+ label: string;
4
+ href?: string;
5
+ /** Icono en lugar del texto (el texto queda para lectores de pantalla). Pensado para el primero, `home-line`. */
6
+ icon?: IconName;
7
+ }
8
+ export interface BreadcrumbsProps {
9
+ /** Ruta desde el inicio. El último es la página actual: se pinta en `text-brand-secondary` y sin enlace. */
10
+ items: BreadcrumbItem[];
11
+ /**
12
+ * Nombre de la navegación para lectores de pantalla.
13
+ * @default "Migas de pan"
14
+ */
15
+ "aria-label"?: string;
16
+ className?: string;
17
+ }
18
+ /** Ruta de navegación encima del título de página. */
19
+ export declare function Breadcrumbs({ items, className, ...rest }: BreadcrumbsProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from "react";
3
+ import { cx } from "../internal/cx.js";
4
+ import { defaultStrings } from "../internal/strings.js";
5
+ import { Icon } from "./Icon.js";
6
+ const CRUMB = "inline-flex items-center rounded-md px-2 py-1";
7
+ /** Ruta de navegación encima del título de página. */
8
+ export function Breadcrumbs({ items, className, ...rest }) {
9
+ return (_jsx("nav", { "aria-label": rest["aria-label"] ?? defaultStrings.breadcrumbs, className: className, children: _jsx("ol", { className: "m-0 flex list-none items-center gap-2 p-0 font-body text-sm font-semibold text-quaternary [&_svg]:text-fg-quaternary", children: items.map((item, i) => {
10
+ const last = i === items.length - 1;
11
+ const content = item.icon && !last ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: item.icon, size: 20 }), _jsx("span", { className: "sr-only", children: item.label })] })) : (item.label);
12
+ return (_jsxs(Fragment, { children: [i > 0 ? (_jsx("li", { "aria-hidden": "true", className: "flex", children: _jsx(Icon, { name: "chevron-right", size: 16 }) })) : null, _jsx("li", { className: "flex", children: last ? (_jsx("span", { "aria-current": "page", className: cx(CRUMB, "text-brand-secondary"), children: item.label })) : item.href ? (_jsx("a", { href: item.href, className: cx(CRUMB, "text-inherit no-underline transition-colors duration-100 hover:bg-primary_hover hover:text-tertiary_hover focus-visible:shadow-focus-ring focus-visible:outline-none"), children: content })) : (_jsx("span", { className: CRUMB, children: content })) })] }, i));
13
+ }) }) }));
14
+ }
@@ -0,0 +1,18 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ export interface BrowserFrameProps {
3
+ /**
4
+ * `safari` semáforo, navegación, dirección y acciones · `minimal` semáforo gris y dirección.
5
+ * @default "safari"
6
+ */
7
+ variant?: "safari" | "minimal";
8
+ /** Dirección que se muestra en la barra. @default "jotis.me" */
9
+ url?: string;
10
+ /** Alto del área de contenido. */
11
+ height?: number | string;
12
+ /** La pantalla: componentes reales, no capturas, para que cambien con el tema. */
13
+ children?: ReactNode;
14
+ className?: string;
15
+ style?: CSSProperties;
16
+ }
17
+ /** Marco de navegador para enseñar una pantalla de la app dentro de una página. Decorativo salvo su contenido. */
18
+ export declare function BrowserFrame({ variant, url, height, children, className, style, }: BrowserFrameProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../internal/cx.js";
3
+ import { Icon } from "./Icon.js";
4
+ /** Marco de navegador para enseñar una pantalla de la app dentro de una página. Decorativo salvo su contenido. */
5
+ export function BrowserFrame({ variant = "safari", url = "jotis.me", height, children, className, style, }) {
6
+ const minimal = variant === "minimal";
7
+ const dot = cx("size-3 rounded-full", minimal ? "bg-[var(--border-primary)]" : "shadow-[inset_0_0_0_0.5px_rgba(0,0,0,0.12)]");
8
+ const address = (_jsxs("div", { className: cx("mx-auto flex min-w-0 flex-[0_1_516px] items-center justify-center gap-1.5 rounded-lg px-3 font-medium text-tertiary", minimal
9
+ ? "h-6 bg-primary text-xs shadow-[inset_0_0_0_1px_var(--border-secondary)]"
10
+ : "h-7 bg-secondary text-[13px] leading-[18px]"), children: [_jsx(Icon, { name: "lock-01", size: 12 }), _jsx("span", { className: "truncate", children: url })] }));
11
+ const nav = "flex shrink-0 items-center gap-3 max-[560px]:hidden";
12
+ return (_jsxs("div", { className: cx("overflow-hidden rounded-xl border border-secondary bg-primary font-body shadow-xl", className), style: style, children: [_jsxs("div", { className: cx("flex items-center gap-4 px-4 text-fg-quaternary shadow-[inset_0_-1px_0_var(--border-secondary)]", minimal ? "h-9 bg-secondary" : "h-11 bg-primary"), children: [_jsxs("div", { "aria-hidden": "true", className: "flex shrink-0 gap-2", children: [_jsx("span", { className: cx(dot, !minimal && "bg-[#ee6a5f]") }), _jsx("span", { className: cx(dot, !minimal && "bg-[#f5bd4f]") }), _jsx("span", { className: cx(dot, !minimal && "bg-[#61c454]") })] }), minimal ? (_jsxs(_Fragment, { children: [address, _jsx("span", { className: "w-[52px] shrink-0" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: nav, children: [_jsx(Icon, { name: "chevron-left", size: 18 }), _jsx(Icon, { name: "chevron-right", size: 18 })] }), address, _jsxs("div", { className: cx(nav, "ml-auto"), children: [_jsx(Icon, { name: "refresh-cw-01", size: 16 }), _jsx(Icon, { name: "plus", size: 18 }), _jsx(Icon, { name: "copy-01", size: 16 })] })] }))] }), _jsx("div", { className: "relative overflow-auto", style: height !== undefined ? { height } : undefined, children: children })] }));
13
+ }
@@ -0,0 +1,51 @@
1
+ import { type AnchorHTMLAttributes, type ButtonHTMLAttributes, type ReactNode } from "react";
2
+ import { type IconName } from "./Icon.js";
3
+ export type ButtonHierarchy = "primary" | "secondary" | "tertiary" | "link-color" | "link-gray";
4
+ export type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
5
+ interface ButtonBase {
6
+ /**
7
+ * Jerarquía: una sola `primary` por vista o modal.
8
+ * @default "primary"
9
+ */
10
+ hierarchy?: ButtonHierarchy;
11
+ /**
12
+ * Alto: `xs` 32 · `sm` 36 · `md` 40 · `lg` 44 · `xl` 48.
13
+ * @default "md"
14
+ */
15
+ size?: ButtonSize;
16
+ /** Acción destructiva. Con `primary`, solo cuando borrar es la acción principal. */
17
+ destructive?: boolean;
18
+ /** Muestra un spinner, marca `aria-busy` y desactiva el botón. */
19
+ loading?: boolean;
20
+ className?: string;
21
+ }
22
+ interface WithText extends ButtonBase {
23
+ /** Icono antes del texto: nombre del set o un nodo. */
24
+ iconLeading?: IconName | ReactNode;
25
+ /** Icono después del texto: nombre del set o un nodo. */
26
+ iconTrailing?: IconName | ReactNode;
27
+ iconOnly?: undefined;
28
+ label?: string;
29
+ children?: ReactNode;
30
+ }
31
+ interface IconOnly extends ButtonBase {
32
+ /** Botón solo con icono. Exige `label` para lectores de pantalla. */
33
+ iconOnly: IconName;
34
+ /** Nombre accesible del botón solo con icono. */
35
+ label: string;
36
+ iconLeading?: undefined;
37
+ iconTrailing?: undefined;
38
+ children?: undefined;
39
+ }
40
+ type AsButton = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "className"> & {
41
+ href?: undefined;
42
+ };
43
+ type AsLink = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "children" | "className"> & {
44
+ /** Con `href` se pinta como enlace `<a>` con el mismo aspecto. */
45
+ href: string;
46
+ disabled?: boolean;
47
+ };
48
+ export type ButtonProps = (WithText | IconOnly) & (AsButton | AsLink);
49
+ /** La acción de la pantalla: cinco jerarquías y cinco tamaños. Acepta `ref` (al `<button>` o al `<a>`). */
50
+ export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLAnchorElement | HTMLButtonElement>>;
51
+ export {};
@@ -0,0 +1,54 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, } from "react";
3
+ import { cx } from "../internal/cx.js";
4
+ import { Icon, iconSlot } from "./Icon.js";
5
+ const BASE = "relative inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-lg font-body text-sm font-semibold no-underline cursor-pointer transition-colors duration-100 ease-linear focus-visible:outline-none focus-visible:shadow-focus-ring disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50";
6
+ const SIZE = {
7
+ xs: "h-8 px-2.5 py-1.5",
8
+ sm: "h-9 px-3 py-2",
9
+ md: "h-10 px-3.5 py-2.5",
10
+ lg: "h-11 px-4 py-2.5 gap-1.5 text-md",
11
+ xl: "h-12 px-4.5 py-3 gap-1.5 text-md",
12
+ };
13
+ const ICON_ONLY = {
14
+ xs: "w-8 p-1.5",
15
+ sm: "w-9 p-2",
16
+ md: "w-10 p-2.5",
17
+ lg: "w-11 p-3",
18
+ xl: "w-12 p-3.5",
19
+ };
20
+ const SKEUO_BORDER = "shadow-[var(--shadow-xs-skeuomorphic),inset_0_0_0_1px_var(--border-primary)]";
21
+ const SKEUO_ERROR_BORDER = "shadow-[var(--shadow-xs-skeuomorphic),inset_0_0_0_1px_var(--border-error_subtle)]";
22
+ // Borde interior del primario: degradado blanco del 12 % a 0 (ficha de Button)
23
+ const HIGHLIGHT =
24
+ // color-fijo: brillo interior del botón primario, blanco 12 % → 0 en ambos temas (ficha de Button)
25
+ "before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:p-0.5 before:content-[''] before:bg-[linear-gradient(180deg,#ffffff1f,#ffffff00)] before:[mask:linear-gradient(black_0_0)_content-box_exclude,linear-gradient(black_0_0)]";
26
+ const HIERARCHY = {
27
+ primary: `bg-brand-solid text-white shadow-xs-skeuomorphic not-disabled:hover:bg-brand-solid_hover ${HIGHLIGHT}`,
28
+ secondary: `bg-primary text-secondary ${SKEUO_BORDER} not-disabled:hover:bg-primary_hover not-disabled:hover:text-secondary_hover [&_svg]:text-fg-quaternary`,
29
+ tertiary: "bg-transparent text-tertiary not-disabled:hover:bg-primary_hover not-disabled:hover:text-tertiary_hover [&_svg]:text-fg-quaternary",
30
+ "link-color": "h-auto p-0 rounded-sm bg-transparent text-brand-secondary not-disabled:hover:text-brand-secondary_hover not-disabled:hover:underline underline-offset-3",
31
+ "link-gray": "h-auto p-0 rounded-sm bg-transparent text-tertiary not-disabled:hover:text-tertiary_hover not-disabled:hover:underline underline-offset-3",
32
+ };
33
+ const DESTRUCTIVE = {
34
+ primary: "bg-error-solid not-disabled:hover:bg-error-solid_hover",
35
+ secondary: `text-error-primary ${SKEUO_ERROR_BORDER} not-disabled:hover:bg-error-primary not-disabled:hover:text-error-primary_hover [&_svg]:text-fg-error-secondary`,
36
+ tertiary: "text-error-primary not-disabled:hover:bg-error-primary not-disabled:hover:text-error-primary_hover [&_svg]:text-fg-error-secondary",
37
+ "link-color": "text-error-primary",
38
+ "link-gray": "",
39
+ };
40
+ /** La acción de la pantalla: cinco jerarquías y cinco tamaños. Acepta `ref` (al `<button>` o al `<a>`). */
41
+ export const Button = forwardRef(function Button({ hierarchy = "primary", size = "md", destructive, loading, iconLeading, iconTrailing, iconOnly, label, children, className, ...rest }, ref) {
42
+ const link = hierarchy.startsWith("link");
43
+ const iconSize = size === "xs" ? 16 : 20;
44
+ const classes = cx(BASE, !link && SIZE[size], iconOnly && !link && ICON_ONLY[size], HIERARCHY[hierarchy], destructive && DESTRUCTIVE[hierarchy], destructive && "focus-visible:shadow-focus-ring-error", className);
45
+ const spinner = _jsx(Icon, { name: "loading-02", size: iconSize, spin: true });
46
+ const content = iconOnly ? (_jsxs(_Fragment, { children: [loading ? spinner : _jsx(Icon, { name: iconOnly, size: iconSize }), _jsx("span", { className: "sr-only", children: label })] })) : (_jsxs(_Fragment, { children: [loading ? spinner : iconSlot(iconLeading, iconSize), _jsx("span", { className: link ? undefined : "px-0.5", children: children }), iconSlot(iconTrailing, iconSize)] }));
47
+ if ("href" in rest && rest.href !== undefined) {
48
+ const { disabled, ...anchor } = rest;
49
+ const inactive = disabled || loading;
50
+ return (_jsx("a", { ...anchor, ref: ref, className: classes, "aria-busy": loading || undefined, "aria-disabled": inactive || undefined, tabIndex: inactive ? -1 : anchor.tabIndex, href: inactive ? undefined : anchor.href, children: content }));
51
+ }
52
+ const button = rest;
53
+ return (_jsx("button", { type: "button", ...button, ref: ref, className: classes, "aria-busy": loading || undefined, disabled: button.disabled || loading || undefined, children: content }));
54
+ });
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+ import { type IconName } from "./Icon.js";
3
+ export interface ButtonGroupItem {
4
+ value: string;
5
+ label?: ReactNode;
6
+ icon?: IconName;
7
+ }
8
+ export interface ButtonGroupProps {
9
+ /** De 2 a 5 opciones cortas. */
10
+ items: ButtonGroupItem[];
11
+ /** Opción elegida (controlado). */
12
+ value?: string;
13
+ /** Opción elegida al principio (no controlado). */
14
+ defaultValue?: string;
15
+ onChange?: (value: string) => void;
16
+ /** Nombre del grupo para lectores de pantalla. */
17
+ "aria-label"?: string;
18
+ className?: string;
19
+ }
20
+ /** Botones secundarios unidos para elegir una opción entre pocas. El elegido lleva `aria-pressed`. */
21
+ export declare function ButtonGroup({ items, value, defaultValue, onChange, className, ...rest }: ButtonGroupProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cx } from "../internal/cx.js";
4
+ import { useControllable } from "../internal/use-controllable.js";
5
+ import { iconSlot } from "./Icon.js";
6
+ /** Botones secundarios unidos para elegir una opción entre pocas. El elegido lleva `aria-pressed`. */
7
+ export function ButtonGroup({ items, value, defaultValue, onChange, className, ...rest }) {
8
+ const [current, setCurrent] = useControllable(value, defaultValue);
9
+ const select = (next) => {
10
+ setCurrent(next);
11
+ onChange?.(next);
12
+ };
13
+ return (_jsx("fieldset", { "aria-label": rest["aria-label"], className: cx("m-0 inline-flex min-w-0 overflow-hidden border-0 p-0 rounded-lg shadow-[var(--shadow-xs-skeuomorphic),inset_0_0_0_1px_var(--border-primary)]", className), children: items.map((item) => (_jsxs("button", { type: "button", "aria-pressed": current === item.value, onClick: () => select(item.value), className: "inline-flex h-10 cursor-pointer items-center gap-2 border-l border-primary bg-primary px-4 py-2.5 font-body text-sm font-semibold text-secondary transition-colors duration-100 first:border-l-0 hover:bg-primary_hover hover:text-secondary_hover focus-visible:relative focus-visible:z-10 focus-visible:shadow-focus-ring focus-visible:outline-none aria-pressed:bg-secondary_hover aria-pressed:text-secondary_hover [&_svg]:text-fg-quaternary", children: [iconSlot(item.icon, 20), item.label] }, item.value))) }));
14
+ }
@@ -0,0 +1,30 @@
1
+ import { type ReactNode } from "react";
2
+ import { type BadgeColor } from "./Badge.js";
3
+ export interface CalendarEvent {
4
+ date: Date;
5
+ title: string;
6
+ /** Hora o importe a la derecha del título. */
7
+ time?: string;
8
+ /** Colores de `Badge`. @default "gray" */
9
+ color?: BadgeColor;
10
+ }
11
+ export interface CalendarViewProps {
12
+ events?: CalendarEvent[];
13
+ /** El día que se marca como hoy (por defecto, la fecha del sistema). */
14
+ today?: Date;
15
+ /** Mes que se ve al principio. Por defecto, el de `today`. */
16
+ month?: Date;
17
+ /**
18
+ * Eventos por día; el resto se resume como "N más…".
19
+ * @default 3
20
+ */
21
+ maxEvents?: number;
22
+ /** Al pulsar un día (con ratón, o con teclado sobre su número). */
23
+ onSelectDate?: (date: Date) => void;
24
+ subtitle?: ReactNode;
25
+ /** Acciones a la derecha de la cabecera, como "Nuevo evento". */
26
+ actions?: ReactNode;
27
+ className?: string;
28
+ }
29
+ /** Calendario mensual con eventos en cada día, cabecera con la fecha de hoy y navegación. Solo vista de mes. */
30
+ export declare function CalendarView({ events, today: todayProp, month, maxEvents, onSelectDate, subtitle, actions, className, }: CalendarViewProps): import("react").JSX.Element;
@@ -0,0 +1,43 @@
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 { formatMonth } from "../internal/format.js";
6
+ import { defaultStrings } from "../internal/strings.js";
7
+ import { COLOR } from "./Badge.js";
8
+ import { Icon } from "./Icon.js";
9
+ const day0 = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
10
+ const same = (a, b) => a.getTime() === b.getTime();
11
+ const NAV = "inline-flex h-10 cursor-pointer items-center gap-2 border-0 border-l border-primary bg-primary px-3 py-2.5 font-body text-sm font-semibold text-secondary first:border-l-0 hover:bg-primary_hover hover:text-secondary_hover focus-visible:relative focus-visible:z-10 focus-visible:shadow-focus-ring focus-visible:outline-none [&_svg]:text-fg-quaternary";
12
+ const longDate = (d) => new Intl.DateTimeFormat("es-ES", { weekday: "long", day: "numeric", month: "long" }).format(d);
13
+ /** Calendario mensual con eventos en cada día, cabecera con la fecha de hoy y navegación. Solo vista de mes. */
14
+ export function CalendarView({ events = [], today: todayProp, month, maxEvents = 3, onSelectDate, subtitle, actions, className, }) {
15
+ const today = day0(todayProp ?? new Date());
16
+ const startMonth = month ?? today;
17
+ const [shown, setShown] = useState(() => new Date(startMonth.getFullYear(), startMonth.getMonth(), 1));
18
+ const y = shown.getFullYear();
19
+ const mo = shown.getMonth();
20
+ // La semana empieza en lunes
21
+ const lead = (new Date(y, mo, 1).getDay() + 6) % 7;
22
+ const weeks = Math.ceil((lead + new Date(y, mo + 1, 0).getDate()) / 7);
23
+ const cells = Array.from({ length: weeks * 7 }, (_, i) => new Date(y, mo, 1 - lead + i));
24
+ const shortMonth = new Intl.DateTimeFormat("es-ES", { month: "short" })
25
+ .format(today)
26
+ .replace(".", "")
27
+ .toUpperCase();
28
+ return (_jsxs("div", { className: cx("overflow-hidden rounded-xl bg-primary font-body shadow-[var(--shadow-xs),inset_0_0_0_1px_var(--border-secondary)]", className), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4 px-6 py-5 shadow-[inset_0_-1px_0_var(--border-secondary)]", children: [_jsxs("div", { "aria-hidden": "true", className: "flex w-16 flex-col overflow-hidden rounded-lg text-center shadow-[inset_0_0_0_1px_var(--border-secondary)]", children: [_jsx("span", { className: "bg-secondary py-1 text-xs font-semibold text-quaternary", children: shortMonth }), _jsx("strong", { className: "pt-0.5 pb-1 text-lg font-bold text-brand-secondary", children: today.getDate() })] }), _jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "m-0 text-lg font-semibold text-primary", "aria-live": "polite", children: formatMonth(shown) }), subtitle ? _jsx("p", { className: "m-0 text-sm text-tertiary", children: subtitle }) : null] }), _jsxs("div", { className: "inline-flex overflow-hidden rounded-lg shadow-[var(--shadow-xs-skeuomorphic),inset_0_0_0_1px_var(--border-primary)]", children: [_jsx("button", { type: "button", "aria-label": defaultStrings.previousMonth, className: NAV, onClick: () => setShown(new Date(y, mo - 1, 1)), children: _jsx(Icon, { name: "chevron-left", size: 20 }) }), _jsx("button", { type: "button", className: NAV, onClick: () => setShown(new Date(today.getFullYear(), today.getMonth(), 1)), children: defaultStrings.today }), _jsx("button", { type: "button", "aria-label": defaultStrings.nextMonth, className: NAV, onClick: () => setShown(new Date(y, mo + 1, 1)), children: _jsx(Icon, { name: "chevron-right", size: 20 }) })] }), actions ?? null] }), _jsxs("table", { className: "w-full table-fixed border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: defaultStrings.weekdaysMin.map((d) => (_jsx("th", { scope: "col", className: "bg-secondary p-2 text-center text-xs font-semibold text-tertiary shadow-[inset_0_-1px_0_var(--border-secondary)]", children: d }, d))) }) }), _jsx("tbody", { children: Array.from({ length: weeks }, (_, w) => (_jsx("tr", { children: cells.slice(w * 7, w * 7 + 7).map((c) => {
29
+ const list = events.filter((e) => same(day0(e.date), c));
30
+ const out = c.getMonth() !== mo;
31
+ const isToday = same(c, today);
32
+ const label = `${longDate(c)}${isToday ? `, ${defaultStrings.today.toLowerCase()}` : ""}`;
33
+ const number = cx("inline-flex size-6 items-center justify-center rounded-full text-xs", isToday
34
+ ? "bg-brand-solid font-semibold text-white"
35
+ : out
36
+ ? "font-medium text-quaternary"
37
+ : "font-semibold text-secondary");
38
+ return (_jsx("td", { onClick: onSelectDate ? () => onSelectDate(c) : undefined, className: cx("h-28 min-w-0 p-2 align-top shadow-[inset_-1px_-1px_0_var(--border-secondary)]", out ? "bg-secondary" : "bg-primary", onSelectDate && "cursor-pointer"), children: _jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [onSelectDate ? (_jsx("button", { type: "button", "aria-label": label, onClick: (e) => {
39
+ e.stopPropagation();
40
+ onSelectDate(c);
41
+ }, className: cx(number, "cursor-pointer border-0 p-0 focus-visible:shadow-focus-ring focus-visible:outline-none", !isToday && "bg-transparent"), children: c.getDate() })) : (_jsxs("span", { className: number, children: [_jsx("span", { "aria-hidden": "true", children: c.getDate() }), _jsx("span", { className: "sr-only", children: label })] })), list.slice(0, maxEvents).map((e, k) => (_jsxs("div", { title: e.title, className: cx("flex min-w-0 justify-between gap-1 rounded-md px-2 py-1 text-xs font-semibold inset-ring", COLOR[e.color ?? "gray"].badge), children: [_jsx("span", { className: "truncate", children: e.title }), e.time ? (_jsx("span", { className: "flex-none font-normal", children: e.time })) : null] }, k))), list.length > maxEvents ? (_jsx("div", { className: "px-2 text-xs font-semibold text-quaternary", children: defaultStrings.showMore(list.length - maxEvents) })) : null] }) }, c.getTime()));
42
+ }) }, w))) })] })] }));
43
+ }
@@ -0,0 +1,21 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ export type CamFrameRatio = "16:9" | "4:3" | "1:1";
3
+ export interface CamFrameProps {
4
+ /** @default "16:9" */
5
+ ratio?: CamFrameRatio;
6
+ /** @default 480 */
7
+ width?: number | string;
8
+ /** Añade el punto verde y la palabra "En directo": nunca solo el color. */
9
+ live?: boolean;
10
+ name?: ReactNode;
11
+ /** Pronombres o cargo, junto al nombre. */
12
+ role?: ReactNode;
13
+ /** Solo para maquetar: en directo no se pasa nada y OBS pone la cámara debajo. */
14
+ children?: ReactNode;
15
+ /** Sustituye a "En directo" (ampliación sobre la guía). */
16
+ liveLabel?: string;
17
+ className?: string;
18
+ style?: CSSProperties;
19
+ }
20
+ /** Marco de cámara con el interior vacío (transparente) y etiqueta de nombre y estado, siempre en oscuro. */
21
+ export declare function CamFrame({ ratio, width, live, name, role, children, liveLabel, className, style, }: CamFrameProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
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
+ /** Marco de cámara con el interior vacío (transparente) y etiqueta de nombre y estado, siempre en oscuro. */
6
+ export function CamFrame({ ratio = "16:9", width, live = false, name, role, children, liveLabel, className, style, }) {
7
+ const [w, h] = ratio.split(":");
8
+ return (_jsxs("div", { className: cx(STREAM_ROOT,
9
+ // Sin fondo: taparía la cámara. El borde va encima del contenido, en un ::after.
10
+ "relative max-w-full overflow-hidden rounded-2xl after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border-2 after:border-primary", width == null && "w-[480px]", className), style: { aspectRatio: `${w} / ${h ?? w}`, ...(width == null ? null : { width }), ...style }, children: [children ?? null, name || live ? (_jsxs("div", { className: "absolute bottom-4 left-4 z-1 inline-flex max-w-[calc(100%-32px)] items-center gap-3 rounded-lg border border-secondary bg-primary_alt px-3 py-1.5 text-sm font-medium text-secondary", children: [live ? (_jsxs("span", { className: "inline-flex items-center gap-2 font-semibold text-primary", children: [_jsx("span", { "aria-hidden": "true", className: "inline-block size-2 flex-none rounded-full bg-fg-success-secondary" }), liveLabel ?? defaultStrings.streamLive] })) : null, name ? _jsx("span", { className: "truncate font-semibold text-primary", children: name }) : null, role ? _jsx("span", { className: "whitespace-nowrap text-tertiary", children: role }) : null] })) : null] }));
11
+ }
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from "react";
2
+ import { type AvatarProps } from "./Avatar.js";
3
+ import { type BadgeColor } from "./Badge.js";
4
+ export interface CardHeaderProps {
5
+ /** Título de la tarjeta (`<h3>`, `text-lg` semibold). */
6
+ title: ReactNode;
7
+ /** Insignia junto al título, como "12 activas". */
8
+ badge?: ReactNode;
9
+ /**
10
+ * Color de la insignia.
11
+ * @default "brand"
12
+ */
13
+ badgeColor?: BadgeColor;
14
+ /** Una frase en `text-tertiary` bajo el título. */
15
+ description?: ReactNode;
16
+ /** Botones a la derecha: la acción principal, la última. */
17
+ actions?: ReactNode;
18
+ /**
19
+ * Menú de la tarjeta: `true` pinta el botón de tres puntos (con `onMenu`); también admite un nodo, como un
20
+ * `Dropdown` con `trigger` (ampliación sobre la guía, como en `MetricCard`).
21
+ */
22
+ menu?: boolean | ReactNode;
23
+ /** Al pulsar el botón de tres puntos. */
24
+ onMenu?: () => void;
25
+ /** Nombre del botón de tres puntos. @default "Más opciones" */
26
+ menuLabel?: string;
27
+ /** Avatar de 48px a la izquierda, para fichas de persona (props de `Avatar`). */
28
+ avatar?: AvatarProps;
29
+ className?: string;
30
+ }
31
+ /** Cabecera de una tarjeta: título con insignia opcional, descripción, acciones y menú, con línea inferior. */
32
+ export declare function CardHeader({ title, badge, badgeColor, description, actions, menu, onMenu, menuLabel, avatar, className, }: CardHeaderProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
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 { Line } from "../internal/line.js";
5
+ import { defaultStrings } from "../internal/strings.js";
6
+ import { Avatar } from "./Avatar.js";
7
+ import { Badge } from "./Badge.js";
8
+ import { Button } from "./Button.js";
9
+ /** Cabecera de una tarjeta: título con insignia opcional, descripción, acciones y menú, con línea inferior. */
10
+ export function CardHeader({ title, badge, badgeColor = "brand", description, actions, menu, onMenu, menuLabel, avatar, className, }) {
11
+ return (_jsxs("div", { className: cx("flex flex-col gap-5 bg-primary font-body", className), children: [_jsxs("div", { className: "flex items-start gap-4 px-6 pt-5", children: [avatar ? _jsx(Avatar, { size: "lg", ...avatar }) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("h3", { className: "m-0 flex items-center gap-2 text-lg font-semibold text-primary", children: [title, badge != null ? (_jsx(Badge, { size: "sm", color: badgeColor, children: badge })) : null] }), description ? _jsx("p", { className: "m-0 text-sm text-tertiary", children: description }) : null] }), actions ? _jsx("div", { className: "flex flex-wrap items-center gap-3", children: actions }) : null, menu === true ? (_jsx(Button, { hierarchy: "tertiary", size: "sm", iconOnly: "dots-vertical", label: menuLabel ?? defaultStrings.moreOptions, onClick: onMenu })) : isValidElement(menu) ? (menu) : null] }), _jsx(Line, {})] }));
12
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ChannelBannerSlot {
3
+ day: ReactNode;
4
+ time: ReactNode;
5
+ }
6
+ export interface ChannelBannerProps {
7
+ /** @default "Tu canal" */
8
+ name?: ReactNode;
9
+ tagline?: ReactNode;
10
+ /** @default "Directos en Twitch" */
11
+ eyebrow?: ReactNode;
12
+ /** Horario a la derecha, dentro de la zona segura; no más de cuatro filas. */
13
+ schedule?: ChannelBannerSlot[];
14
+ /** Nombre del horario para lectores de pantalla (ampliación sobre la guía). @default "Horario de directos" */
15
+ scheduleLabel?: string;
16
+ /** Mide `1200 × scale` (`scale={1}` para exportarlo tal cual). Sin él se ajusta al ancho del contenedor. */
17
+ scale?: number;
18
+ className?: string;
19
+ }
20
+ /**
21
+ * Banner de perfil de 1200×480, siempre en oscuro. Twitch lo recorta en móvil y tapa parte con el avatar: todo
22
+ * el contenido queda a 120px de cada lado (zona segura). Es de servidor: solo el lienzo, que mide, es de cliente.
23
+ */
24
+ export declare function ChannelBanner({ name, tagline, eyebrow, schedule, scheduleLabel, scale, className, }: ChannelBannerProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../internal/cx.js";
3
+ import { keyText, stableKeys, streamGrid, TABULAR } from "../internal/stream.js";
4
+ import { StreamStage } from "../internal/stream-stage.js";
5
+ const GRID = streamGrid(80, "80% 90%");
6
+ /**
7
+ * Banner de perfil de 1200×480, siempre en oscuro. Twitch lo recorta en móvil y tapa parte con el avatar: todo
8
+ * el contenido queda a 120px de cada lado (zona segura). Es de servidor: solo el lienzo, que mide, es de cliente.
9
+ */
10
+ export function ChannelBanner({ name, tagline, eyebrow, schedule, scheduleLabel, scale, className, }) {
11
+ const keys = stableKeys(schedule ?? [], (r) => `${keyText(r.day)}|${keyText(r.time)}`);
12
+ return (_jsxs(StreamStage, { width: 1200, height: 480, scale: scale, className: className, children: [_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0", style: GRID }), _jsxs("div", { className: "absolute top-0 bottom-0 left-[120px] flex w-[560px] flex-col justify-center gap-3", children: [_jsx("span", { className: "text-xl leading-7 font-medium text-tertiary", children: eyebrow ?? "Directos en Twitch" }), _jsx("h2", { className: "m-0 font-display text-[96px] leading-[1.04] font-semibold tracking-[-0.02em] wrap-anywhere text-primary", children: name ?? "Tu canal" }), tagline ? _jsx("p", { className: "m-0 text-[28px] leading-10 text-tertiary", children: tagline }) : null] }), schedule?.length ? (_jsx("ul", { "aria-label": scheduleLabel ?? "Horario de directos", className: "absolute top-1/2 right-[120px] m-0 w-[300px] -translate-y-1/2 list-none overflow-hidden rounded-xl border-2 border-secondary bg-primary_alt p-0", children: schedule.map((r, i) => (_jsxs("li", { className: "flex justify-between gap-4 border-t border-secondary px-6 py-4 text-[24px] leading-8 text-tertiary first:border-t-0", children: [_jsx("span", { children: r.day }), _jsx("b", { className: cx(TABULAR, "font-semibold text-primary"), children: r.time })] }, keys[i]))) })) : null] }));
13
+ }
@@ -0,0 +1,36 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ButtonHierarchy } from "./Button.js";
3
+ import { type IconName } from "./Icon.js";
4
+ export interface ChannelPanelRow {
5
+ label: ReactNode;
6
+ value: ReactNode;
7
+ }
8
+ export interface ChannelPanelAction {
9
+ label: ReactNode;
10
+ href?: string;
11
+ /** Icono tras el texto. */
12
+ icon?: IconName | ReactNode;
13
+ /** @default "secondary" */
14
+ hierarchy?: ButtonHierarchy;
15
+ /** Sin `href`, lo que hace el botón (ampliación sobre la guía). */
16
+ onClick?: () => void;
17
+ }
18
+ export interface ChannelPanelProps {
19
+ /** En Bricolage Grotesque 24/32 semibold. */
20
+ title: ReactNode;
21
+ icon?: IconName;
22
+ /** Párrafos; no más de cuatro líneas cada uno. */
23
+ children?: ReactNode;
24
+ /** Filas de datos: horario, normas cortas o enlaces. */
25
+ rows?: ChannelPanelRow[];
26
+ /** Una sola acción, a todo el ancho. */
27
+ action?: ChannelPanelAction;
28
+ /**
29
+ * Twitch pide 320px de ancho (exporta a 640px, 2x).
30
+ * @default 320
31
+ */
32
+ width?: number | string;
33
+ className?: string;
34
+ }
35
+ /** Panel de 320px para la página del canal de Twitch, siempre en oscuro. */
36
+ export declare function ChannelPanel({ title, icon, children, rows, action, width, className }: ChannelPanelProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { cx } from "../internal/cx.js";
4
+ import { keyText, STREAM_ROOT, stableKeys, TABULAR } from "../internal/stream.js";
5
+ import { Button } from "./Button.js";
6
+ import { Icon } from "./Icon.js";
7
+ /** Panel de 320px para la página del canal de Twitch, siempre en oscuro. */
8
+ export function ChannelPanel({ title, icon, children, rows, action, width, className }) {
9
+ const titleId = useId();
10
+ const keys = stableKeys(rows ?? [], (r) => `${keyText(r.label)}|${keyText(r.value)}`);
11
+ return (_jsxs("section", { "aria-labelledby": titleId, className: cx(STREAM_ROOT, "max-w-full rounded-xl border border-secondary bg-primary_alt p-6", width == null && "w-[320px]", className), style: width == null ? undefined : { width }, children: [_jsxs("header", { className: "flex items-center gap-3", children: [icon ? (_jsx("span", { className: "inline-flex size-9 flex-none items-center justify-center rounded-lg bg-tertiary text-fg-secondary", children: _jsx(Icon, { name: icon, size: 20 }) })) : null, _jsx("h3", { id: titleId, className: "m-0 font-display text-display-xs font-semibold text-primary", children: title })] }), children ? (_jsx("div", { className: "mt-3 text-md text-secondary [&_p]:mb-2 [&_p]:last:mb-0", children: children })) : null, rows?.length ? (_jsx("dl", { className: "mt-3 mb-0", children: rows.map((r, i) => (_jsxs("div", { className: "flex justify-between gap-4 border-t border-secondary py-3 text-md", children: [_jsx("dt", { className: "text-tertiary", children: r.label }), _jsx("dd", { className: cx(TABULAR, "m-0 text-right font-medium text-primary"), children: r.value })] }, keys[i]))) })) : null, action ? (_jsx("div", { className: "mt-4", children: action.href ? (_jsx(Button, { hierarchy: action.hierarchy ?? "secondary", size: "md", href: action.href, iconTrailing: action.icon, className: "w-full", children: action.label })) : (_jsx(Button, { hierarchy: action.hierarchy ?? "secondary", size: "md", iconTrailing: action.icon, onClick: action.onClick, className: "w-full", children: action.label })) })) : null] }));
12
+ }
@@ -0,0 +1,12 @@
1
+ export interface ChartLegendProps {
2
+ /** Series en el orden de la gráfica: sin `color`, el de su posición en la paleta. */
3
+ series: {
4
+ name?: string;
5
+ color?: string;
6
+ }[];
7
+ /** En columna (como junto a `PieChart`) en lugar de en fila. */
8
+ vertical?: boolean;
9
+ className?: string;
10
+ }
11
+ /** Leyenda de series con un punto de color y el nombre. */
12
+ export declare function ChartLegend({ series, vertical, className }: ChartLegendProps): import("react").JSX.Element;