@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,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { seriesColor } from "../internal/chart.js";
|
|
3
|
+
import { cx } from "../internal/cx.js";
|
|
4
|
+
/** Leyenda de series con un punto de color y el nombre. */
|
|
5
|
+
export function ChartLegend({ series, vertical, className }) {
|
|
6
|
+
return (_jsx("ul", { className: cx("m-0 flex list-none gap-x-3 gap-y-1 p-0 font-body", vertical ? "flex-col" : "flex-wrap", className), children: series.map((s, i) => (_jsxs("li", { className: "inline-flex items-center gap-2 text-sm text-tertiary", children: [_jsx("span", { "aria-hidden": "true", className: "size-2 shrink-0 rounded-full", style: { background: seriesColor(i, s.color) } }), s.name] }, i))) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ChatBadgeKind = "broadcaster" | "mod" | "vip" | "sub" | "bits";
|
|
2
|
+
export interface ChatBadgeProps {
|
|
3
|
+
/** @default "sub" */
|
|
4
|
+
kind?: ChatBadgeKind;
|
|
5
|
+
/**
|
|
6
|
+
* Lado en px; 18 como mínimo en el chat.
|
|
7
|
+
* @default 20
|
|
8
|
+
*/
|
|
9
|
+
size?: number;
|
|
10
|
+
/** Sustituye al texto accesible y al del `title` ("Streamer", "Moderador", "VIP", "Suscriptor", "Bits"). */
|
|
11
|
+
title?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Insignia cuadrada de chat para el rol del usuario, siempre en oscuro. */
|
|
15
|
+
export declare function ChatBadge({ kind, size, title, className }: ChatBadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../internal/cx.js";
|
|
3
|
+
import { STREAM_ROOT } from "../internal/stream.js";
|
|
4
|
+
import { Icon } from "./Icon.js";
|
|
5
|
+
// Receta de Badge sobre las escalas utility en oscuro: fondo -50, borde -200, icono -700. Cada rol lleva además su
|
|
6
|
+
// icono, así que no depende solo del color. Clases enteras: Tailwind solo genera las que ve escritas.
|
|
7
|
+
const BADGES = {
|
|
8
|
+
broadcaster: {
|
|
9
|
+
icon: "play",
|
|
10
|
+
label: "Streamer",
|
|
11
|
+
tone: "bg-utility-brand-50 text-utility-brand-700 inset-ring-utility-brand-200",
|
|
12
|
+
},
|
|
13
|
+
mod: {
|
|
14
|
+
icon: "shield-tick",
|
|
15
|
+
label: "Moderador",
|
|
16
|
+
tone: "bg-utility-emerald-50 text-utility-emerald-700 inset-ring-utility-emerald-200",
|
|
17
|
+
},
|
|
18
|
+
vip: {
|
|
19
|
+
icon: "stars-02",
|
|
20
|
+
label: "VIP",
|
|
21
|
+
tone: "bg-utility-pink-50 text-utility-pink-700 inset-ring-utility-pink-200",
|
|
22
|
+
},
|
|
23
|
+
sub: {
|
|
24
|
+
icon: "star-01",
|
|
25
|
+
label: "Suscriptor",
|
|
26
|
+
tone: "bg-utility-sky-50 text-utility-sky-700 inset-ring-utility-sky-200",
|
|
27
|
+
},
|
|
28
|
+
bits: {
|
|
29
|
+
icon: "zap",
|
|
30
|
+
label: "Bits",
|
|
31
|
+
tone: "bg-utility-purple-50 text-utility-purple-700 inset-ring-utility-purple-200",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
/** Insignia cuadrada de chat para el rol del usuario, siempre en oscuro. */
|
|
35
|
+
export function ChatBadge({ kind = "sub", size = 20, title, className }) {
|
|
36
|
+
const b = BADGES[kind] ?? BADGES.sub;
|
|
37
|
+
const name = title ?? b.label;
|
|
38
|
+
return (_jsx("span", { role: "img", "aria-label": name, title: name, className: cx(STREAM_ROOT, "inline-flex flex-none items-center justify-center rounded-sm inset-ring", b.tone, className), style: { width: size, height: size }, children: _jsx(Icon, { name: b.icon, size: Math.round(size * 0.65), strokeWidth: 2.25 }) }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type ChatBadgeKind } from "./ChatBadge.js";
|
|
3
|
+
export type ChatTone = "brand" | "sky" | "emerald" | "pink" | "purple" | "orange" | "indigo" | "neutral";
|
|
4
|
+
export interface ChatMessage {
|
|
5
|
+
user: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Lo que escribe el espectador. Siempre texto plano: llega del público y se pinta tal cual, nunca como HTML
|
|
8
|
+
* (por eso es `string` y no un nodo).
|
|
9
|
+
*/
|
|
10
|
+
text: string;
|
|
11
|
+
/**
|
|
12
|
+
* Tono del nombre: escala `utility-<tono>-700`.
|
|
13
|
+
* @default "neutral"
|
|
14
|
+
*/
|
|
15
|
+
tone?: ChatTone;
|
|
16
|
+
/** Insignias delante del nombre; no más de tres. */
|
|
17
|
+
badges?: ChatBadgeKind[];
|
|
18
|
+
/** Clave estable. Sin ella se deriva del usuario y el texto. */
|
|
19
|
+
id?: string | number;
|
|
20
|
+
}
|
|
21
|
+
export interface ChatBoxProps {
|
|
22
|
+
/** Del más antiguo al más reciente; no más de 12 visibles. */
|
|
23
|
+
messages: ChatMessage[];
|
|
24
|
+
/** @default 420 */
|
|
25
|
+
width?: number | string;
|
|
26
|
+
/** @default 520 */
|
|
27
|
+
height?: number | string;
|
|
28
|
+
/** Fondo mezclado con transparencia y desenfoque de 16px; sólido si el navegador no tiene `color-mix`. */
|
|
29
|
+
translucent?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Desvanece la parte de arriba de la lista.
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
fade?: boolean;
|
|
35
|
+
/** Solo el último mensaje entra con fundido y 8px de subida. */
|
|
36
|
+
animate?: boolean;
|
|
37
|
+
/** @default "Chat del directo" */
|
|
38
|
+
"aria-label"?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Chat del directo para overlay: anclado abajo, desvanecido arriba y siempre en oscuro. */
|
|
42
|
+
export declare function ChatBox({ messages, width, height, translucent, fade, animate, "aria-label": ariaLabel, className, }: ChatBoxProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../internal/cx.js";
|
|
3
|
+
import { keyText, STREAM_ROOT, stableKeys, streamMotion } from "../internal/stream.js";
|
|
4
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
5
|
+
import { ChatBadge } from "./ChatBadge.js";
|
|
6
|
+
// Clases enteras: Tailwind solo genera las que ve escritas.
|
|
7
|
+
const TONE = {
|
|
8
|
+
brand: "text-utility-brand-700",
|
|
9
|
+
sky: "text-utility-sky-700",
|
|
10
|
+
emerald: "text-utility-emerald-700",
|
|
11
|
+
pink: "text-utility-pink-700",
|
|
12
|
+
purple: "text-utility-purple-700",
|
|
13
|
+
orange: "text-utility-orange-700",
|
|
14
|
+
indigo: "text-utility-indigo-700",
|
|
15
|
+
neutral: "text-utility-neutral-700",
|
|
16
|
+
};
|
|
17
|
+
// La máscara solo usa el alfa: negro opaco arriba del 30 %, transparente en el borde.
|
|
18
|
+
const FADE = {
|
|
19
|
+
// color-fijo: máscara de desvanecido (solo cuenta el alfa), como BackgroundPattern
|
|
20
|
+
maskImage: "linear-gradient(to bottom, transparent 0, black 30%)",
|
|
21
|
+
WebkitMaskImage: "linear-gradient(to bottom, transparent 0, black 30%)",
|
|
22
|
+
};
|
|
23
|
+
const contentKey = (m) => `${keyText(m.user)}|${m.text}`;
|
|
24
|
+
/** Chat del directo para overlay: anclado abajo, desvanecido arriba y siempre en oscuro. */
|
|
25
|
+
export function ChatBox({ messages, width, height, translucent = false, fade = true, animate = false, "aria-label": ariaLabel, className, }) {
|
|
26
|
+
const keys = stableKeys(messages, contentKey);
|
|
27
|
+
const size = {};
|
|
28
|
+
if (width != null)
|
|
29
|
+
size.width = width;
|
|
30
|
+
if (height != null)
|
|
31
|
+
size.height = height;
|
|
32
|
+
return (_jsx("div", { role: "log", "aria-live": "polite", "aria-label": ariaLabel ?? defaultStrings.streamChat, className: cx(STREAM_ROOT, "max-w-full overflow-hidden rounded-2xl border border-secondary", width == null && "w-[420px]", height == null && "h-[520px]",
|
|
33
|
+
// Tailwind emite el sólido y, con `@supports (color: color-mix(…))`, la mezcla al 72 %.
|
|
34
|
+
translucent ? "bg-primary_alt/72 backdrop-blur-lg" : "bg-primary_alt", className), style: size, children: _jsx("div", { className: "flex h-full flex-col justify-end gap-3 p-4", style: fade ? FADE : undefined, children: messages.map((m, i) => (_jsxs("div", { className: cx("text-xl wrap-anywhere text-primary", i === messages.length - 1 && animate && streamMotion("enter")), children: [m.badges?.length ? (_jsx("span", { className: "mr-2 inline-flex gap-1 align-[-4px]", children: m.badges.map((b) => (_jsx(ChatBadge, { kind: b }, b))) })) : null, _jsxs("span", { className: cx("font-semibold", TONE[m.tone ?? "neutral"] ?? TONE.neutral), children: [m.user, _jsx("span", { className: "mr-2", children: ":" })] }), _jsx("span", { className: "text-primary", children: m.text })] }, keys[i]))) }) }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type InputHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { type ChoiceSize } from "../internal/choice.js";
|
|
3
|
+
export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
|
|
4
|
+
/** Etiqueta junto a la casilla. */
|
|
5
|
+
label?: ReactNode;
|
|
6
|
+
/** Texto de apoyo bajo la etiqueta. */
|
|
7
|
+
hint?: ReactNode;
|
|
8
|
+
/** Selección parcial: guion en lugar de check y `aria-checked="mixed"`. */
|
|
9
|
+
indeterminate?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Caja de 16px (`sm`) o 20px (`md`).
|
|
12
|
+
* @default "sm"
|
|
13
|
+
*/
|
|
14
|
+
size?: ChoiceSize;
|
|
15
|
+
}
|
|
16
|
+
/** Casilla para activar opciones independientes, con etiqueta y texto de apoyo. */
|
|
17
|
+
export declare function Checkbox({ label, hint, indeterminate, size, id: givenId, className, ...rest }: CheckboxProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef } from "react";
|
|
4
|
+
import { ChoiceText, choiceBox, choiceLabel, hiddenInput } from "../internal/choice.js";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
import { Icon } from "./Icon.js";
|
|
7
|
+
/** Casilla para activar opciones independientes, con etiqueta y texto de apoyo. */
|
|
8
|
+
export function Checkbox({ label, hint, indeterminate, size = "sm", id: givenId, className, ...rest }) {
|
|
9
|
+
const autoId = useId();
|
|
10
|
+
const id = givenId ?? autoId;
|
|
11
|
+
const ref = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (ref.current)
|
|
14
|
+
ref.current.indeterminate = !!indeterminate;
|
|
15
|
+
}, [indeterminate]);
|
|
16
|
+
const markSize = size === "md" ? 14 : 12;
|
|
17
|
+
return (_jsxs("label", { htmlFor: id, className: cx(choiceLabel(size), className), children: [_jsx("input", { ref: ref, id: id, type: "checkbox", "aria-checked": indeterminate ? "mixed" : undefined, ...rest, className: hiddenInput }), _jsx("span", { "aria-hidden": "true", className: cx(choiceBox(size, false), "peer-checked:bg-brand-solid peer-checked:shadow-none peer-focus-visible:shadow-focus-ring peer-disabled:opacity-50", indeterminate && "bg-brand-solid shadow-none"), children: indeterminate ? (_jsx(Icon, { name: "minus", size: markSize, strokeWidth: 1.67 })) : (_jsx("span", { className: "hidden group-has-[:checked]:inline-flex", children: _jsx(Icon, { name: "check", size: markSize, strokeWidth: 1.67 }) })) }), _jsx(ChoiceText, { label: label, hint: hint, size: size, className: "peer-disabled:opacity-50" })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
2
|
+
export interface CloseButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
3
|
+
/** Nombre accesible. Por defecto, "Cerrar". */
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Botón cuadrado de 36px con la X, para cerrar modales, alertas y notificaciones. */
|
|
7
|
+
export declare function CloseButton({ label, className, ...rest }: CloseButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } 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
|
+
/** Botón cuadrado de 36px con la X, para cerrar modales, alertas y notificaciones. */
|
|
6
|
+
export function CloseButton({ label = defaultStrings.close, className, ...rest }) {
|
|
7
|
+
return (_jsx("button", { type: "button", "aria-label": label, ...rest, className: cx("inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-transparent p-2 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", className), children: _jsx(Icon, { name: "x-close", size: 20 }) }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface CodeSnippetTab {
|
|
3
|
+
label: string;
|
|
4
|
+
code: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CodeSnippetProps {
|
|
7
|
+
/** Código de una sola pestaña. */
|
|
8
|
+
code?: string;
|
|
9
|
+
/** Nombre de esa pestaña única (no cambia el resaltado, que es el mismo para todos). */
|
|
10
|
+
language?: string;
|
|
11
|
+
/** Varias versiones del mismo código, cada una en su pestaña. */
|
|
12
|
+
tabs?: CodeSnippetTab[];
|
|
13
|
+
/**
|
|
14
|
+
* Columna de números de línea.
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
lineNumbers?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Trocea el código en comentarios, cadenas, palabras clave y nombres con los colores de la guía. */
|
|
21
|
+
export declare function highlight(src: string): ReactNode[];
|
|
22
|
+
/** Bloque de código con números de línea, resaltado sencillo, pestañas y botón de copiar. */
|
|
23
|
+
export declare function CodeSnippet({ code, language, tabs, lineNumbers, className }: CodeSnippetProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
// Resaltado deliberadamente simple (JS, TS, bash, Python básico), el mismo que la guía
|
|
8
|
+
const KEYWORDS = /^(import|from|export|default|const|let|var|function|return|new|if|else|for|while|class|extends|async|await|try|catch|throw|true|false|null|undefined|def|in|of|as|type|interface)$/;
|
|
9
|
+
const TOKENS = /(\/\/[^\n]*|#[^\n]*|\/\*[\s\S]*?\*\/)|('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`)|([A-Za-z_$][\w$]*)|([\s\S])/g;
|
|
10
|
+
/** Trocea el código en comentarios, cadenas, palabras clave y nombres con los colores de la guía. */
|
|
11
|
+
export function highlight(src) {
|
|
12
|
+
const out = [];
|
|
13
|
+
let k = 0;
|
|
14
|
+
for (const m of src.matchAll(TOKENS)) {
|
|
15
|
+
const at = m.index ?? 0;
|
|
16
|
+
if (m[1])
|
|
17
|
+
out.push(_jsx("span", { className: "text-utility-green-700", children: m[1] }, k++));
|
|
18
|
+
else if (m[2])
|
|
19
|
+
out.push(_jsx("span", { className: "text-primary", children: m[2] }, k++));
|
|
20
|
+
else if (m[3]) {
|
|
21
|
+
const word = m[3];
|
|
22
|
+
const next = /^\s*[:(]/.test(src.slice(at + word.length));
|
|
23
|
+
const prev = src[at - 1];
|
|
24
|
+
if (KEYWORDS.test(word))
|
|
25
|
+
out.push(_jsx("span", { className: "text-utility-pink-600", children: word }, k++));
|
|
26
|
+
else if (/^[A-Z]/.test(word) || prev === "." || next)
|
|
27
|
+
out.push(_jsx("span", { className: "text-utility-blue-600", children: word }, k++));
|
|
28
|
+
else
|
|
29
|
+
out.push(word);
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
out.push(m[4]);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** Bloque de código con números de línea, resaltado sencillo, pestañas y botón de copiar. */
|
|
37
|
+
export function CodeSnippet({ code, language, tabs, lineNumbers = true, className }) {
|
|
38
|
+
const list = tabs ?? [{ label: language ?? defaultStrings.code, code: code ?? "" }];
|
|
39
|
+
const [index, setIndex] = useState(0);
|
|
40
|
+
const [copied, setCopied] = useState(false);
|
|
41
|
+
const timer = useRef(undefined);
|
|
42
|
+
const id = useId();
|
|
43
|
+
const tabRefs = useRef([]);
|
|
44
|
+
const current = list[Math.min(index, list.length - 1)];
|
|
45
|
+
const lines = current.code.replace(/\n$/, "").split("\n");
|
|
46
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
47
|
+
const copy = async () => {
|
|
48
|
+
try {
|
|
49
|
+
await navigator.clipboard.writeText(current.code);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Sin permiso de portapapeles: el botón confirma igual, como en la guía
|
|
53
|
+
}
|
|
54
|
+
setCopied(true);
|
|
55
|
+
clearTimeout(timer.current);
|
|
56
|
+
timer.current = setTimeout(() => setCopied(false), 1500);
|
|
57
|
+
};
|
|
58
|
+
const onTabKey = (e, i) => {
|
|
59
|
+
const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
|
|
60
|
+
if (!step)
|
|
61
|
+
return;
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const next = (i + step + list.length) % list.length;
|
|
64
|
+
setIndex(next);
|
|
65
|
+
tabRefs.current[next]?.focus();
|
|
66
|
+
};
|
|
67
|
+
const multi = list.length > 1;
|
|
68
|
+
return (_jsxs("div", { className: cx("relative overflow-hidden rounded-xl bg-primary font-body shadow-[inset_0_0_0_1px_var(--border-secondary)]", className), children: [multi ? (_jsx("div", { role: "tablist", "aria-label": defaultStrings.code, className: "flex gap-1 bg-secondary px-3 py-2 shadow-[inset_0_-1px_0_var(--border-secondary)]", children: list.map((t, i) => (_jsx("button", { ref: (el) => {
|
|
69
|
+
tabRefs.current[i] = el;
|
|
70
|
+
}, type: "button", role: "tab", id: `${id}-tab-${i}`, "aria-selected": i === index, "aria-controls": `${id}-panel`, tabIndex: i === index ? 0 : -1, onClick: () => setIndex(i), onKeyDown: (e) => onTabKey(e, i), className: "cursor-pointer rounded-md border-0 bg-transparent px-2 py-1 font-body text-sm font-semibold text-quaternary focus-visible:shadow-focus-ring focus-visible:outline-none aria-selected:bg-primary aria-selected:text-primary aria-selected:shadow-[var(--shadow-xs),inset_0_0_0_1px_var(--border-secondary)]", children: t.label }, i))) })) : null, _jsxs("div", { className: "relative flex", ...(multi
|
|
71
|
+
? { role: "tabpanel", id: `${id}-panel`, "aria-labelledby": `${id}-tab-${index}` }
|
|
72
|
+
: {}), children: [lineNumbers ? (_jsx("div", { "aria-hidden": "true", className: "flex-none bg-secondary px-4 py-5 text-right font-mono text-code-sm text-quaternary shadow-[inset_-1px_0_0_var(--border-secondary)] select-none", children: lines.map((_, i) => (_jsx("div", { children: i + 1 }, i))) })) : null, _jsx("pre", {
|
|
73
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: si una línea no cabe, el bloque se desplaza y tiene que poder hacerse con teclado
|
|
74
|
+
tabIndex: 0, className: "m-0 flex-1 overflow-auto p-5 pr-14 font-mono text-code-sm text-primary [tab-size:2] focus-visible:shadow-[inset_0_0_0_2px_var(--focus-ring)] focus-visible:outline-none", children: _jsx("code", { children: highlight(lines.join("\n")) }) }), _jsx("button", { type: "button", onClick: copy, "aria-label": copied ? defaultStrings.copied : defaultStrings.copyCode, className: "absolute top-3 right-3 inline-flex cursor-pointer rounded-md border-0 bg-primary p-1.5 text-fg-quaternary shadow-[inset_0_0_0_1px_var(--border-secondary)] hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:shadow-focus-ring focus-visible:outline-none", children: _jsx(Icon, { name: copied ? "check" : "copy-01", size: 16 }) }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: copied ? defaultStrings.copied : "" })] })] }));
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ColorPickerProps {
|
|
2
|
+
/** Color inicial en hex. Por defecto, el índigo de la marca. */
|
|
3
|
+
defaultValue?: string;
|
|
4
|
+
/** Recibe el hex en minúsculas con `#` en cada cambio. */
|
|
5
|
+
onChange?: (hex: string) => void;
|
|
6
|
+
/** Colores guardados, en hex. */
|
|
7
|
+
swatches?: string[];
|
|
8
|
+
/** @default "Colores guardados" */
|
|
9
|
+
swatchesLabel?: string;
|
|
10
|
+
/** Nombre accesible del campo hexadecimal. @default "Hex" */
|
|
11
|
+
hexLabel?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Selector de color: área de saturación y brillo, barra de tono, campo hex y muestras guardadas. */
|
|
15
|
+
export declare function ColorPicker({ defaultValue, onChange, swatches, swatchesLabel, hexLabel, className, }: ColorPickerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ColorArea, ColorSlider, ColorThumb, I18nProvider, parseColor, SliderTrack, } from "react-aria-components";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
7
|
+
import { Input } from "./Input.js";
|
|
8
|
+
const HEX = /^#?([0-9a-f]{6})$/i;
|
|
9
|
+
const toHsb = (hex) => parseColor(`#${HEX.exec(hex)?.[1] ?? "4f39f6"}`).toFormat("hsb");
|
|
10
|
+
const hexOf = (c) => c.toString("hex").toLowerCase();
|
|
11
|
+
// Mandos blancos de 20px con filo oscuro, legibles sobre cualquier color (ficha de ColorPicker)
|
|
12
|
+
const THUMB =
|
|
13
|
+
// color-fijo: mando blanco con filo negro al 10 % sobre cualquier color (ficha de ColorPicker)
|
|
14
|
+
"size-5 rounded-full border-2 border-[#ffffff] shadow-[var(--shadow-md),0_0_0_1px_#0000001a] data-focus-visible:shadow-[var(--shadow-md),0_0_0_1px_#0000001a,var(--focus-ring-shadow)]";
|
|
15
|
+
// color-fijo: filo negro al 10 % de las muestras, que pueden ser de cualquier color (ficha de ColorPicker)
|
|
16
|
+
const HAIRLINE = "shadow-[inset_0_0_0_1px_#0000001a]";
|
|
17
|
+
/** Selector de color: área de saturación y brillo, barra de tono, campo hex y muestras guardadas. */
|
|
18
|
+
export function ColorPicker({
|
|
19
|
+
// color-fijo: valor inicial, el índigo de marca de la vista previa (ficha de ColorPicker)
|
|
20
|
+
defaultValue = "#4f39f6", onChange, swatches, swatchesLabel = defaultStrings.savedColors, hexLabel = defaultStrings.hex, className, }) {
|
|
21
|
+
const [color, setColor] = useState(() => toHsb(defaultValue));
|
|
22
|
+
// Lo que se está tecleando en el campo hex, hasta que sea un color válido
|
|
23
|
+
const [typing, setTyping] = useState(null);
|
|
24
|
+
const hex = hexOf(color);
|
|
25
|
+
const set = (next) => {
|
|
26
|
+
setColor(next);
|
|
27
|
+
setTyping(null);
|
|
28
|
+
onChange?.(hexOf(next));
|
|
29
|
+
};
|
|
30
|
+
return (_jsx(I18nProvider, { locale: "es-ES", children: _jsxs("div", { className: cx("flex w-80 flex-col gap-4 rounded-2xl bg-primary_alt p-5 font-body shadow-[var(--shadow-xl),inset_0_0_0_1px_var(--border-secondary_alt)]", className), children: [_jsx(ColorArea, { value: color, onChange: set, xChannel: "saturation", yChannel: "brightness", className: "h-50 w-full cursor-crosshair touch-none rounded-lg", children: _jsx(ColorThumb, { className: THUMB }) }), _jsx(ColorSlider, { value: color, onChange: set, channel: "hue", colorSpace: "hsb", children: _jsx(SliderTrack, { className: "h-3 cursor-pointer touch-none rounded-full", children: _jsx(ColorThumb, { className: cx(THUMB, "top-1/2") }) }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: cx("size-9 flex-none rounded-lg", HAIRLINE), style: { background: hex } }), _jsx(Input, { size: "sm", "aria-label": hexLabel, className: "flex-1", value: typing ?? hex.toUpperCase(), onChange: (e) => {
|
|
31
|
+
const v = e.target.value;
|
|
32
|
+
setTyping(v);
|
|
33
|
+
if (HEX.test(v)) {
|
|
34
|
+
const next = toHsb(v);
|
|
35
|
+
setColor(next);
|
|
36
|
+
onChange?.(hexOf(next));
|
|
37
|
+
}
|
|
38
|
+
}, onBlur: () => setTyping(null) })] }), swatches?.length ? (_jsxs("div", { children: [_jsx("div", { className: "mb-2 text-sm font-medium text-secondary", children: swatchesLabel }), _jsx("div", { className: "flex flex-wrap gap-2", children: swatches.map((c) => {
|
|
39
|
+
const on = c.toLowerCase() === hex;
|
|
40
|
+
return (_jsx("button", { type: "button", "aria-label": c.toUpperCase(), "aria-pressed": on, onClick: () => set(toHsb(c)), style: { background: c }, className: cx("size-6 cursor-pointer rounded-full border-0 focus-visible:shadow-focus-ring focus-visible:outline-none", on ? "shadow-focus-ring" : HAIRLINE) }, c));
|
|
41
|
+
}) })] })) : null] }) }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type IconName } from "./Icon.js";
|
|
3
|
+
export interface CommandItem {
|
|
4
|
+
icon?: IconName;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Texto de apoyo tras la etiqueta, en `text-tertiary`. */
|
|
7
|
+
description?: string;
|
|
8
|
+
/** Atajo que se muestra a la derecha (solo texto; no lo registra). */
|
|
9
|
+
shortcut?: string;
|
|
10
|
+
/** Palabras extra para el filtro, separadas por espacios. */
|
|
11
|
+
keywords?: string;
|
|
12
|
+
onSelect?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface CommandGroup {
|
|
15
|
+
label: string;
|
|
16
|
+
items: CommandItem[];
|
|
17
|
+
}
|
|
18
|
+
export interface CommandMenuLabels {
|
|
19
|
+
/** Nombre del diálogo. @default "Menú de comandos" */
|
|
20
|
+
dialog: string;
|
|
21
|
+
/** Nombre del buscador. @default "Buscar comandos" */
|
|
22
|
+
search: string;
|
|
23
|
+
/** Título del estado vacío. @default "Sin resultados" */
|
|
24
|
+
noResults: string;
|
|
25
|
+
/** Pie: "↑ ↓ moverse", "↵ abrir", "esc cerrar". */
|
|
26
|
+
move: string;
|
|
27
|
+
open: string;
|
|
28
|
+
close: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CommandMenuProps {
|
|
31
|
+
/**
|
|
32
|
+
* Visible. Con `false` no se pinta.
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
open?: boolean;
|
|
36
|
+
/** Al pulsar Escape, fuera del panel o tras ejecutar un comando. */
|
|
37
|
+
onClose?: () => void;
|
|
38
|
+
/** Primero las apps de la suite, luego las acciones de la app actual. */
|
|
39
|
+
groups: CommandGroup[];
|
|
40
|
+
/**
|
|
41
|
+
* Placeholder del buscador.
|
|
42
|
+
* @default "Busca o escribe un comando"
|
|
43
|
+
*/
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Texto bajo "Sin resultados".
|
|
47
|
+
* @default "Prueba con otra palabra."
|
|
48
|
+
*/
|
|
49
|
+
emptyText?: ReactNode;
|
|
50
|
+
/** Se pinta dentro de su contenedor (posición relativa) en vez de sobre toda la página, sin atrapar el foco. */
|
|
51
|
+
contained?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Lleva el foco al buscador al abrirse.
|
|
54
|
+
* @default true
|
|
55
|
+
*/
|
|
56
|
+
autoFocus?: boolean;
|
|
57
|
+
/** Textos que trae el componente, para sustituirlos. */
|
|
58
|
+
labels?: Partial<CommandMenuLabels>;
|
|
59
|
+
/** Cada cambio del buscador (ampliación sobre la guía): para pedir resultados a otra fuente, como un índice de búsqueda. */
|
|
60
|
+
onQueryChange?: (query: string) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Filtra los elementos por lo escrito. Con `false`, los grupos se pintan tal cual y el filtro es de la app (ampliación sobre la guía).
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
65
|
+
filter?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Menú de comandos (⌘K) para saltar entre apps de la suite y lanzar acciones escribiendo.
|
|
69
|
+
* Filtra mientras escribes (etiqueta, descripción y `keywords`); ↑ ↓ para moverse, ↵ para ejecutar, Esc para cerrar.
|
|
70
|
+
* El atajo global lo registra la app: este componente solo se abre y se cierra con `open` y `onClose`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function CommandMenu({ open, onClose, groups, placeholder, emptyText, contained, autoFocus, labels, onQueryChange, filter, }: CommandMenuProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { Dialog, Modal, ModalOverlay } from "react-aria-components";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
import { Kbd } from "../internal/kbd.js";
|
|
7
|
+
import { OVERLAY, OVERLAY_POSITION } from "../internal/overlay.js";
|
|
8
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
9
|
+
import { FeaturedIcon } from "./FeaturedIcon.js";
|
|
10
|
+
import { Icon } from "./Icon.js";
|
|
11
|
+
const matches = (item, needle) => !needle ||
|
|
12
|
+
`${item.label} ${item.description ?? ""} ${item.keywords ?? ""}`.toLowerCase().includes(needle);
|
|
13
|
+
/**
|
|
14
|
+
* Menú de comandos (⌘K) para saltar entre apps de la suite y lanzar acciones escribiendo.
|
|
15
|
+
* Filtra mientras escribes (etiqueta, descripción y `keywords`); ↑ ↓ para moverse, ↵ para ejecutar, Esc para cerrar.
|
|
16
|
+
* El atajo global lo registra la app: este componente solo se abre y se cierra con `open` y `onClose`.
|
|
17
|
+
*/
|
|
18
|
+
export function CommandMenu({ open = true, onClose, groups, placeholder, emptyText, contained, autoFocus = true, labels, onQueryChange, filter = true, }) {
|
|
19
|
+
const t = {
|
|
20
|
+
dialog: defaultStrings.commandMenu,
|
|
21
|
+
search: defaultStrings.searchCommands,
|
|
22
|
+
noResults: defaultStrings.noResults,
|
|
23
|
+
move: defaultStrings.toMove,
|
|
24
|
+
open: defaultStrings.toOpen,
|
|
25
|
+
close: defaultStrings.toClose,
|
|
26
|
+
...labels,
|
|
27
|
+
};
|
|
28
|
+
const [query, setQuery] = useState("");
|
|
29
|
+
const [active, setActive] = useState(0);
|
|
30
|
+
const id = useId();
|
|
31
|
+
const listRef = useRef(null);
|
|
32
|
+
const needle = filter ? query.trim().toLowerCase() : "";
|
|
33
|
+
const visible = groups
|
|
34
|
+
.map((g) => ({ label: g.label, items: g.items.filter((it) => matches(it, needle)) }))
|
|
35
|
+
.filter((g) => g.items.length);
|
|
36
|
+
const flat = visible.flatMap((g) => g.items);
|
|
37
|
+
const current = Math.min(active, Math.max(flat.length - 1, 0));
|
|
38
|
+
const optionId = (i) => `${id}-opt-${i}`;
|
|
39
|
+
// Mantiene la opción activa a la vista desplazando solo la lista (scrollIntoView movería también la página)
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const list = listRef.current;
|
|
42
|
+
const el = list?.ownerDocument.getElementById(optionId(current));
|
|
43
|
+
if (!list || !el)
|
|
44
|
+
return;
|
|
45
|
+
const top = el.offsetTop; // la lista es `relative`: offsetTop ya es relativo a ella
|
|
46
|
+
if (top < list.scrollTop)
|
|
47
|
+
list.scrollTop = top;
|
|
48
|
+
else if (top + el.offsetHeight > list.scrollTop + list.clientHeight)
|
|
49
|
+
list.scrollTop = top + el.offsetHeight - list.clientHeight;
|
|
50
|
+
});
|
|
51
|
+
if (!open)
|
|
52
|
+
return null;
|
|
53
|
+
const run = (item) => {
|
|
54
|
+
if (!item)
|
|
55
|
+
return;
|
|
56
|
+
item.onSelect?.();
|
|
57
|
+
onClose?.();
|
|
58
|
+
};
|
|
59
|
+
const onKeyDown = (e) => {
|
|
60
|
+
const n = Math.max(flat.length, 1);
|
|
61
|
+
if (e.key === "ArrowDown") {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
setActive((current + 1) % n);
|
|
64
|
+
}
|
|
65
|
+
else if (e.key === "ArrowUp") {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
setActive((current - 1 + n) % n);
|
|
68
|
+
}
|
|
69
|
+
else if (e.key === "Enter") {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
run(flat[current]);
|
|
72
|
+
}
|
|
73
|
+
else if (e.key === "Escape" && contained) {
|
|
74
|
+
// Fuera de `contained`, Escape lo gestiona el modal de React Aria
|
|
75
|
+
onClose?.();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
let index = -1;
|
|
79
|
+
const listId = `${id}-list`;
|
|
80
|
+
const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-2 p-4 text-fg-quaternary shadow-[inset_0_-1px_0_var(--border-secondary)]", children: [_jsx(Icon, { name: "search-lg", size: 20 }), _jsx("input", {
|
|
81
|
+
// biome-ignore lint/a11y/noAutofocus: el buscador es lo único que hay que hacer al abrir el menú
|
|
82
|
+
autoFocus: autoFocus, value: query, placeholder: placeholder ?? defaultStrings.commandPlaceholder, "aria-label": t.search, role: "combobox", "aria-expanded": flat.length > 0, "aria-controls": flat.length ? listId : undefined, "aria-autocomplete": "list", "aria-activedescendant": flat.length ? optionId(current) : undefined, onChange: (e) => {
|
|
83
|
+
setQuery(e.target.value);
|
|
84
|
+
setActive(0);
|
|
85
|
+
onQueryChange?.(e.target.value);
|
|
86
|
+
}, onKeyDown: onKeyDown, className: "min-w-0 flex-1 border-0 bg-transparent px-0.5 py-px font-body text-md text-primary outline-none placeholder:text-placeholder" }), _jsx(Kbd, { children: "\u2318K" })] }), flat.length ? (_jsx("div", { ref: listRef, id: listId, role: "listbox", "aria-label": t.dialog, className: "relative max-h-[360px] overflow-auto", children: visible.map((g, gi) => (_jsxs("div", { role: "group", "aria-labelledby": `${id}-g${gi}`, className: "py-2 shadow-[inset_0_-1px_0_var(--border-secondary)]", children: [_jsx("div", { id: `${id}-g${gi}`, className: "px-[18px] pt-2 pb-1 text-xs font-semibold text-primary", children: g.label }), g.items.map((item) => {
|
|
87
|
+
index++;
|
|
88
|
+
const i = index;
|
|
89
|
+
return (_jsxs("div", { id: optionId(i), role: "option", "aria-selected": i === current, tabIndex: -1, onMouseEnter: () => setActive(i), onClick: () => run(item), className: "mx-2 my-0.5 flex cursor-pointer items-center gap-2 rounded-lg py-2 pr-2 pl-2.5 text-sm font-medium text-secondary aria-selected:bg-primary_hover aria-selected:text-secondary_hover [&_svg]:text-fg-quaternary", children: [item.icon ? _jsx(Icon, { name: item.icon, size: 20 }) : null, _jsxs("span", { className: "flex min-w-0 flex-1 items-baseline gap-2", children: [_jsx("span", { className: "shrink-0 whitespace-nowrap", children: item.label }), item.description ? (_jsx("span", { className: "truncate font-normal text-tertiary", children: item.description })) : null] }), item.shortcut ? _jsx(Kbd, { children: item.shortcut }) : null] }, optionId(i)));
|
|
90
|
+
})] }, gi))) })) : (_jsxs("div", { role: "status", className: "flex flex-col items-center gap-2 px-4 py-8 text-center", children: [_jsx(FeaturedIcon, { icon: "search-lg", variant: "modern", color: "gray", size: "md" }), _jsx("div", { className: "m-0 text-md font-semibold text-primary", children: t.noResults }), _jsx("div", { className: "mt-1 max-w-[352px] text-sm text-tertiary", children: emptyText ?? defaultStrings.tryAnother })] })), _jsxs("div", { className: "flex gap-4 bg-secondary px-4 py-3 text-xs text-tertiary", children: [_jsxs("span", { children: [_jsx(Kbd, { className: "mr-0.5 bg-primary", children: "\u2191" }), _jsx(Kbd, { className: "mr-0.5 bg-primary", children: "\u2193" }), " ", t.move] }), _jsxs("span", { children: [_jsx(Kbd, { className: "mr-0.5 bg-primary", children: "\u21B5" }), " ", t.open] }), _jsxs("span", { children: [_jsx(Kbd, { className: "mr-0.5 bg-primary", children: "esc" }), " ", t.close] })] })] }));
|
|
91
|
+
const PANEL = "w-full max-w-[640px] overflow-hidden rounded-xl bg-primary_alt font-body shadow-[var(--shadow-xl),inset_0_0_0_1px_var(--border-secondary_alt)] outline-none";
|
|
92
|
+
if (contained) {
|
|
93
|
+
return (_jsx("div", { className: cx(OVERLAY, OVERLAY_POSITION(true), "items-start justify-center pt-6"), onClick: (e) => {
|
|
94
|
+
if (e.target === e.currentTarget)
|
|
95
|
+
onClose?.();
|
|
96
|
+
}, children: _jsx("div", { role: "dialog", "aria-label": t.dialog, className: PANEL, children: body }) }));
|
|
97
|
+
}
|
|
98
|
+
return (_jsx(ModalOverlay, { isOpen: true, isDismissable: true, onOpenChange: (next) => {
|
|
99
|
+
if (!next)
|
|
100
|
+
onClose?.();
|
|
101
|
+
}, className: cx(OVERLAY, OVERLAY_POSITION(false), "items-start justify-center pt-40"), children: _jsx(Modal, { className: PANEL, children: _jsx(Dialog, { "aria-label": t.dialog, className: "outline-none", children: body }) }) }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface ContentDividerProps {
|
|
3
|
+
/** Texto en el centro, como "o continúa con" o una fecha. */
|
|
4
|
+
label?: ReactNode;
|
|
5
|
+
/** El texto como título (`text-md` semibold en `text-primary`) en lugar de `text-sm` en `text-tertiary`. */
|
|
6
|
+
heading?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* `single` una línea · `dual` dos líneas · `fill` banda en `bg-secondary`.
|
|
9
|
+
* @default "single"
|
|
10
|
+
*/
|
|
11
|
+
variant?: "single" | "dual" | "fill";
|
|
12
|
+
/** Un `Button` en el centro, como "Cargar más". Sustituye a `label`. */
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Separador horizontal, solo o con texto o un botón en el centro. */
|
|
17
|
+
export declare function ContentDivider({ label, heading, variant, children, className, }: ContentDividerProps): 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
|
+
const LINE = {
|
|
4
|
+
single: "border-t border-secondary",
|
|
5
|
+
dual: "h-[5px] border-y border-secondary",
|
|
6
|
+
fill: "h-8 bg-secondary",
|
|
7
|
+
};
|
|
8
|
+
/** Separador horizontal, solo o con texto o un botón en el centro. */
|
|
9
|
+
export function ContentDivider({ label, heading, variant = "single", children, className, }) {
|
|
10
|
+
const mid = children ? (children) : label ? (_jsx("span", { className: cx("whitespace-nowrap", heading ? "text-md font-semibold text-primary" : "text-sm font-medium text-tertiary", variant === "fill" && "inline-flex h-8 items-center bg-secondary px-2"), children: label })) : null;
|
|
11
|
+
const line = _jsx("span", { "aria-hidden": "true", className: cx("flex-1", LINE[variant]) });
|
|
12
|
+
return (_jsxs("div", { role: mid ? undefined : "separator", className: cx("flex items-center font-body", variant === "fill" ? "gap-0" : "gap-2", className), children: [line, mid, mid ? line : null] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
export type CreditCardTheme = "brand-dark" | "brand-light" | "gray-dark" | "gray-light" | "transparent";
|
|
3
|
+
export interface CreditCardProps {
|
|
4
|
+
/**
|
|
5
|
+
* Acabado de la tarjeta. Sus colores son fijos: es un objeto y no cambia con el tema.
|
|
6
|
+
* @default "brand-dark"
|
|
7
|
+
*/
|
|
8
|
+
theme?: CreditCardTheme;
|
|
9
|
+
/** Texto arriba a la izquierda, en Bricolage Grotesque. @default "jotis.me" */
|
|
10
|
+
company?: ReactNode;
|
|
11
|
+
/** Titular, en mayúsculas. @default "ADMINISTRADOR" */
|
|
12
|
+
holder?: string;
|
|
13
|
+
/** Caducidad, como "06/29". */
|
|
14
|
+
expiry?: string;
|
|
15
|
+
/** Los cuatro últimos dígitos: nunca se muestra el número entero. @default "1234" */
|
|
16
|
+
last4?: string;
|
|
17
|
+
/** Red de pago como texto, abajo a la derecha. Sin logotipos. */
|
|
18
|
+
network?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Nombre para lectores de pantalla (ampliación sobre la guía). Por defecto resume los datos:
|
|
21
|
+
* "Tarjeta Pecunia terminada en 4821, titular Administrador, caduca 06/29".
|
|
22
|
+
*/
|
|
23
|
+
"aria-label"?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
}
|
|
27
|
+
/** Maqueta de tarjeta de pago (316×190) para métodos de pago, onboarding o capturas. Es una imagen con nombre. */
|
|
28
|
+
export declare function CreditCard({ theme, company, holder, expiry, last4, network, className, style, ...rest }: CreditCardProps): import("react").JSX.Element;
|