@jotis-me/react 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +15 -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 +26 -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 +17 -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 +101 -0
- package/dist/internal/strings.js +103 -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,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../internal/cx.js";
|
|
3
|
+
// Colores fijos de la ficha: la tarjeta es un objeto, no parte de la interfaz.
|
|
4
|
+
const THEME = {
|
|
5
|
+
"brand-dark": {
|
|
6
|
+
card: "bg-[linear-gradient(45deg,#312c85,#432dd7)] shadow-lg",
|
|
7
|
+
fg: "text-[#ffffff]",
|
|
8
|
+
sub: "text-[#ffffffcc]",
|
|
9
|
+
edge: true,
|
|
10
|
+
darkChip: false,
|
|
11
|
+
},
|
|
12
|
+
"brand-light": {
|
|
13
|
+
card: "bg-[linear-gradient(45deg,#c6d2ff,#e0e7ff)] shadow-lg",
|
|
14
|
+
fg: "text-[#312c85]",
|
|
15
|
+
sub: "text-[#432dd7]",
|
|
16
|
+
edge: false,
|
|
17
|
+
darkChip: true,
|
|
18
|
+
},
|
|
19
|
+
"gray-dark": {
|
|
20
|
+
card: "bg-[linear-gradient(45deg,#0a0a0a,#404040)] shadow-lg",
|
|
21
|
+
fg: "text-[#ffffff]",
|
|
22
|
+
sub: "text-[#ffffffcc]",
|
|
23
|
+
edge: true,
|
|
24
|
+
darkChip: false,
|
|
25
|
+
},
|
|
26
|
+
"gray-light": {
|
|
27
|
+
card: "bg-[#f5f5f5] shadow-[var(--shadow-lg),inset_0_0_0_1px_#e5e5e5]",
|
|
28
|
+
fg: "text-[#404040]",
|
|
29
|
+
sub: "text-[#525252]",
|
|
30
|
+
edge: false,
|
|
31
|
+
darkChip: true,
|
|
32
|
+
},
|
|
33
|
+
transparent: {
|
|
34
|
+
card: "bg-[linear-gradient(135deg,#ffffff47,#ffffff0f)] shadow-lg backdrop-blur-lg",
|
|
35
|
+
fg: "text-[#ffffff]",
|
|
36
|
+
sub: "text-[#ffffffcc]",
|
|
37
|
+
edge: true,
|
|
38
|
+
darkChip: false,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
// Canto: borde degradado blanco (0,5 → 0,08) recortado al anillo de 1px
|
|
42
|
+
const EDGE = {
|
|
43
|
+
padding: 1,
|
|
44
|
+
background: "linear-gradient(135deg, #ffffff80, #ffffff14)",
|
|
45
|
+
mask: "linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)",
|
|
46
|
+
WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
|
|
47
|
+
WebkitMaskComposite: "xor",
|
|
48
|
+
};
|
|
49
|
+
function Contactless({ className }) {
|
|
50
|
+
return (_jsxs("svg", { "aria-hidden": "true", width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", className: className, children: [_jsx("path", { d: "M8.5 8.5a5 5 0 0 1 0 7" }), _jsx("path", { d: "M12 6a8.5 8.5 0 0 1 0 12" }), _jsx("path", { d: "M15.5 3.5a12 12 0 0 1 0 17" })] }));
|
|
51
|
+
}
|
|
52
|
+
/** Maqueta de tarjeta de pago (316×190) para métodos de pago, onboarding o capturas. Es una imagen con nombre. */
|
|
53
|
+
export function CreditCard({ theme = "brand-dark", company = "jotis.me", holder = "ADMINISTRADOR", expiry, last4 = "1234", network, className, style, ...rest }) {
|
|
54
|
+
const t = THEME[theme];
|
|
55
|
+
const last = String(last4).slice(-4);
|
|
56
|
+
const label = rest["aria-label"] ??
|
|
57
|
+
`Tarjeta ${typeof company === "string" ? `${company} ` : ""}terminada en ${last}` +
|
|
58
|
+
(holder ? `, titular ${holder}` : "") +
|
|
59
|
+
(expiry ? `, caduca ${expiry}` : "");
|
|
60
|
+
return (_jsxs("div", { role: "img", "aria-label": label, className: cx("relative isolate flex h-[190px] w-[316px] shrink-0 flex-col overflow-hidden rounded-2xl py-4 pr-4 pl-5 font-body", t.card, t.fg, className), style: style, children: [t.edge ? (_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 -z-10 rounded-[inherit]", style: EDGE })) : null, _jsxs("div", { className: "flex items-start justify-between", children: [_jsx("span", { className: "font-display text-md font-semibold tracking-[-0.01em]", children: company }), _jsx(Contactless, { className: t.sub })] }), _jsx("div", { className: cx("mt-3.5 h-7 w-9 rounded-md", t.darkChip
|
|
61
|
+
? "bg-[linear-gradient(135deg,#0000001f,#0000000a)] shadow-[inset_0_0_0_1px_#00000014]"
|
|
62
|
+
: "bg-[linear-gradient(135deg,#ffffff8c,#ffffff26)] shadow-[inset_0_0_0_1px_#ffffff59]") }), _jsxs("div", { className: "mt-auto flex items-end gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("div", { className: cx("flex justify-between gap-2 text-xs font-semibold tracking-[0.05em]", t.sub), children: [_jsx("span", { className: "truncate uppercase", children: holder }), expiry ? _jsx("span", { children: expiry }) : null] }), _jsxs("span", { className: "text-md font-medium tracking-[0.06em] tabular-nums", children: ["\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 ", last] })] }), network ? (_jsx("span", { className: "rounded-md bg-[#ffffff1a] px-2 py-1 text-sm/6 font-bold italic", children: network })) : null] })] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface DateRange {
|
|
2
|
+
start: Date | null;
|
|
3
|
+
end?: Date | null;
|
|
4
|
+
}
|
|
5
|
+
export interface DatePickerPreset {
|
|
6
|
+
label: string;
|
|
7
|
+
start: Date;
|
|
8
|
+
/** Sin `end`, el preset es un solo día. */
|
|
9
|
+
end?: Date;
|
|
10
|
+
}
|
|
11
|
+
export interface DatePickerLabels {
|
|
12
|
+
/** Texto del disparador sin fecha y sin `placeholder`, con una fecha. */
|
|
13
|
+
pickDate: string;
|
|
14
|
+
/** Texto del disparador sin fecha y sin `placeholder`, con rango. */
|
|
15
|
+
pickDates: string;
|
|
16
|
+
/** Nombre accesible del panel. */
|
|
17
|
+
dialog: string;
|
|
18
|
+
/** Nombre accesible de la cuadrícula del mes. */
|
|
19
|
+
calendar: string;
|
|
20
|
+
previousMonth: string;
|
|
21
|
+
nextMonth: string;
|
|
22
|
+
cancel: string;
|
|
23
|
+
apply: string;
|
|
24
|
+
}
|
|
25
|
+
export interface DatePickerProps {
|
|
26
|
+
/** Elige un rango (inicio y fin) en lugar de una fecha. */
|
|
27
|
+
range?: boolean;
|
|
28
|
+
/** Valor inicial: `{ start, end }`; con una fecha, solo `start`. */
|
|
29
|
+
defaultValue?: DateRange;
|
|
30
|
+
/** Se llama al pulsar Aplicar, no al elegir cada día. */
|
|
31
|
+
onApply?: (value: DateRange) => void;
|
|
32
|
+
/** Atajos a la izquierda del calendario: "Este mes", "Mes pasado". */
|
|
33
|
+
presets?: DatePickerPreset[];
|
|
34
|
+
/** Texto del disparador mientras no hay fecha aplicada. */
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
/** Abre o cierra el panel desde fuera. */
|
|
37
|
+
open?: boolean;
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
/** Pinta el panel sin botón ni ventana flotante. */
|
|
40
|
+
inline?: boolean;
|
|
41
|
+
/** Día marcado como hoy. Por defecto, la fecha del sistema. */
|
|
42
|
+
today?: Date;
|
|
43
|
+
/** Textos sustituibles. */
|
|
44
|
+
labels?: Partial<DatePickerLabels>;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Selector de una fecha o de un rango, con presets y botones de aplicar y cancelar. */
|
|
48
|
+
export declare function DatePicker({ range, defaultValue, onApply, presets, placeholder, open: openProp, defaultOpen, inline, today: todayProp, labels, className, }: DatePickerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { CalendarDate, isSameDay } from "@internationalized/date";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Button as AriaButton, Calendar as AriaCalendar, RangeCalendar as AriaRangeCalendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Dialog, I18nProvider, Popover, } from "react-aria-components";
|
|
6
|
+
import { cx } from "../internal/cx.js";
|
|
7
|
+
import { formatDate, formatMonth } from "../internal/format.js";
|
|
8
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
9
|
+
import { useControllable } from "../internal/use-controllable.js";
|
|
10
|
+
import { Button } from "./Button.js";
|
|
11
|
+
import { Icon } from "./Icon.js";
|
|
12
|
+
const DEFAULT_LABELS = {
|
|
13
|
+
pickDate: defaultStrings.pickDate,
|
|
14
|
+
pickDates: defaultStrings.pickDates,
|
|
15
|
+
dialog: defaultStrings.datePicker,
|
|
16
|
+
calendar: defaultStrings.calendar,
|
|
17
|
+
previousMonth: defaultStrings.previousMonth,
|
|
18
|
+
nextMonth: defaultStrings.nextMonth,
|
|
19
|
+
cancel: defaultStrings.cancel,
|
|
20
|
+
apply: defaultStrings.apply,
|
|
21
|
+
};
|
|
22
|
+
const day = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
23
|
+
const toCalendar = (d) => new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
|
24
|
+
const toDate = (c) => new Date(c.year, c.month - 1, c.day);
|
|
25
|
+
const sameDay = (a, b) => !!a && !!b && a.getTime() === b.getTime();
|
|
26
|
+
// "lun" → "Lu", "sáb" → "Sá": las iniciales de dos letras de la guía
|
|
27
|
+
const weekday = (d) => d.charAt(0).toUpperCase() + d.slice(1, 2);
|
|
28
|
+
const NAV = "inline-flex cursor-pointer rounded-lg border-0 bg-transparent p-2 text-fg-quaternary outline-none transition-colors duration-100 data-hovered:bg-primary_hover data-hovered:text-fg-quaternary_hover data-focus-visible:shadow-focus-ring";
|
|
29
|
+
const TODAY_DOT = "after:absolute after:bottom-[5px] after:left-1/2 after:-ml-[2.5px] after:size-[5px] after:rounded-full after:content-['']";
|
|
30
|
+
// Extremos del rango: círculo de marca sobre media celda del tono del tramo, hacia dentro
|
|
31
|
+
const RANGE_START = "rounded-none bg-transparent bg-[radial-gradient(circle,var(--bg-brand-solid)_19.5px,transparent_20px),linear-gradient(to_right,transparent_50%,var(--bg-secondary_hover)_50%)]";
|
|
32
|
+
const RANGE_END = "rounded-none bg-transparent bg-[radial-gradient(circle,var(--bg-brand-solid)_19.5px,transparent_20px),linear-gradient(to_left,transparent_50%,var(--bg-secondary_hover)_50%)]";
|
|
33
|
+
function cellClass(range, today) {
|
|
34
|
+
return ({ date, isSelected, isSelectionStart, isSelectionEnd, isOutsideMonth, }) => {
|
|
35
|
+
const start = range && isSelectionStart;
|
|
36
|
+
const end = range && isSelectionEnd;
|
|
37
|
+
const selected = range ? start || end : isSelected;
|
|
38
|
+
const between = range && isSelected && !start && !end;
|
|
39
|
+
const isToday = isSameDay(date, today);
|
|
40
|
+
return cx("relative flex size-10 cursor-pointer items-center justify-center rounded-full text-sm text-secondary outline-none transition-colors duration-100 data-focus-visible:shadow-focus-ring", !selected && !between && "data-hovered:bg-primary_hover", isToday && `bg-tertiary font-medium ${TODAY_DOT} after:bg-fg-brand-primary`, between && "rounded-none bg-secondary_hover", selected && "font-medium text-white after:bg-fg-white", selected && start === end && "bg-brand-solid", start && !end && RANGE_START, end && !start && RANGE_END, isOutsideMonth && "hidden");
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Selector de una fecha o de un rango, con presets y botones de aplicar y cancelar. */
|
|
44
|
+
export function DatePicker({ range = false, defaultValue, onApply, presets, placeholder, open: openProp, defaultOpen = false, inline, today: todayProp, labels, className, }) {
|
|
45
|
+
const t = { ...DEFAULT_LABELS, ...labels };
|
|
46
|
+
const initial = {
|
|
47
|
+
start: defaultValue?.start ? day(defaultValue.start) : null,
|
|
48
|
+
end: range && defaultValue?.end ? day(defaultValue.end) : null,
|
|
49
|
+
};
|
|
50
|
+
const [draft, setDraft] = useState(initial);
|
|
51
|
+
const [applied, setApplied] = useState(initial);
|
|
52
|
+
const [open, setOpen] = useControllable(openProp, defaultOpen);
|
|
53
|
+
const today = toCalendar(todayProp ?? new Date());
|
|
54
|
+
const [focused, setFocused] = useState(() => (initial.start ? toCalendar(initial.start) : today));
|
|
55
|
+
const triggerRef = useRef(null);
|
|
56
|
+
const complete = !!draft.start && (!range || !!draft.end);
|
|
57
|
+
const close = () => setOpen(false);
|
|
58
|
+
// Al cerrar, el foco vuelve al disparador; antes no se puede, el resto de la página está inerte mientras el panel está abierto
|
|
59
|
+
const wasOpen = useRef(open);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (wasOpen.current && !open && !inline)
|
|
62
|
+
triggerRef.current?.querySelector("button")?.focus();
|
|
63
|
+
wasOpen.current = open;
|
|
64
|
+
}, [open, inline]);
|
|
65
|
+
const cancel = () => {
|
|
66
|
+
setDraft(applied);
|
|
67
|
+
setFocused(applied.start ? toCalendar(applied.start) : today);
|
|
68
|
+
close();
|
|
69
|
+
};
|
|
70
|
+
const apply = () => {
|
|
71
|
+
setApplied(draft);
|
|
72
|
+
close();
|
|
73
|
+
onApply?.(range ? draft : { start: draft.start });
|
|
74
|
+
};
|
|
75
|
+
const choosePreset = (p) => {
|
|
76
|
+
const start = day(p.start);
|
|
77
|
+
setDraft({ start, end: range ? day(p.end ?? p.start) : null });
|
|
78
|
+
setFocused(toCalendar(start));
|
|
79
|
+
};
|
|
80
|
+
const calendarBody = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mb-3 flex items-center justify-between", children: [_jsx(AriaButton, { slot: "previous", "aria-label": t.previousMonth, className: NAV, children: _jsx(Icon, { name: "chevron-left", size: 20 }) }), _jsx("span", { className: "text-md font-semibold text-secondary", children: formatMonth(toDate(focused)) }), _jsx(AriaButton, { slot: "next", "aria-label": t.nextMonth, className: NAV, children: _jsx(Icon, { name: "chevron-right", size: 20 }) })] }), _jsxs(CalendarGrid, { weekdayStyle: "short", className: "-my-1 border-separate border-spacing-x-0 border-spacing-y-1", children: [_jsx(CalendarGridHeader, { children: (d) => (_jsx(CalendarHeaderCell, { className: "size-10 p-0 text-sm font-medium text-secondary", children: weekday(d) })) }), _jsx(CalendarGridBody, { children: (date) => _jsx(CalendarCell, { date: date, className: cellClass(range, today) }) })] })] }));
|
|
81
|
+
const calendarProps = {
|
|
82
|
+
"aria-label": t.calendar,
|
|
83
|
+
firstDayOfWeek: "mon",
|
|
84
|
+
focusedValue: focused,
|
|
85
|
+
onFocusChange: setFocused,
|
|
86
|
+
autoFocus: !inline,
|
|
87
|
+
className: "px-6 py-5",
|
|
88
|
+
};
|
|
89
|
+
const dateBox = (d) => (_jsx("span", { className: "min-w-26 rounded-lg px-3 py-2 text-sm text-primary shadow-[var(--shadow-xs),inset_0_0_0_1px_var(--border-primary)]", children: d ? formatDate(d) : "—" }));
|
|
90
|
+
const panel = (_jsxs("div", { className: cx("overflow-hidden rounded-2xl bg-primary font-body shadow-[var(--shadow-xl),inset_0_0_0_1px_var(--border-secondary_alt)]", inline ? "inline-flex" : "flex"), children: [presets?.length ? (_jsx("div", { className: "flex w-38 flex-none flex-col gap-0.5 p-3 shadow-[inset_-1px_0_0_var(--border-secondary)]", children: presets.map((p) => {
|
|
91
|
+
const on = sameDay(draft.start, day(p.start)) &&
|
|
92
|
+
(!range || sameDay(draft.end, day(p.end ?? p.start)));
|
|
93
|
+
return (_jsx("button", { type: "button", "aria-pressed": on, onClick: () => choosePreset(p), className: cx("cursor-pointer rounded-md border-0 bg-transparent px-3 py-2 text-left text-sm font-medium text-secondary transition-colors duration-100 hover:bg-primary_hover hover:text-secondary_hover focus-visible:shadow-focus-ring focus-visible:outline-none", on && "bg-primary_hover text-secondary_hover"), children: p.label }, p.label));
|
|
94
|
+
}) })) : null, _jsxs("div", { className: "flex flex-col", children: [_jsx(I18nProvider, { locale: "es-ES", children: range ? (_jsx(AriaRangeCalendar, { ...calendarProps, value: draft.start && draft.end
|
|
95
|
+
? { start: toCalendar(draft.start), end: toCalendar(draft.end) }
|
|
96
|
+
: null, onChange: (v) => v && setDraft({ start: toDate(v.start), end: toDate(v.end) }), children: calendarBody })) : (_jsx(AriaCalendar, { ...calendarProps, value: draft.start ? toCalendar(draft.start) : null, onChange: (v) => setDraft({ start: toDate(v), end: null }), children: calendarBody })) }), _jsxs("div", { className: "flex items-center justify-between gap-3 p-4 shadow-[inset_0_1px_0_var(--border-secondary)]", children: [_jsxs("div", { className: "flex items-center gap-2", children: [dateBox(draft.start), range ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-quaternary", children: "\u2013" }), dateBox(draft.end)] })) : null] }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { hierarchy: "secondary", size: "sm", onClick: cancel, children: t.cancel }), _jsx(Button, { size: "sm", disabled: !complete, onClick: apply, children: t.apply })] })] })] })] }));
|
|
97
|
+
if (inline)
|
|
98
|
+
return (_jsx("fieldset", { "aria-label": t.dialog, className: cx("m-0 inline-flex min-w-0 border-0 p-0", className), children: panel }));
|
|
99
|
+
const label = applied.start
|
|
100
|
+
? range && applied.end
|
|
101
|
+
? `${formatDate(applied.start)} – ${formatDate(applied.end)}`
|
|
102
|
+
: formatDate(applied.start)
|
|
103
|
+
: placeholder || (range ? t.pickDates : t.pickDate);
|
|
104
|
+
return (_jsxs("div", { ref: triggerRef, className: cx("inline-flex", className), children: [_jsx(Button, { hierarchy: "secondary", size: "sm", iconLeading: "calendar", "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => setOpen(!open), className: applied.start ? undefined : "text-placeholder", children: label }), _jsx(Popover, { triggerRef: triggerRef, isOpen: open, onOpenChange: (next) => (next ? setOpen(true) : cancel()), placement: "bottom start", offset: 8, children: _jsx(Dialog, { "aria-label": t.dialog, className: "outline-none", children: panel }) })] }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type IconName } from "./Icon.js";
|
|
3
|
+
export interface DropdownAction {
|
|
4
|
+
icon?: IconName;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
/** Atajo de teclado que se muestra a la derecha (solo texto; no lo registra). */
|
|
7
|
+
shortcut?: string;
|
|
8
|
+
/** Acción destructiva: va al final, tras un separador. */
|
|
9
|
+
danger?: boolean;
|
|
10
|
+
onSelect?: () => void;
|
|
11
|
+
/** Texto para la búsqueda por teclado si `label` no es texto. */
|
|
12
|
+
textValue?: string;
|
|
13
|
+
}
|
|
14
|
+
export type DropdownItem = DropdownAction | {
|
|
15
|
+
divider: true;
|
|
16
|
+
};
|
|
17
|
+
export interface DropdownProps {
|
|
18
|
+
/**
|
|
19
|
+
* Texto del botón que abre el menú.
|
|
20
|
+
* @default "Opciones"
|
|
21
|
+
*/
|
|
22
|
+
label?: ReactNode;
|
|
23
|
+
/** Disparador propio en lugar del botón: un elemento que pase `ref` y props al DOM, como un `Button` solo icono. */
|
|
24
|
+
trigger?: ReactNode;
|
|
25
|
+
/** Cabecera del panel, por ejemplo un `AvatarLabel`. */
|
|
26
|
+
header?: ReactNode;
|
|
27
|
+
/** Abierto (controlado). */
|
|
28
|
+
open?: boolean;
|
|
29
|
+
/** Abierto al principio (no controlado). Así abierto no se lleva el foco ni bloquea la página. */
|
|
30
|
+
defaultOpen?: boolean;
|
|
31
|
+
/** Avisa al abrir y cerrar (ampliación sobre la guía, para el modo controlado). */
|
|
32
|
+
onOpenChange?: (open: boolean) => void;
|
|
33
|
+
items: DropdownItem[];
|
|
34
|
+
/** Nombre del menú para lectores de pantalla. Por defecto, el texto del botón. */
|
|
35
|
+
"aria-label"?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Menú de acciones que se abre desde un botón. La acción destructiva va al final, tras un separador, con `danger`. */
|
|
39
|
+
export declare function Dropdown({ label, trigger, header, open: openProp, defaultOpen, onOpenChange, items, className, ...rest }: DropdownProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement, useRef, useState } from "react";
|
|
4
|
+
import { Menu, MenuItem, MenuTrigger, Popover, Pressable, Separator } from "react-aria-components";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
import { Kbd } from "../internal/kbd.js";
|
|
7
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
8
|
+
import { useControllable } from "../internal/use-controllable.js";
|
|
9
|
+
import { Button } from "./Button.js";
|
|
10
|
+
import { Icon } from "./Icon.js";
|
|
11
|
+
const PANEL = "min-w-60 rounded-lg bg-primary_alt py-1 font-body shadow-[var(--shadow-lg),inset_0_0_0_1px_var(--border-secondary_alt)] outline-none";
|
|
12
|
+
const isDivider = (item) => "divider" in item;
|
|
13
|
+
/** Menú de acciones que se abre desde un botón. La acción destructiva va al final, tras un separador, con `danger`. */
|
|
14
|
+
export function Dropdown({ label, trigger, header, open: openProp, defaultOpen = false, onOpenChange, items, className, ...rest }) {
|
|
15
|
+
const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);
|
|
16
|
+
// Abierto por el usuario: menú modal con el foco dentro, y React Aria lo devuelve al disparador al cerrar.
|
|
17
|
+
// Abierto de inicio (defaultOpen): sin foco ni bloqueo de la página.
|
|
18
|
+
const [byUser, setByUser] = useState(false);
|
|
19
|
+
// MenuTrigger abre (no alterna) al pulsar con ratón; el menú abierto de inicio no es modal y nada lo cerraría.
|
|
20
|
+
const closeOnPress = useRef(false);
|
|
21
|
+
const menuLabel = rest["aria-label"] ?? (typeof label === "string" ? label : defaultStrings.options);
|
|
22
|
+
const panel = (_jsxs(_Fragment, { children: [header ? (_jsx("div", { className: "mb-1 flex items-center gap-3 px-4 py-3 shadow-[inset_0_-1px_0_var(--border-secondary)]", children: header })) : null, _jsx(Menu, { "aria-label": menuLabel, ...(byUser ? {} : { autoFocus: false }), className: "outline-none", children: items.map((item, i) => isDivider(item) ? (_jsx(Separator
|
|
23
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: los separadores no tienen otra identidad
|
|
24
|
+
, { className: "my-1 h-px border-0 bg-[var(--border-secondary)]" }, `sep-${i}`)) : (_jsxs(MenuItem
|
|
25
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: las etiquetas pueden ser nodos o repetirse
|
|
26
|
+
, { id: i, textValue: item.textValue ?? (typeof item.label === "string" ? item.label : undefined), onAction: () => item.onSelect?.(), className: cx("mx-1.5 my-px flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-[9px] text-sm font-medium outline-none data-focused:bg-primary_hover", item.danger
|
|
27
|
+
? "text-error-primary [&_svg]:text-fg-error-secondary"
|
|
28
|
+
: "text-secondary data-focused:text-secondary_hover [&_svg]:text-fg-quaternary"), children: [item.icon ? _jsx(Icon, { name: item.icon, size: 16 }) : null, item.label, item.shortcut ? _jsx(Kbd, { className: "ml-auto", children: item.shortcut }) : null] }, i))) })] }));
|
|
29
|
+
return (_jsx("div", { className: cx("relative inline-block", className), children: _jsxs(MenuTrigger, { isOpen: open, onOpenChange: (next) => {
|
|
30
|
+
setByUser(next);
|
|
31
|
+
setOpen(next);
|
|
32
|
+
}, children: [_jsx(Pressable, { onPressStart: () => {
|
|
33
|
+
closeOnPress.current = open && !byUser;
|
|
34
|
+
}, onPress: () => {
|
|
35
|
+
if (closeOnPress.current)
|
|
36
|
+
setOpen(false);
|
|
37
|
+
closeOnPress.current = false;
|
|
38
|
+
}, children: trigger && isValidElement(trigger) ? trigger : (_jsx(Button, { hierarchy: "secondary", iconTrailing: "chevron-down", children: label ?? defaultStrings.options })) }), open && !byUser ? (_jsx("div", { className: cx("absolute top-[calc(100%+4px)] left-0 z-30", PANEL), children: panel })) : (_jsx(Popover, { placement: "bottom start", offset: 4, className: cx("z-30", PANEL), children: panel }))] }) }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { IconName } from "./Icon.js";
|
|
3
|
+
export interface EmptyStateProps {
|
|
4
|
+
/**
|
|
5
|
+
* Icono destacado (`FeaturedIcon` modern gris de 48px).
|
|
6
|
+
* @default "search-lg"
|
|
7
|
+
*/
|
|
8
|
+
icon?: IconName;
|
|
9
|
+
/** Qué pasa: "Sin resultados", "Aún no hay gastos". */
|
|
10
|
+
title: ReactNode;
|
|
11
|
+
/** Por qué está vacío y qué hacer. Nada de "¡Ups!". */
|
|
12
|
+
description?: ReactNode;
|
|
13
|
+
/** La siguiente acción: normalmente un `Button` secundario y uno primario. */
|
|
14
|
+
actions?: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Lo que se ve cuando una lista, tabla o búsqueda no tiene contenido: qué pasa y qué hacer. */
|
|
18
|
+
export declare function EmptyState({ icon, title, description, actions, className }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../internal/cx.js";
|
|
3
|
+
import { FeaturedIcon } from "./FeaturedIcon.js";
|
|
4
|
+
/** Lo que se ve cuando una lista, tabla o búsqueda no tiene contenido: qué pasa y qué hacer. */
|
|
5
|
+
export function EmptyState({ icon = "search-lg", title, description, actions, className }) {
|
|
6
|
+
return (_jsxs("div", { className: cx("flex flex-col items-center gap-4 px-4 py-8 text-center font-body", className), children: [_jsx(FeaturedIcon, { icon: icon, color: "gray", size: "lg", variant: "modern" }), _jsxs("div", { children: [_jsx("h3", { className: "m-0 text-md font-semibold text-primary", children: title }), description ? (_jsx("p", { className: "mt-1 mb-0 max-w-[352px] text-sm text-tertiary", children: description })) : null] }), actions ? (_jsx("div", { className: "mt-2 flex flex-wrap items-center justify-center gap-3", children: actions })) : null] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StatusColor } from "./Badge.js";
|
|
2
|
+
import { type IconName } from "./Icon.js";
|
|
3
|
+
export type FeaturedIconSize = "sm" | "md" | "lg" | "xl";
|
|
4
|
+
export type FeaturedIconVariant = "light" | "modern" | "outline";
|
|
5
|
+
export interface FeaturedIconProps {
|
|
6
|
+
/** Por defecto, `check-circle`. */
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
/** Tiene que coincidir con el significado del contenedor: un modal de borrado lleva `error`. */
|
|
9
|
+
color?: StatusColor;
|
|
10
|
+
/** Lado: `sm` 32 · `md` 40 · `lg` 48 · `xl` 56. */
|
|
11
|
+
size?: FeaturedIconSize;
|
|
12
|
+
/** `light` círculo tintado · `modern` cuadrado blanco esqueumórfico · `outline` anillos concéntricos. */
|
|
13
|
+
variant?: FeaturedIconVariant;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Icono dentro de una forma, para encabezar modales, alertas, estados vacíos y tarjetas. Es decorativo. */
|
|
17
|
+
export declare function FeaturedIcon({ icon, color, size, variant, className, }: FeaturedIconProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../internal/cx.js";
|
|
3
|
+
import { Icon } from "./Icon.js";
|
|
4
|
+
const SIZE = { sm: "size-8", md: "size-10", lg: "size-12", xl: "size-14" };
|
|
5
|
+
const ICON_SIZE = { sm: 16, md: 20, lg: 24, xl: 28 };
|
|
6
|
+
const LIGHT = {
|
|
7
|
+
brand: "bg-brand-secondary text-featured-icon-light-fg-brand",
|
|
8
|
+
gray: "bg-tertiary text-featured-icon-light-fg-gray",
|
|
9
|
+
error: "bg-error-secondary text-featured-icon-light-fg-error",
|
|
10
|
+
warning: "bg-warning-secondary text-featured-icon-light-fg-warning",
|
|
11
|
+
success: "bg-success-secondary text-featured-icon-light-fg-success",
|
|
12
|
+
};
|
|
13
|
+
// Los anillos heredan el color del icono: interior al 30 %, exterior al 10 %
|
|
14
|
+
const OUTLINE_RINGS = "relative before:absolute before:inset-1.5 before:rounded-full before:border-2 before:border-current before:opacity-30 before:content-[''] after:absolute after:inset-px after:rounded-full after:border-2 after:border-current after:opacity-10 after:content-['']";
|
|
15
|
+
const OUTLINE = {
|
|
16
|
+
brand: "text-fg-brand-primary",
|
|
17
|
+
gray: "text-fg-tertiary",
|
|
18
|
+
error: "text-fg-error-primary",
|
|
19
|
+
warning: "text-fg-warning-primary",
|
|
20
|
+
success: "text-fg-success-primary",
|
|
21
|
+
};
|
|
22
|
+
const MODERN = "bg-primary text-fg-secondary shadow-[var(--shadow-xs-skeuomorphic),inset_0_0_0_1px_var(--border-primary)]";
|
|
23
|
+
const MODERN_RADIUS = {
|
|
24
|
+
sm: "rounded-md",
|
|
25
|
+
md: "rounded-lg",
|
|
26
|
+
lg: "rounded-lg",
|
|
27
|
+
xl: "rounded-xl",
|
|
28
|
+
};
|
|
29
|
+
/** Icono dentro de una forma, para encabezar modales, alertas, estados vacíos y tarjetas. Es decorativo. */
|
|
30
|
+
export function FeaturedIcon({ icon = "check-circle", color = "brand", size = "md", variant = "light", className, }) {
|
|
31
|
+
return (_jsx("span", { "aria-hidden": "true", className: cx("inline-flex shrink-0 items-center justify-center rounded-full", SIZE[size], variant === "light" && LIGHT[color], variant === "outline" && [OUTLINE_RINGS, OUTLINE[color]], variant === "modern" && [MODERN, MODERN_RADIUS[size]], className), children: _jsx(Icon, { name: icon, size: ICON_SIZE[size] }) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface UploadFile {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Tamaño en bytes. */
|
|
5
|
+
size?: number;
|
|
6
|
+
/** De 0 a 100. */
|
|
7
|
+
progress?: number;
|
|
8
|
+
status?: "complete" | "error";
|
|
9
|
+
/** Mensaje si `status` es `error`. */
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FileUploadLabels {
|
|
13
|
+
upload: string;
|
|
14
|
+
orDrag: string;
|
|
15
|
+
complete: string;
|
|
16
|
+
uploading: string;
|
|
17
|
+
failed: string;
|
|
18
|
+
remove: (name: string) => string;
|
|
19
|
+
progress: (name: string) => string;
|
|
20
|
+
}
|
|
21
|
+
export interface FileUploadProps {
|
|
22
|
+
/** Formatos aceptados, como en `<input type="file">`: `.csv,.md`, `image/*`. */
|
|
23
|
+
accept?: string;
|
|
24
|
+
multiple?: boolean;
|
|
25
|
+
/** Formatos y tamaño máximo. Dilo siempre antes de que el usuario lo intente. */
|
|
26
|
+
hint?: ReactNode;
|
|
27
|
+
/** Archivos elegidos con clic o soltados. El componente no sube nada. */
|
|
28
|
+
onFiles?: (files: File[]) => void;
|
|
29
|
+
/** Archivos en curso, con el progreso que gestionas tú. */
|
|
30
|
+
files?: UploadFile[];
|
|
31
|
+
onRemove?: (index: number) => void;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** Textos sustituibles. */
|
|
34
|
+
labels?: Partial<FileUploadLabels>;
|
|
35
|
+
id?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Zona para subir archivos con clic o arrastrando, y lista de archivos con su progreso. */
|
|
39
|
+
export declare function FileUpload({ accept, multiple, hint, onFiles, files, onRemove, disabled, labels, id: givenId, className, }: FileUploadProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { cx } from "../internal/cx.js";
|
|
5
|
+
import { formatBytes } from "../internal/format.js";
|
|
6
|
+
import { defaultStrings } from "../internal/strings.js";
|
|
7
|
+
import { FeaturedIcon } from "./FeaturedIcon.js";
|
|
8
|
+
import { Icon } from "./Icon.js";
|
|
9
|
+
import { ProgressBar } from "./ProgressBar.js";
|
|
10
|
+
const DEFAULT_LABELS = {
|
|
11
|
+
upload: defaultStrings.upload,
|
|
12
|
+
orDrag: defaultStrings.orDrag,
|
|
13
|
+
complete: defaultStrings.uploadComplete,
|
|
14
|
+
uploading: defaultStrings.uploading,
|
|
15
|
+
failed: defaultStrings.uploadFailed,
|
|
16
|
+
remove: defaultStrings.removeFile,
|
|
17
|
+
progress: defaultStrings.uploadProgress,
|
|
18
|
+
};
|
|
19
|
+
/** Zona para subir archivos con clic o arrastrando, y lista de archivos con su progreso. */
|
|
20
|
+
export function FileUpload({ accept, multiple, hint, onFiles, files = [], onRemove, disabled, labels, id: givenId, className, }) {
|
|
21
|
+
const autoId = useId();
|
|
22
|
+
const id = givenId ?? autoId;
|
|
23
|
+
const t = { ...DEFAULT_LABELS, ...labels };
|
|
24
|
+
const [dragging, setDragging] = useState(false);
|
|
25
|
+
const take = (list) => {
|
|
26
|
+
if (!disabled && onFiles && list?.length)
|
|
27
|
+
onFiles(Array.from(list));
|
|
28
|
+
};
|
|
29
|
+
const onDrop = (e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
setDragging(false);
|
|
32
|
+
take(e.dataTransfer.files);
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-4 font-body", className), children: [_jsxs("label", { htmlFor: id, onDragOver: (e) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
if (!disabled)
|
|
37
|
+
setDragging(true);
|
|
38
|
+
}, onDragLeave: () => setDragging(false), onDrop: onDrop, className: cx("flex cursor-pointer flex-col items-center gap-3 rounded-xl bg-primary px-6 py-4 text-center shadow-[inset_0_0_0_1px_var(--border-secondary)] focus-within:shadow-[inset_0_0_0_2px_var(--border-brand),var(--focus-ring-shadow)]", disabled
|
|
39
|
+
? "cursor-not-allowed opacity-50"
|
|
40
|
+
: "hover:shadow-[inset_0_0_0_2px_var(--border-brand)]", dragging && "shadow-[inset_0_0_0_2px_var(--border-brand)]"), "data-dragging": dragging || undefined, children: [_jsx("input", { id: id, type: "file", accept: accept, multiple: multiple, disabled: disabled, onChange: (e) => {
|
|
41
|
+
take(e.target.files);
|
|
42
|
+
e.target.value = "";
|
|
43
|
+
}, className: "sr-only" }), _jsx(FeaturedIcon, { icon: "upload-cloud-02", variant: "modern", size: "md", color: "gray" }), _jsxs("span", { className: "text-sm text-tertiary", children: [_jsx("span", { className: "font-semibold text-brand-secondary", children: t.upload }), " ", t.orDrag] }), hint ? _jsx("span", { className: "-mt-2 text-xs text-tertiary", children: hint }) : null] }), files.length ? (_jsx("ul", { className: "m-0 flex list-none flex-col gap-3 p-0", children: files.map((f, i) => {
|
|
44
|
+
const failed = f.status === "error";
|
|
45
|
+
const done = !failed && (f.status === "complete" || (f.progress ?? 0) >= 100);
|
|
46
|
+
return (_jsxs("li", { className: cx("relative flex gap-3 rounded-xl bg-primary p-4", failed
|
|
47
|
+
? "shadow-[inset_0_0_0_2px_var(--border-error_subtle)]"
|
|
48
|
+
: "shadow-[inset_0_0_0_1px_var(--border-secondary)]"), children: [_jsx(FeaturedIcon, { icon: failed ? "alert-circle" : "file-04", variant: "modern", size: "md", color: failed ? "error" : "gray" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1 pr-8", children: [_jsx("div", { className: "truncate text-sm font-medium text-secondary", children: f.name }), _jsx("div", { className: "flex items-center gap-1 text-sm text-tertiary [&>span]:inline-flex [&>span]:items-center [&>span]:gap-1", children: failed ? (_jsx("span", { className: "text-error-primary", children: f.error || t.failed })) : (_jsxs(_Fragment, { children: [formatBytes(f.size ?? 0), _jsx("span", { "aria-hidden": "true", children: " | " }), done ? (_jsxs("span", { className: "font-medium text-success-primary", children: [_jsx(Icon, { name: "check-circle", size: 14 }), " ", t.complete] })) : (_jsxs("span", { children: [_jsx(Icon, { name: "loading-02", size: 14, spin: true }), " ", t.uploading] }))] })) }), !done && !failed ? (_jsx(ProgressBar, { value: f.progress ?? 0, "aria-label": t.progress(f.name), className: "mt-1" })) : null] }), _jsx("button", { type: "button", onClick: () => onRemove?.(i), "aria-label": t.remove(f.name), className: "absolute top-3 right-3 inline-flex cursor-pointer rounded-md border-0 bg-transparent p-1.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: "trash-01", size: 16 }) })] }, i));
|
|
49
|
+
}) })) : null] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type ButtonGroupItem } from "./ButtonGroup.js";
|
|
3
|
+
export interface FilterBarLabels {
|
|
4
|
+
/** Nombre del grupo de vistas rápidas. @default "Vista" */
|
|
5
|
+
segments: string;
|
|
6
|
+
/** Nombre del buscador (el placeholder es `searchPlaceholder`). @default "Buscar" */
|
|
7
|
+
search: string;
|
|
8
|
+
/** @default "Filtros" */
|
|
9
|
+
filters: string;
|
|
10
|
+
/** @default "Limpiar todo" */
|
|
11
|
+
clear: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FilterBarProps {
|
|
14
|
+
/** Vistas rápidas, en un `ButtonGroup`. */
|
|
15
|
+
segments?: ButtonGroupItem[];
|
|
16
|
+
/** Vista elegida (controlado). */
|
|
17
|
+
segmentValue?: string;
|
|
18
|
+
/** Vista elegida al principio (no controlado). */
|
|
19
|
+
defaultSegment?: string;
|
|
20
|
+
onSegment?: (value: string) => void;
|
|
21
|
+
/** Muestra el buscador (también aparece si pasas `onSearch`). */
|
|
22
|
+
search?: boolean;
|
|
23
|
+
/** Recibe el texto del buscador en cada cambio. */
|
|
24
|
+
onSearch?: (query: string) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Placeholder del buscador.
|
|
27
|
+
* @default "Buscar"
|
|
28
|
+
*/
|
|
29
|
+
searchPlaceholder?: string;
|
|
30
|
+
/** Abre los filtros avanzados, en un `Slideout` (no en un modal). */
|
|
31
|
+
onOpenFilters?: () => void;
|
|
32
|
+
/** Filtros aplicados: se ven siempre, como `Tag` con su X. El botón Filtros muestra cuántos hay. */
|
|
33
|
+
activeFilters?: ({
|
|
34
|
+
label: string;
|
|
35
|
+
} | string)[];
|
|
36
|
+
onRemoveFilter?: (index: number) => void;
|
|
37
|
+
/** "Limpiar todo", junto a los filtros aplicados. */
|
|
38
|
+
onClear?: () => void;
|
|
39
|
+
/** Controles extra antes del botón Filtros, como un `DatePicker`. */
|
|
40
|
+
extra?: ReactNode;
|
|
41
|
+
/** Textos que trae el componente, para sustituirlos. */
|
|
42
|
+
labels?: Partial<FilterBarLabels>;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Barra encima de una tabla o lista: vista rápida, búsqueda, botón de filtros y filtros activos. */
|
|
46
|
+
export declare function FilterBar({ segments, segmentValue, defaultSegment, onSegment, search, onSearch, searchPlaceholder, onOpenFilters, activeFilters, onRemoveFilter, onClear, extra, labels, className, }: FilterBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { Button } from "./Button.js";
|
|
5
|
+
import { ButtonGroup } from "./ButtonGroup.js";
|
|
6
|
+
import { Input } from "./Input.js";
|
|
7
|
+
import { Tag } from "./Tag.js";
|
|
8
|
+
/** Barra encima de una tabla o lista: vista rápida, búsqueda, botón de filtros y filtros activos. */
|
|
9
|
+
export function FilterBar({ segments, segmentValue, defaultSegment, onSegment, search, onSearch, searchPlaceholder, onOpenFilters, activeFilters = [], onRemoveFilter, onClear, extra, labels, className, }) {
|
|
10
|
+
const t = {
|
|
11
|
+
segments: defaultStrings.view,
|
|
12
|
+
search: defaultStrings.search,
|
|
13
|
+
filters: defaultStrings.filters,
|
|
14
|
+
clear: defaultStrings.clearAll,
|
|
15
|
+
...labels,
|
|
16
|
+
};
|
|
17
|
+
const count = activeFilters.length;
|
|
18
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-3 font-body", className), children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [segments ? (_jsx(ButtonGroup, { items: segments, value: segmentValue, defaultValue: defaultSegment, onChange: onSegment, "aria-label": t.segments })) : null, _jsxs("div", { className: "ml-auto flex flex-wrap items-center gap-3", children: [search || onSearch ? (_jsx("div", { className: "w-[296px] max-w-full", children: _jsx(Input, { size: "sm", icon: "search-lg", placeholder: searchPlaceholder ?? t.search, "aria-label": t.search, onChange: onSearch ? (e) => onSearch(e.target.value) : undefined }) })) : null, extra, _jsxs(Button, { hierarchy: "secondary", size: "sm", iconLeading: "filter-lines", onClick: onOpenFilters, children: [t.filters, count ? " " : null, count ? (_jsx("span", { className: "inline-flex min-w-5 justify-center rounded-full bg-utility-brand-50 px-1.5 text-xs font-medium text-utility-brand-700 inset-ring inset-ring-utility-brand-200", children: count })) : null] })] })] }), count ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [activeFilters.map((f, i) => {
|
|
19
|
+
const text = typeof f === "string" ? f : f.label;
|
|
20
|
+
return (_jsx(Tag
|
|
21
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: el índice es lo que recibe onRemoveFilter
|
|
22
|
+
, { size: "md", onRemove: onRemoveFilter ? () => onRemoveFilter(i) : undefined, children: text }, i));
|
|
23
|
+
}), _jsx(Button, { hierarchy: "link-gray", size: "sm", onClick: onClear, children: t.clear })] })) : null] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Icon, type IconName, type IconProps } from "@jotis-me/icons";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export { Icon, type IconName, type IconProps };
|
|
4
|
+
/** Pinta un icono por nombre o deja pasar un nodo ya hecho (para props como `iconLeading`). */
|
|
5
|
+
export declare function iconSlot(icon: IconName | ReactNode | undefined, size: number): ReactNode;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from "@jotis-me/icons";
|
|
3
|
+
export { Icon };
|
|
4
|
+
/** Pinta un icono por nombre o deja pasar un nodo ya hecho (para props como `iconLeading`). */
|
|
5
|
+
export function iconSlot(icon, size) {
|
|
6
|
+
if (typeof icon === "string")
|
|
7
|
+
return _jsx(Icon, { name: icon, size: size });
|
|
8
|
+
return icon ?? null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { IconName } from "./Icon.js";
|
|
3
|
+
export interface InlineCTAProps {
|
|
4
|
+
/** Qué se propone, como "Conecta tu banco" (`<h3>`). */
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
/** Qué gana quien lo haga. */
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/** Los botones: siempre uno para descartar ("Ahora no") y la acción, la última. */
|
|
9
|
+
actions?: ReactNode;
|
|
10
|
+
/** Icono destacado (`FeaturedIcon` modern gris de 48px) sobre el título. */
|
|
11
|
+
icon?: IconName;
|
|
12
|
+
/** Imagen a la izquierda (240px de ancho): una URL o un nodo. */
|
|
13
|
+
image?: string | ReactNode;
|
|
14
|
+
/** Texto alternativo de la imagen cuando es una URL. Sin él, la imagen es decorativa. */
|
|
15
|
+
imageAlt?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Tarjeta de invitación dentro de una página para animar a una acción secundaria. Una por pantalla como mucho. */
|
|
19
|
+
export declare function InlineCTA({ title, description, actions, icon, image, imageAlt, className }: InlineCTAProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../internal/cx.js";
|
|
3
|
+
import { FeaturedIcon } from "./FeaturedIcon.js";
|
|
4
|
+
/** Tarjeta de invitación dentro de una página para animar a una acción secundaria. Una por pantalla como mucho. */
|
|
5
|
+
export function InlineCTA({ title, description, actions, icon, image, imageAlt, className }) {
|
|
6
|
+
return (_jsxs("div", { className: cx("flex overflow-hidden rounded-xl bg-primary font-body shadow-[var(--shadow-xs),inset_0_0_0_1px_var(--border-secondary)]", className), children: [image ? (_jsx("div", { className: "shrink-0 grow-0 basis-60 bg-tertiary", children: typeof image === "string" ? (_jsx("img", { src: image, alt: imageAlt ?? "", className: "block size-full object-cover" })) : (image) })) : null, _jsxs("div", { className: "flex flex-1 flex-col gap-5 p-6", children: [icon ? _jsx(FeaturedIcon, { icon: icon, variant: "modern", size: "lg", color: "gray" }) : null, _jsxs("div", { children: [_jsx("h3", { className: "m-0 text-lg font-semibold text-primary", children: title }), description ? _jsx("p", { className: "mt-0.5 mb-0 text-sm text-tertiary", children: description }) : null] }), actions ? _jsx("div", { className: "flex flex-wrap items-center gap-3", children: actions }) : null] })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from "react";
|
|
2
|
+
import { type FieldProps, type FieldSize } from "../internal/field.js";
|
|
3
|
+
import { type IconName } from "./Icon.js";
|
|
4
|
+
export interface InputProps extends FieldProps, Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
5
|
+
/**
|
|
6
|
+
* Alto: `sm` 36 · `md` 40 · `lg` 44. En `md` y `lg` el valor va a 16px para que iOS no haga zoom.
|
|
7
|
+
* @default "md"
|
|
8
|
+
*/
|
|
9
|
+
size?: FieldSize;
|
|
10
|
+
/** Icono inicial, 20px en `fg-quaternary`. */
|
|
11
|
+
icon?: IconName;
|
|
12
|
+
/** Icono final, 16px. Con error se sustituye por el de alerta. */
|
|
13
|
+
trailing?: IconName;
|
|
14
|
+
}
|
|
15
|
+
/** Campo de texto de una línea con etiqueta, pista y error, en tres alturas. */
|
|
16
|
+
export declare function Input({ label, required, hint, error, size, icon, trailing, id: givenId, className, disabled, ...rest }: InputProps): import("react").JSX.Element;
|