@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.
- package/README.md +77 -2
- package/dist/components/ActivityFeed.d.ts +29 -0
- package/dist/components/ActivityFeed.js +10 -0
- package/dist/components/Alert.d.ts +34 -0
- package/dist/components/Alert.js +27 -0
- package/dist/components/AppIcon.d.ts +27 -0
- package/dist/components/AppIcon.js +9 -0
- package/dist/components/Avatar.d.ts +20 -0
- package/dist/components/Avatar.js +27 -0
- package/dist/components/AvatarGroup.d.ts +10 -0
- package/dist/components/AvatarGroup.js +7 -0
- package/dist/components/AvatarLabel.d.ts +10 -0
- package/dist/components/AvatarLabel.js +7 -0
- package/dist/components/BackgroundPattern.d.ts +35 -0
- package/dist/components/BackgroundPattern.js +73 -0
- package/dist/components/Badge.d.ts +38 -0
- package/dist/components/Badge.js +82 -0
- package/dist/components/Banner.d.ts +48 -0
- package/dist/components/Banner.js +60 -0
- package/dist/components/BarChart.d.ts +25 -0
- package/dist/components/BarChart.js +30 -0
- package/dist/components/Breadcrumbs.d.ts +19 -0
- package/dist/components/Breadcrumbs.js +14 -0
- package/dist/components/BrowserFrame.d.ts +18 -0
- package/dist/components/BrowserFrame.js +13 -0
- package/dist/components/Button.d.ts +51 -0
- package/dist/components/Button.js +54 -0
- package/dist/components/ButtonGroup.d.ts +21 -0
- package/dist/components/ButtonGroup.js +14 -0
- package/dist/components/CalendarView.d.ts +30 -0
- package/dist/components/CalendarView.js +43 -0
- package/dist/components/CamFrame.d.ts +21 -0
- package/dist/components/CamFrame.js +11 -0
- package/dist/components/CardHeader.d.ts +32 -0
- package/dist/components/CardHeader.js +12 -0
- package/dist/components/ChannelBanner.d.ts +24 -0
- package/dist/components/ChannelBanner.js +13 -0
- package/dist/components/ChannelPanel.d.ts +36 -0
- package/dist/components/ChannelPanel.js +12 -0
- package/dist/components/ChartLegend.d.ts +12 -0
- package/dist/components/ChartLegend.js +7 -0
- package/dist/components/ChatBadge.d.ts +15 -0
- package/dist/components/ChatBadge.js +39 -0
- package/dist/components/ChatBox.d.ts +42 -0
- package/dist/components/ChatBox.js +35 -0
- package/dist/components/Checkbox.d.ts +17 -0
- package/dist/components/Checkbox.js +18 -0
- package/dist/components/CloseButton.d.ts +7 -0
- package/dist/components/CloseButton.js +8 -0
- package/dist/components/CodeSnippet.d.ts +23 -0
- package/dist/components/CodeSnippet.js +75 -0
- package/dist/components/ColorPicker.d.ts +15 -0
- package/dist/components/ColorPicker.js +42 -0
- package/dist/components/CommandMenu.d.ts +72 -0
- package/dist/components/CommandMenu.js +102 -0
- package/dist/components/ContentDivider.d.ts +17 -0
- package/dist/components/ContentDivider.js +13 -0
- package/dist/components/CreditCard.d.ts +28 -0
- package/dist/components/CreditCard.js +63 -0
- package/dist/components/DatePicker.d.ts +48 -0
- package/dist/components/DatePicker.js +105 -0
- package/dist/components/Dropdown.d.ts +39 -0
- package/dist/components/Dropdown.js +39 -0
- package/dist/components/EmptyState.d.ts +18 -0
- package/dist/components/EmptyState.js +7 -0
- package/dist/components/FeaturedIcon.d.ts +17 -0
- package/dist/components/FeaturedIcon.js +32 -0
- package/dist/components/FileUpload.d.ts +39 -0
- package/dist/components/FileUpload.js +50 -0
- package/dist/components/FilterBar.d.ts +46 -0
- package/dist/components/FilterBar.js +24 -0
- package/dist/components/Icon.d.ts +5 -0
- package/dist/components/Icon.js +9 -0
- package/dist/components/InlineCTA.d.ts +19 -0
- package/dist/components/InlineCTA.js +7 -0
- package/dist/components/Input.d.ts +16 -0
- package/dist/components/Input.js +10 -0
- package/dist/components/LineChart.d.ts +25 -0
- package/dist/components/LineChart.js +23 -0
- package/dist/components/Message.d.ts +22 -0
- package/dist/components/Message.js +10 -0
- package/dist/components/MessageInput.d.ts +17 -0
- package/dist/components/MessageInput.js +25 -0
- package/dist/components/MetricCard.d.ts +28 -0
- package/dist/components/MetricCard.js +11 -0
- package/dist/components/Modal.d.ts +45 -0
- package/dist/components/Modal.js +15 -0
- package/dist/components/Notification.d.ts +15 -0
- package/dist/components/Notification.js +33 -0
- package/dist/components/NowPlaying.d.ts +25 -0
- package/dist/components/NowPlaying.js +16 -0
- package/dist/components/PageHeader.d.ts +22 -0
- package/dist/components/PageHeader.js +8 -0
- package/dist/components/Pagination.d.ts +29 -0
- package/dist/components/Pagination.js +33 -0
- package/dist/components/PieChart.d.ts +32 -0
- package/dist/components/PieChart.js +30 -0
- package/dist/components/ProgressBar.d.ts +11 -0
- package/dist/components/ProgressBar.js +8 -0
- package/dist/components/Radio.d.ts +15 -0
- package/dist/components/Radio.js +10 -0
- package/dist/components/RadioGroup.d.ts +26 -0
- package/dist/components/RadioGroup.js +9 -0
- package/dist/components/SectionFooter.d.ts +12 -0
- package/dist/components/SectionFooter.js +7 -0
- package/dist/components/SectionHeader.d.ts +24 -0
- package/dist/components/SectionHeader.js +7 -0
- package/dist/components/Select.d.ts +22 -0
- package/dist/components/Select.js +14 -0
- package/dist/components/SidebarNav.d.ts +38 -0
- package/dist/components/SidebarNav.js +26 -0
- package/dist/components/Skeleton.d.ts +15 -0
- package/dist/components/Skeleton.js +10 -0
- package/dist/components/Slideout.d.ts +28 -0
- package/dist/components/Slideout.js +15 -0
- package/dist/components/Slider.d.ts +15 -0
- package/dist/components/Slider.js +17 -0
- package/dist/components/Spinner.d.ts +11 -0
- package/dist/components/Spinner.js +12 -0
- package/dist/components/StreamAlert.d.ts +34 -0
- package/dist/components/StreamAlert.js +10 -0
- package/dist/components/StreamCountdown.d.ts +30 -0
- package/dist/components/StreamCountdown.js +44 -0
- package/dist/components/StreamEvents.d.ts +40 -0
- package/dist/components/StreamEvents.js +20 -0
- package/dist/components/StreamGoal.d.ts +22 -0
- package/dist/components/StreamGoal.js +21 -0
- package/dist/components/StreamScene.d.ts +35 -0
- package/dist/components/StreamScene.js +33 -0
- package/dist/components/StreamStatus.d.ts +28 -0
- package/dist/components/StreamStatus.js +26 -0
- package/dist/components/Table.d.ts +36 -0
- package/dist/components/Table.js +15 -0
- package/dist/components/Tabs.d.ts +38 -0
- package/dist/components/Tabs.js +34 -0
- package/dist/components/Tag.d.ts +18 -0
- package/dist/components/Tag.js +15 -0
- package/dist/components/TextEditor.d.ts +37 -0
- package/dist/components/TextEditor.js +103 -0
- package/dist/components/Textarea.d.ts +6 -0
- package/dist/components/Textarea.js +10 -0
- package/dist/components/Toggle.d.ts +22 -0
- package/dist/components/Toggle.js +12 -0
- package/dist/components/Tooltip.d.ts +15 -0
- package/dist/components/Tooltip.js +27 -0
- package/dist/components/TreeView.d.ts +33 -0
- package/dist/components/TreeView.js +48 -0
- package/dist/components/VideoPlayer.d.ts +25 -0
- package/dist/components/VideoPlayer.js +52 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +76 -0
- package/dist/internal/chart.d.ts +38 -0
- package/dist/internal/chart.js +33 -0
- package/dist/internal/choice.d.ts +18 -0
- package/dist/internal/choice.js +17 -0
- package/dist/internal/cx.d.ts +3 -0
- package/dist/internal/cx.js +26 -0
- package/dist/internal/field.d.ts +29 -0
- package/dist/internal/field.js +23 -0
- package/dist/internal/format.d.ts +12 -0
- package/dist/internal/format.js +40 -0
- package/dist/internal/kbd.d.ts +6 -0
- package/dist/internal/kbd.js +6 -0
- package/dist/internal/layer.d.ts +21 -0
- package/dist/internal/layer.js +27 -0
- package/dist/internal/line.d.ts +2 -0
- package/dist/internal/line.js +5 -0
- package/dist/internal/overlay.d.ts +2 -0
- package/dist/internal/overlay.js +4 -0
- package/dist/internal/stream-stage.d.ts +18 -0
- package/dist/internal/stream-stage.js +38 -0
- package/dist/internal/stream.d.ts +56 -0
- package/dist/internal/stream.js +112 -0
- package/dist/internal/strings.d.ts +100 -0
- package/dist/internal/strings.js +102 -0
- package/dist/internal/use-controllable.d.ts +5 -0
- package/dist/internal/use-controllable.js +17 -0
- package/package.json +66 -5
- package/styles.css +7 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type StreamStatusValue = "live" | "offline" | "rerun";
|
|
3
|
+
export interface StreamStatusLabels {
|
|
4
|
+
/** @default "En directo" */
|
|
5
|
+
live: string;
|
|
6
|
+
/** @default "Reemisión" */
|
|
7
|
+
rerun: string;
|
|
8
|
+
/** @default "Sin directo" */
|
|
9
|
+
offline: string;
|
|
10
|
+
/** Tras la cifra, solo para lectores de pantalla. @default "espectadores" */
|
|
11
|
+
viewers: string;
|
|
12
|
+
/** Tras el tiempo en emisión, solo para lectores de pantalla. @default "en emisión" */
|
|
13
|
+
uptime: string;
|
|
14
|
+
}
|
|
15
|
+
export interface StreamStatusProps {
|
|
16
|
+
/** @default "live" */
|
|
17
|
+
status?: StreamStatusValue;
|
|
18
|
+
/** Espectadores; solo en `live`. Quítalo si no quieres exponer la cifra. */
|
|
19
|
+
viewers?: number;
|
|
20
|
+
/** Tiempo en emisión como texto ("2:14:05"); solo en `live`. */
|
|
21
|
+
uptime?: string;
|
|
22
|
+
category?: ReactNode;
|
|
23
|
+
/** Sustituye los textos (ampliación sobre la guía). */
|
|
24
|
+
labels?: Partial<StreamStatusLabels>;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Píldora de estado del canal (en directo, reemisión o sin directo), siempre en oscuro. */
|
|
28
|
+
export declare function StreamStatus({ status, viewers, uptime, category, labels, className, }: StreamStatusProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../internal/cx.js";
|
|
3
|
+
import { formatNumber } from "../internal/format.js";
|
|
4
|
+
import { STREAM_ROOT, TABULAR } from "../internal/stream.js";
|
|
5
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
6
|
+
import { Icon } from "./Icon.js";
|
|
7
|
+
// Nunca rojo para "en directo": en el sistema el rojo es error. El punto va siempre junto a la palabra.
|
|
8
|
+
const DOT = {
|
|
9
|
+
live: "bg-fg-success-secondary",
|
|
10
|
+
rerun: "bg-fg-warning-secondary",
|
|
11
|
+
offline: "bg-fg-quaternary",
|
|
12
|
+
};
|
|
13
|
+
/** Píldora de estado del canal (en directo, reemisión o sin directo), siempre en oscuro. */
|
|
14
|
+
export function StreamStatus({ status = "live", viewers, uptime, category, labels, className, }) {
|
|
15
|
+
const t = {
|
|
16
|
+
live: defaultStrings.streamLive,
|
|
17
|
+
rerun: defaultStrings.streamRerun,
|
|
18
|
+
offline: defaultStrings.streamOffline,
|
|
19
|
+
viewers: defaultStrings.viewers,
|
|
20
|
+
uptime: defaultStrings.onAir,
|
|
21
|
+
...labels,
|
|
22
|
+
};
|
|
23
|
+
const live = status === "live";
|
|
24
|
+
const meta = cx(TABULAR, "inline-flex items-center gap-1.5");
|
|
25
|
+
return (_jsxs("div", { role: "status", className: cx(STREAM_ROOT, "inline-flex items-center gap-4 rounded-full border border-secondary bg-primary_alt px-4 py-2 text-sm font-medium text-secondary", className), children: [_jsxs("span", { className: "inline-flex items-center gap-2 font-semibold text-primary", children: [_jsx("span", { "aria-hidden": "true", className: cx("inline-block size-2 flex-none rounded-full", DOT[status]) }), t[status]] }), live && viewers != null ? (_jsxs("span", { className: meta, children: [_jsx(Icon, { name: "eye", size: 16, className: "text-fg-tertiary" }), formatNumber(viewers), _jsxs("span", { className: "sr-only", children: [" ", t.viewers] })] })) : null, live && uptime ? (_jsxs("span", { className: meta, children: [_jsx(Icon, { name: "clock", size: 16, className: "text-fg-tertiary" }), uptime, _jsxs("span", { className: "sr-only", children: [" ", t.uptime] })] })) : null, category ? _jsx("span", { className: meta, children: category }) : null] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface TableColumn<R = Record<string, unknown>> {
|
|
3
|
+
key: string;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
/** Columna principal (la primera): `text-primary` medium. Solo una. */
|
|
6
|
+
primary?: boolean;
|
|
7
|
+
/** Los números, a la derecha. @default "left" */
|
|
8
|
+
align?: "left" | "right";
|
|
9
|
+
/** Pinta la celda; por defecto, `row[key]`. */
|
|
10
|
+
render?: (row: R) => ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export interface TableProps<R = Record<string, unknown>> {
|
|
13
|
+
columns: TableColumn<R>[];
|
|
14
|
+
/** Filas. Si tienen `id`, se usa como clave. */
|
|
15
|
+
rows: R[];
|
|
16
|
+
/** Título de la tarjeta; también da nombre a la tabla. */
|
|
17
|
+
title?: ReactNode;
|
|
18
|
+
/** Total junto al título, en un `Badge` de marca. */
|
|
19
|
+
count?: ReactNode;
|
|
20
|
+
description?: ReactNode;
|
|
21
|
+
/** Acciones de la cabecera (buscar, exportar): a la vista, sin esconderlas. */
|
|
22
|
+
actions?: ReactNode;
|
|
23
|
+
/** Pie, por ejemplo una `Pagination`. */
|
|
24
|
+
footer?: ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* Alto de fila: `md` 72px · `sm` 56px.
|
|
27
|
+
* @default "md"
|
|
28
|
+
*/
|
|
29
|
+
size?: "sm" | "md";
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Tabla de datos en tarjeta con cabecera, columnas y pie opcional. Mínima: nada que no sea dato.
|
|
34
|
+
* Si no cabe a lo ancho, se desplaza dentro de la tarjeta (también con teclado).
|
|
35
|
+
*/
|
|
36
|
+
export declare function Table<R = Record<string, unknown>>({ columns, rows, title, count, description, actions, footer, size, className, }: TableProps<R>): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
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 { Badge } from "./Badge.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tabla de datos en tarjeta con cabecera, columnas y pie opcional. Mínima: nada que no sea dato.
|
|
7
|
+
* Si no cabe a lo ancho, se desplaza dentro de la tarjeta (también con teclado).
|
|
8
|
+
*/
|
|
9
|
+
export function Table({ columns, rows, title, count, description, actions, footer, size = "md", className, }) {
|
|
10
|
+
const id = useId();
|
|
11
|
+
const titleId = title ? `${id}-title` : undefined;
|
|
12
|
+
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: [title ? (_jsxs("div", { className: "flex items-center gap-4 px-6 py-5", children: [_jsxs("div", { className: "flex-1", children: [_jsxs("h3", { id: titleId, className: "m-0 flex items-center gap-2 text-lg font-semibold text-primary", children: [title, count != null ? (_jsx(Badge, { color: "brand", size: "sm", children: count })) : null] }), description ? (_jsx("p", { className: "mt-1 mb-0 text-sm text-tertiary", children: description })) : null] }), actions ?? null] })) : null, _jsx("div", { ...(titleId ? { role: "region", "aria-labelledby": titleId } : {}),
|
|
13
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: región desplazable; sin foco no se podría recorrer con teclado
|
|
14
|
+
tabIndex: 0, className: "overflow-x-auto focus-visible:shadow-[inset_0_0_0_2px_var(--focus-ring)] focus-visible:outline-none", children: _jsxs("table", { "aria-labelledby": titleId, className: "w-full border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: columns.map((c) => (_jsx("th", { scope: "col", className: cx("h-11 border-y border-secondary bg-secondary px-6 py-3 text-xs font-semibold whitespace-nowrap text-quaternary", c.align === "right" ? "text-right" : "text-left"), children: c.label }, c.key))) }) }), _jsx("tbody", { children: rows.map((row, i) => (_jsx("tr", { className: "hover:bg-primary_hover [&:last-child>td]:border-b-0", children: columns.map((c) => (_jsx("td", { className: cx("border-b border-secondary px-6 text-sm", size === "sm" ? "h-14 py-3" : "h-18 py-4", c.primary ? "font-medium text-primary" : "text-tertiary", c.align === "right" && "text-right"), children: c.render ? c.render(row) : row[c.key] }, c.key))) }, String(row.id ?? i)))) })] }) }), footer ?? null] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type TabsType = "underline" | "button-brand" | "button-gray" | "button-border";
|
|
3
|
+
export type TabsSize = "sm" | "md";
|
|
4
|
+
export interface TabItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
/** Contador junto a la etiqueta (por ejemplo, elementos sin leer). */
|
|
8
|
+
count?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TabsProps {
|
|
11
|
+
items: TabItem[];
|
|
12
|
+
/** Pestaña activa (controlado). */
|
|
13
|
+
value?: string;
|
|
14
|
+
/** Pestaña activa al principio (no controlado). Si falta, la primera. */
|
|
15
|
+
defaultValue?: string;
|
|
16
|
+
onChange?: (id: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* `underline` para secciones de página; `button-border` como control segmentado dentro de una tarjeta;
|
|
19
|
+
* `button-brand` cuando la navegación es la protagonista.
|
|
20
|
+
* @default "underline"
|
|
21
|
+
*/
|
|
22
|
+
type?: TabsType;
|
|
23
|
+
/**
|
|
24
|
+
* Alto: `sm` 36 · `md` 44.
|
|
25
|
+
* @default "sm"
|
|
26
|
+
*/
|
|
27
|
+
size?: TabsSize;
|
|
28
|
+
/** Nombre de la lista de pestañas para lectores de pantalla. */
|
|
29
|
+
"aria-label"?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Contenido de la pestaña activa (ampliación sobre la guía). Si llega, se pinta en un `tabpanel` enlazado a su
|
|
32
|
+
* pestaña; si no, el componente es solo la lista y tú pintas el contenido.
|
|
33
|
+
*/
|
|
34
|
+
children?: (id: string) => ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Pestañas para cambiar entre vistas hermanas dentro de una página. Flechas, Inicio y Fin mueven la selección. */
|
|
38
|
+
export declare function Tabs({ items, value, defaultValue, onChange, type, size, children, className, ...rest }: TabsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Tabs as AriaTabs, Tab, TabList, TabPanel } from "react-aria-components";
|
|
4
|
+
import { cx } from "../internal/cx.js";
|
|
5
|
+
import { useControllable } from "../internal/use-controllable.js";
|
|
6
|
+
const LIST = {
|
|
7
|
+
underline: "gap-3 shadow-[inset_0_-1px_0_var(--border-secondary)]",
|
|
8
|
+
"button-brand": "",
|
|
9
|
+
"button-gray": "",
|
|
10
|
+
"button-border": "inline-flex w-fit rounded-lg bg-secondary_alt p-1 shadow-[inset_0_0_0_1px_var(--border-secondary)]",
|
|
11
|
+
};
|
|
12
|
+
const TAB = {
|
|
13
|
+
underline: "h-auto rounded-none px-1 pt-0 pb-3 data-selected:text-brand-secondary data-selected:shadow-[inset_0_-2px_0_var(--fg-brand-primary_alt)] data-selected:data-focus-visible:shadow-[inset_0_-2px_0_var(--fg-brand-primary_alt),var(--focus-ring-shadow)]",
|
|
14
|
+
"button-brand": "data-hovered:bg-brand-primary_alt data-hovered:text-brand-secondary data-selected:bg-brand-primary_alt data-selected:text-brand-secondary",
|
|
15
|
+
"button-gray": "data-hovered:bg-primary_hover data-hovered:text-secondary data-selected:bg-primary_hover data-selected:text-secondary",
|
|
16
|
+
"button-border": "data-selected:bg-primary_alt data-selected:text-secondary data-selected:shadow-sm",
|
|
17
|
+
};
|
|
18
|
+
/** Pestañas para cambiar entre vistas hermanas dentro de una página. Flechas, Inicio y Fin mueven la selección. */
|
|
19
|
+
export function Tabs({ items, value, defaultValue, onChange, type = "underline", size = "sm", children, className, ...rest }) {
|
|
20
|
+
const [current, setCurrent] = useControllable(value, defaultValue ?? items[0]?.id);
|
|
21
|
+
return (_jsxs(AriaTabs, { selectedKey: current, onSelectionChange: (key) => {
|
|
22
|
+
const id = String(key);
|
|
23
|
+
if (id === current)
|
|
24
|
+
return;
|
|
25
|
+
setCurrent(id);
|
|
26
|
+
onChange?.(id);
|
|
27
|
+
}, className: cx(className), children: [_jsx(TabList, { "aria-label": rest["aria-label"], className: cx("flex gap-1 font-body", LIST[type]), children: items.map((item) => (_jsxs(Tab, { id: item.id,
|
|
28
|
+
// Sin paneles, React Aria apuntaría aria-controls a un id que no existe
|
|
29
|
+
render: children
|
|
30
|
+
? undefined
|
|
31
|
+
: ({ "aria-controls": _, ...props }) => _jsx("div", { ...props }), className: cx("group inline-flex cursor-pointer items-center gap-2 whitespace-nowrap rounded-md px-3 py-2 font-semibold text-quaternary outline-none transition-colors duration-100 data-hovered:text-secondary data-focus-visible:shadow-focus-ring", size === "md" ? "h-11 text-md" : "h-9 text-sm", TAB[type]), children: [item.label, item.count != null ? (_jsx("span", { className: "rounded-full bg-utility-neutral-50 px-2 text-xs font-medium text-utility-neutral-700 inset-ring inset-ring-utility-neutral-200 group-data-selected:bg-utility-brand-50 group-data-selected:text-utility-brand-700 group-data-selected:inset-ring-utility-brand-200", children: item.count })) : null] }, item.id))) }), children
|
|
32
|
+
? items.map((item) => (_jsx(TabPanel, { id: item.id, className: "mt-4 rounded-md outline-none data-focus-visible:shadow-focus-ring", children: children(item.id) }, item.id)))
|
|
33
|
+
: null] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type TagSize = "sm" | "md" | "lg";
|
|
3
|
+
export interface TagProps {
|
|
4
|
+
/** Alto: `sm` 24 · `md` 24 con texto de 14 · `lg` 28. */
|
|
5
|
+
size?: TagSize;
|
|
6
|
+
/** Contador tras el texto. */
|
|
7
|
+
count?: number;
|
|
8
|
+
/** Punto verde antes del texto. */
|
|
9
|
+
dot?: boolean;
|
|
10
|
+
/** Muestra una X para quitarlo. */
|
|
11
|
+
onRemove?: () => void;
|
|
12
|
+
/** Nombre accesible de la X. Por defecto, "Quitar" seguido del texto. */
|
|
13
|
+
removeLabel?: string;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Chip neutro que pone o quita el usuario: filtros aplicados, etiquetas, selecciones. */
|
|
18
|
+
export declare function Tag({ size, count, dot, onRemove, removeLabel, children, className }: TagProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
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 { Icon } from "./Icon.js";
|
|
5
|
+
const SIZE = {
|
|
6
|
+
sm: "px-2 py-[3px] text-xs",
|
|
7
|
+
md: "px-[9px] py-0.5 text-sm",
|
|
8
|
+
lg: "px-2.5 py-1 text-sm",
|
|
9
|
+
};
|
|
10
|
+
/** Chip neutro que pone o quita el usuario: filtros aplicados, etiquetas, selecciones. */
|
|
11
|
+
export function Tag({ size = "sm", count, dot, onRemove, removeLabel, children, className }) {
|
|
12
|
+
const label = removeLabel ??
|
|
13
|
+
(typeof children === "string" ? `${defaultStrings.remove} ${children}` : defaultStrings.remove);
|
|
14
|
+
return (_jsxs("span", { className: cx("inline-flex items-center gap-[3px] whitespace-nowrap rounded-md bg-primary font-body font-medium text-secondary inset-ring inset-ring-primary", SIZE[size], className), children: [dot ? (_jsx("span", { "aria-hidden": "true", className: "m-px size-1.5 shrink-0 rounded-full bg-fg-success-secondary" })) : null, children, count != null ? (_jsx("span", { className: "rounded-[3px] bg-tertiary px-[5px] text-xs text-secondary", children: count })) : null, onRemove ? (_jsx("button", { type: "button", onClick: onRemove, "aria-label": label, className: "inline-flex cursor-pointer rounded-[3px] bg-transparent p-0.5 text-fg-quaternary transition-colors duration-100 hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:shadow-focus-ring focus-visible:outline-none", children: _jsx(Icon, { name: "x-close", size: size === "sm" ? 12 : 14 }) })) : null] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface TextEditorLabels {
|
|
3
|
+
toolbar: string;
|
|
4
|
+
bold: string;
|
|
5
|
+
italic: string;
|
|
6
|
+
underline: string;
|
|
7
|
+
list: string;
|
|
8
|
+
link: string;
|
|
9
|
+
linkPrompt: string;
|
|
10
|
+
alignLeft: string;
|
|
11
|
+
alignCenter: string;
|
|
12
|
+
alignRight: string;
|
|
13
|
+
charactersLeft: (n: number) => string;
|
|
14
|
+
}
|
|
15
|
+
export interface TextEditorProps {
|
|
16
|
+
/** Etiqueta visible encima de la barra. */
|
|
17
|
+
label?: ReactNode;
|
|
18
|
+
/** HTML inicial. */
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
/** Recibe el HTML en cada cambio. Límpialo antes de guardarlo. */
|
|
21
|
+
onChange?: (html: string) => void;
|
|
22
|
+
/** @default "Escribe aquí…" */
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
/** Muestra los caracteres que quedan en lugar de `hint`. No corta el texto. */
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
/** Pista bajo el área, si no hay `maxLength`. */
|
|
27
|
+
hint?: ReactNode;
|
|
28
|
+
/** Alto mínimo del área en px. @default 160 */
|
|
29
|
+
minHeight?: number;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** Textos sustituibles. */
|
|
32
|
+
labels?: Partial<TextEditorLabels>;
|
|
33
|
+
id?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Editor de texto enriquecido sencillo sobre el modo de edición nativo del navegador. */
|
|
37
|
+
export declare function TextEditor({ label, defaultValue, onChange, placeholder, maxLength, hint, minHeight, disabled, labels, id: givenId, className, }: TextEditorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../internal/cx.js";
|
|
5
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
6
|
+
import { Icon } from "./Icon.js";
|
|
7
|
+
const DEFAULT_LABELS = {
|
|
8
|
+
toolbar: defaultStrings.formatting,
|
|
9
|
+
bold: defaultStrings.bold,
|
|
10
|
+
italic: defaultStrings.italic,
|
|
11
|
+
underline: defaultStrings.underline,
|
|
12
|
+
list: defaultStrings.list,
|
|
13
|
+
link: defaultStrings.link,
|
|
14
|
+
linkPrompt: defaultStrings.linkPrompt,
|
|
15
|
+
alignLeft: defaultStrings.alignLeft,
|
|
16
|
+
alignCenter: defaultStrings.alignCenter,
|
|
17
|
+
alignRight: defaultStrings.alignRight,
|
|
18
|
+
charactersLeft: defaultStrings.charactersLeft,
|
|
19
|
+
};
|
|
20
|
+
const COMMANDS = [
|
|
21
|
+
"bold",
|
|
22
|
+
"italic",
|
|
23
|
+
"underline",
|
|
24
|
+
"insertUnorderedList",
|
|
25
|
+
"justifyLeft",
|
|
26
|
+
"justifyCenter",
|
|
27
|
+
"justifyRight",
|
|
28
|
+
];
|
|
29
|
+
const BUTTON = "inline-flex size-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-editor-icon-fg transition-colors duration-100 hover:bg-primary_hover hover:text-editor-icon-fg_active focus-visible:shadow-focus-ring focus-visible:outline-none disabled:cursor-not-allowed";
|
|
30
|
+
/** Editor de texto enriquecido sencillo sobre el modo de edición nativo del navegador. */
|
|
31
|
+
export function TextEditor({ label, defaultValue, onChange, placeholder = defaultStrings.writeHere, maxLength, hint, minHeight, disabled, labels, id: givenId, className, }) {
|
|
32
|
+
const t = { ...DEFAULT_LABELS, ...labels };
|
|
33
|
+
const autoId = useId();
|
|
34
|
+
const id = givenId ?? autoId;
|
|
35
|
+
const area = useRef(null);
|
|
36
|
+
// El HTML inicial se pinta una vez (también en servidor); después lo gestiona el navegador
|
|
37
|
+
const [initial] = useState(() => ({ __html: defaultValue ?? "" }));
|
|
38
|
+
const [active, setActive] = useState({});
|
|
39
|
+
const [count, setCount] = useState(() => textLength(defaultValue ?? ""));
|
|
40
|
+
const refresh = () => {
|
|
41
|
+
const next = {};
|
|
42
|
+
for (const c of COMMANDS) {
|
|
43
|
+
try {
|
|
44
|
+
next[c] = document.queryCommandState?.(c) ?? false;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
next[c] = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
setActive(next);
|
|
51
|
+
setCount(visibleLength(area.current));
|
|
52
|
+
};
|
|
53
|
+
const changed = () => {
|
|
54
|
+
refresh();
|
|
55
|
+
if (area.current)
|
|
56
|
+
onChange?.(area.current.innerHTML);
|
|
57
|
+
};
|
|
58
|
+
const run = (command, value) => {
|
|
59
|
+
area.current?.focus();
|
|
60
|
+
try {
|
|
61
|
+
document.execCommand?.(command, false, value);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Sin modo de edición (entornos sin DOM completo): no hace nada
|
|
65
|
+
}
|
|
66
|
+
changed();
|
|
67
|
+
};
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setCount(visibleLength(area.current));
|
|
70
|
+
}, []);
|
|
71
|
+
const button = (command, icon, name) => {
|
|
72
|
+
const toggles = command !== "createLink";
|
|
73
|
+
return (_jsx("button", { type: "button", "aria-label": name, title: name, "aria-pressed": toggles ? !!active[command] : undefined, disabled: disabled,
|
|
74
|
+
// Sin perder la selección del área al pulsar
|
|
75
|
+
onMouseDown: (e) => e.preventDefault(), onClick: () => {
|
|
76
|
+
if (toggles)
|
|
77
|
+
return run(command);
|
|
78
|
+
const url = window.prompt(t.linkPrompt);
|
|
79
|
+
if (url)
|
|
80
|
+
run("createLink", url);
|
|
81
|
+
}, className: cx(BUTTON, toggles && active[command] && "bg-primary_hover text-editor-icon-fg_active"), children: _jsx(Icon, { name: icon, size: 20 }) }));
|
|
82
|
+
};
|
|
83
|
+
const separator = _jsx("span", { "aria-hidden": "true", className: "mx-1.5 h-5 w-px bg-[var(--border-secondary)]" });
|
|
84
|
+
const below = maxLength !== undefined ? t.charactersLeft(Math.max(0, maxLength - count)) : (hint ?? null);
|
|
85
|
+
return (_jsxs("div", { className: cx("flex min-w-0 flex-col gap-1.5 font-body", disabled && "opacity-50", className), children: [label ? (_jsx("span", { id: `${id}-label`, className: "text-sm font-medium text-secondary", children: label })) : null, _jsxs("div", { role: "toolbar", "aria-label": t.toolbar, className: "flex flex-wrap items-center gap-0.5", children: [button("bold", "bold-01", t.bold), button("italic", "italic-01", t.italic), button("underline", "underline-01", t.underline), separator, button("insertUnorderedList", "dotpoints-01", t.list), button("createLink", "link-01", t.link), separator, button("justifyLeft", "align-left", t.alignLeft), button("justifyCenter", "align-center", t.alignCenter), button("justifyRight", "align-right", t.alignRight)] }), _jsx("div", { id: id, ref: area, role: "textbox", tabIndex: disabled ? -1 : 0, "aria-multiline": "true", "aria-labelledby": label ? `${id}-label` : undefined, "aria-label": label ? undefined : placeholder, "aria-describedby": below !== null ? `${id}-hint` : undefined, "aria-disabled": disabled || undefined, contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder,
|
|
86
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: el HTML inicial lo da quien usa el componente (la ficha pide limpiarlo antes de guardar)
|
|
87
|
+
dangerouslySetInnerHTML: initial, onInput: changed, onKeyUp: refresh, onMouseUp: refresh, style: minHeight ? { minHeight } : undefined, className: "min-h-40 rounded-xl bg-primary p-5 text-md text-primary shadow-[var(--shadow-xs),inset_0_0_0_1px_var(--border-primary)] outline-none [overflow-wrap:anywhere] focus:shadow-[var(--shadow-xs),inset_0_0_0_2px_var(--border-brand)] focus-visible:shadow-[var(--shadow-xs),inset_0_0_0_2px_var(--border-brand),var(--focus-ring-shadow)] empty:before:text-placeholder empty:before:content-[attr(data-placeholder)] [&_a]:text-brand-secondary [&_a]:underline [&_ol]:list-decimal [&_ol]:pl-10 [&_p]:mt-0 [&_p]:mb-2 [&_ul]:mt-0 [&_ul]:mb-2 [&_ul]:list-disc [&_ul]:pl-10" }), below !== null ? (_jsx("p", { id: `${id}-hint`, className: "m-0 text-sm text-tertiary", children: below })) : null] }));
|
|
88
|
+
}
|
|
89
|
+
/** Caracteres del área como los ve el usuario: innerText separa bloques con saltos de línea. */
|
|
90
|
+
function visibleLength(el) {
|
|
91
|
+
if (!el)
|
|
92
|
+
return 0;
|
|
93
|
+
// biome-ignore lint/performance/useDomNodeTextContent: textContent no cuenta los saltos entre bloques, como hace la guía
|
|
94
|
+
return (el.innerText ?? el.textContent ?? "").trim().length;
|
|
95
|
+
}
|
|
96
|
+
/** Longitud del texto visible de un HTML, sin etiquetas ni espacios a los lados. */
|
|
97
|
+
function textLength(html) {
|
|
98
|
+
return html
|
|
99
|
+
.replace(/<[^>]*>/g, "")
|
|
100
|
+
.replace(/ /g, " ")
|
|
101
|
+
.replace(/&[a-z]+;|&#\d+;/gi, "x")
|
|
102
|
+
.trim().length;
|
|
103
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type TextareaHTMLAttributes } from "react";
|
|
2
|
+
import { type FieldProps } from "../internal/field.js";
|
|
3
|
+
export interface TextareaProps extends FieldProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
|
+
}
|
|
5
|
+
/** Campo de texto multilínea con la misma etiqueta, pista y estados que `Input`. */
|
|
6
|
+
export declare function Textarea({ label, required, hint, error, id: givenId, className, disabled, rows, ...rest }: TextareaProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { cx } from "../internal/cx.js";
|
|
4
|
+
import { controlClasses, describedBy, FieldShell, nativeClasses, } from "../internal/field.js";
|
|
5
|
+
/** Campo de texto multilínea con la misma etiqueta, pista y estados que `Input`. */
|
|
6
|
+
export function Textarea({ label, required, hint, error, id: givenId, className, disabled, rows = 4, ...rest }) {
|
|
7
|
+
const autoId = useId();
|
|
8
|
+
const id = givenId ?? autoId;
|
|
9
|
+
return (_jsx(FieldShell, { id: id, label: label, required: required, hint: hint, error: error, className: className, children: _jsx("div", { className: cx(controlClasses(undefined, error, disabled), "px-3.5 py-3"), children: _jsx("textarea", { id: id, rows: rows, required: required, disabled: disabled, ...describedBy(id, { hint, error }), ...rest, className: cx(nativeClasses(), "min-h-[104px] resize-y") }) }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type ChoiceSize } from "../internal/choice.js";
|
|
3
|
+
export interface ToggleProps {
|
|
4
|
+
/** Etiqueta junto al interruptor. */
|
|
5
|
+
label?: ReactNode;
|
|
6
|
+
/** Texto de apoyo bajo la etiqueta. */
|
|
7
|
+
hint?: ReactNode;
|
|
8
|
+
checked?: boolean;
|
|
9
|
+
defaultChecked?: boolean;
|
|
10
|
+
onChange?: (checked: boolean) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Pista de 36×20 (`sm`) o 44×24 (`md`).
|
|
13
|
+
* @default "sm"
|
|
14
|
+
*/
|
|
15
|
+
size?: ChoiceSize;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Nombre del campo al enviar el formulario. */
|
|
18
|
+
name?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Interruptor (`role="switch"`) para ajustes que se aplican al momento, sin botón de guardar. */
|
|
22
|
+
export declare function Toggle({ label, hint, checked, defaultChecked, onChange, size, disabled, name, className, }: ToggleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Switch } from "react-aria-components";
|
|
4
|
+
import { ChoiceText, choiceLabel } from "../internal/choice.js";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
/** Interruptor (`role="switch"`) para ajustes que se aplican al momento, sin botón de guardar. */
|
|
7
|
+
export function Toggle({ label, hint, checked, defaultChecked, onChange, size = "sm", disabled, name, className, }) {
|
|
8
|
+
const md = size === "md";
|
|
9
|
+
return (_jsxs(Switch, { isSelected: checked, defaultSelected: defaultChecked, onChange: onChange, isDisabled: disabled, name: name, className: cx(choiceLabel(size), className), children: [_jsx("span", { "aria-hidden": "true", className: cx("relative flex-none rounded-full bg-tertiary p-0.5 transition-colors duration-150 group-data-selected:bg-brand-solid group-data-selected:group-data-hovered:bg-brand-solid_hover group-data-focus-visible:shadow-focus-ring group-data-disabled:opacity-50", md ? "h-6 w-11" : "h-5 w-9"), children: _jsx("span", { className: cx("block rounded-full bg-fg-white shadow-sm transition-transform duration-150 motion-reduce:transition-none", md
|
|
10
|
+
? "size-5 group-data-selected:translate-x-5"
|
|
11
|
+
: "size-4 group-data-selected:translate-x-4") }) }), _jsx(ChoiceText, { label: label, hint: hint, size: size, className: "group-data-disabled:opacity-50" })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface TooltipProps {
|
|
3
|
+
title: ReactNode;
|
|
4
|
+
/** Segunda línea, en `tooltip-supporting-text`. */
|
|
5
|
+
text?: ReactNode;
|
|
6
|
+
/** Fuerza el tooltip visible (vistas previas y capturas). */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
/** El elemento que lo dispara: tiene que poder recibir el foco y pasar `ref` y props al DOM, como `Button`. */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Burbuja oscura que explica un control al pasar el ratón o al enfocarlo; Escape la cierra.
|
|
13
|
+
* Nada imprescindible dentro: en móvil no hay hover.
|
|
14
|
+
*/
|
|
15
|
+
export declare function Tooltip({ title, text, open, children }: TooltipProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, isValidElement, useId, } from "react";
|
|
4
|
+
import { Tooltip as AriaTooltip, Focusable, OverlayArrow, TooltipTrigger } from "react-aria-components";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
const BUBBLE = "z-20 w-max max-w-[320px] rounded-lg bg-primary-solid font-body text-xs font-semibold text-white shadow-lg";
|
|
7
|
+
const SUB = "mt-1 block font-medium text-tooltip-supporting-text";
|
|
8
|
+
// Flecha de 12×6 centrada abajo, del color de la burbuja
|
|
9
|
+
const ARROW = "after:absolute after:top-full after:left-1/2 after:-ml-1.5 after:border-[6px] after:border-b-0 after:border-transparent after:border-t-[var(--bg-primary-solid)]";
|
|
10
|
+
/**
|
|
11
|
+
* Burbuja oscura que explica un control al pasar el ratón o al enfocarlo; Escape la cierra.
|
|
12
|
+
* Nada imprescindible dentro: en móvil no hay hover.
|
|
13
|
+
*/
|
|
14
|
+
export function Tooltip({ title, text, open, children }) {
|
|
15
|
+
const id = useId();
|
|
16
|
+
const padding = text ? "p-3" : "px-3 py-2";
|
|
17
|
+
const content = (_jsxs(_Fragment, { children: [title, text ? _jsx("span", { className: SUB, children: text }) : null] }));
|
|
18
|
+
if (open) {
|
|
19
|
+
// Forzado (vista previa): en línea sobre el disparador, como en la guía, sin capa flotante
|
|
20
|
+
return (_jsxs("span", { className: "relative inline-flex", children: [isValidElement(children)
|
|
21
|
+
? cloneElement(children, {
|
|
22
|
+
"aria-describedby": id,
|
|
23
|
+
})
|
|
24
|
+
: children, _jsx("span", { id: id, role: "tooltip", className: cx(BUBBLE, ARROW, padding, "pointer-events-none absolute bottom-[calc(100%+10px)] left-1/2 -translate-x-1/2"), children: content })] }));
|
|
25
|
+
}
|
|
26
|
+
return (_jsxs(TooltipTrigger, { delay: 300, closeDelay: 0, children: [_jsx(Focusable, { children: children }), _jsxs(AriaTooltip, { placement: "top", offset: 10, className: cx(BUBBLE, padding), children: [_jsx(OverlayArrow, { className: "group", children: _jsx("svg", { width: 12, height: 6, viewBox: "0 0 12 6", "aria-hidden": "true", className: "block fill-[var(--bg-primary-solid)] group-data-[placement=bottom]:rotate-180", children: _jsx("path", { d: "M0 0 L6 6 L12 0 Z" }) }) }), content] })] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type IconName } from "./Icon.js";
|
|
3
|
+
export interface TreeItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
/** Icono de 16px. Por defecto, `folder` si tiene hijos y `file-04` si no. */
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
/** Dato a la derecha, como el tamaño del archivo. */
|
|
9
|
+
meta?: ReactNode;
|
|
10
|
+
children?: TreeItem[];
|
|
11
|
+
/** Texto para la búsqueda por teclado si `label` no es texto. */
|
|
12
|
+
textValue?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TreeViewProps {
|
|
15
|
+
/** Más de cuatro niveles es mala señal. */
|
|
16
|
+
items: TreeItem[];
|
|
17
|
+
/** Ids de las carpetas abiertas al principio. */
|
|
18
|
+
defaultOpen?: string[];
|
|
19
|
+
/** Elemento seleccionado al principio (no controlado). */
|
|
20
|
+
defaultSelected?: string;
|
|
21
|
+
/** Elemento seleccionado (controlado). */
|
|
22
|
+
selected?: string;
|
|
23
|
+
/** Al elegir un elemento con clic, Enter o espacio. */
|
|
24
|
+
onSelect?: (item: TreeItem) => void;
|
|
25
|
+
/** Nombre del árbol para lectores de pantalla. */
|
|
26
|
+
"aria-label"?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Árbol de carpetas y archivos desplegable, con guías de nivel.
|
|
31
|
+
* Clic, Enter o espacio eligen el elemento y abren o cierran la carpeta; las flechas mueven el foco sin elegir (→ ← abren y cierran).
|
|
32
|
+
*/
|
|
33
|
+
export declare function TreeView({ items, defaultOpen, defaultSelected, selected, onSelect, className, ...rest }: TreeViewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { TreeItem as AriaTreeItem, Tree, TreeItemContent } from "react-aria-components";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
import { useControllable } from "../internal/use-controllable.js";
|
|
7
|
+
import { Icon } from "./Icon.js";
|
|
8
|
+
function find(items, id) {
|
|
9
|
+
for (const it of items) {
|
|
10
|
+
if (it.id === id)
|
|
11
|
+
return it;
|
|
12
|
+
const inner = it.children && find(it.children, id);
|
|
13
|
+
if (inner)
|
|
14
|
+
return inner;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Árbol de carpetas y archivos desplegable, con guías de nivel.
|
|
20
|
+
* Clic, Enter o espacio eligen el elemento y abren o cierran la carpeta; las flechas mueven el foco sin elegir (→ ← abren y cierran).
|
|
21
|
+
*/
|
|
22
|
+
export function TreeView({ items, defaultOpen = [], defaultSelected, selected, onSelect, className, ...rest }) {
|
|
23
|
+
const [current, setCurrent] = useControllable(selected, defaultSelected);
|
|
24
|
+
const [expanded, setExpanded] = useState(() => new Set(defaultOpen));
|
|
25
|
+
const choose = (id, toggle) => {
|
|
26
|
+
const item = find(items, id);
|
|
27
|
+
if (!item)
|
|
28
|
+
return;
|
|
29
|
+
if (toggle && item.children?.length) {
|
|
30
|
+
const next = new Set(expanded);
|
|
31
|
+
if (next.has(id))
|
|
32
|
+
next.delete(id);
|
|
33
|
+
else
|
|
34
|
+
next.add(id);
|
|
35
|
+
setExpanded(next);
|
|
36
|
+
}
|
|
37
|
+
setCurrent(item.id);
|
|
38
|
+
onSelect?.(item);
|
|
39
|
+
};
|
|
40
|
+
const render = (list) => list.map((item) => {
|
|
41
|
+
const kids = !!item.children?.length;
|
|
42
|
+
return (_jsxs(AriaTreeItem, { id: item.id, textValue: item.textValue ?? (typeof item.label === "string" ? item.label : item.id), onPress: () => choose(item.id, true), style: ({ level }) => ({ paddingLeft: 8 + (level - 1) * 24 }), className: "group relative flex cursor-pointer items-center gap-2 rounded-md py-1.5 pr-2 text-sm font-semibold text-tertiary outline-none data-focus-visible:shadow-focus-ring data-hovered:bg-primary_hover data-hovered:text-secondary data-selected:bg-secondary_hover data-selected:text-secondary [&_svg]:text-fg-quaternary data-selected:[&_svg]:text-fg-tertiary", children: [_jsx(TreeItemContent, { children: ({ level, isExpanded }) => (_jsxs(_Fragment, { children: [level > 1 ? (_jsx("span", { "aria-hidden": "true", children: Array.from({ length: level - 1 }, (_, k) => (_jsx("i", { className: "absolute -top-0.5 -bottom-0.5 w-px bg-[var(--border-secondary)]", style: { left: 19 + k * 24 } }, k))) })) : null, _jsx("span", { className: "inline-flex w-4 flex-none", children: kids ? (_jsx(Icon, { name: isExpanded ? "chevron-down" : "chevron-right", size: 16 })) : null }), _jsx(Icon, { name: item.icon ?? (kids ? "folder" : "file-04"), size: 16 }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }), item.meta != null ? (_jsx("span", { className: "text-xs font-medium text-quaternary", children: item.meta })) : null] })) }), kids ? render(item.children) : null] }, item.id));
|
|
43
|
+
});
|
|
44
|
+
// Un solo elemento raíz: React Aria deja junto al árbol una plantilla y marcadores ocultos
|
|
45
|
+
return (_jsx("div", { className: cx("max-w-[280px]", className), children: _jsx(Tree, { "aria-label": rest["aria-label"], selectionMode: "single", selectionBehavior: "toggle", disallowEmptySelection: true, selectedKeys: current ? [current] : [],
|
|
46
|
+
// La selección la llevan los clics y Enter o espacio (onPress de cada fila); React Aria solo la pinta
|
|
47
|
+
onSelectionChange: () => { }, expandedKeys: expanded, onExpandedChange: setExpanded, className: "flex flex-col gap-0.5 font-body outline-none", children: render(items) }) }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
export interface VideoPlayerLabels {
|
|
3
|
+
play: string;
|
|
4
|
+
pause: string;
|
|
5
|
+
seek: string;
|
|
6
|
+
mute: string;
|
|
7
|
+
unmute: string;
|
|
8
|
+
fullscreen: string;
|
|
9
|
+
/** Texto de la posición para lectores de pantalla: "1:05 de 4:12". */
|
|
10
|
+
position: (current: string, total: string) => string;
|
|
11
|
+
}
|
|
12
|
+
export interface VideoPlayerProps {
|
|
13
|
+
/** Fichero de vídeo. Sin él, el reproductor es una maqueta (con `poster` y `duration`). */
|
|
14
|
+
src?: string;
|
|
15
|
+
/** Imagen antes de reproducir. */
|
|
16
|
+
poster?: string;
|
|
17
|
+
/** Duración en segundos cuando no hay `src`. */
|
|
18
|
+
duration?: number;
|
|
19
|
+
/** Textos de los controles (ampliación sobre la guía). */
|
|
20
|
+
labels?: Partial<VideoPlayerLabels>;
|
|
21
|
+
className?: string;
|
|
22
|
+
style?: CSSProperties;
|
|
23
|
+
}
|
|
24
|
+
/** Reproductor de vídeo 16:9 con botón central, barra de controles, tiempo, volumen y pantalla completa. */
|
|
25
|
+
export declare function VideoPlayer({ src, poster, duration, labels, className, style }: VideoPlayerProps): import("react").JSX.Element;
|