@eduardoalvarez/arrecife 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  export { tokens } from './chunk-VPT32GPG.js';
2
2
  import { alternarTema, temaActual, escucharTema } from './chunk-KPZNNMV5.js';
3
3
  export { TEMA_ATRIBUTO, TEMA_CLAVE, TEMA_EVENTO, alternarTema, aplicarTema, escucharTema, scriptTema, temaActual, temaGuardado, temaPreferido } from './chunk-KPZNNMV5.js';
4
- import { CaraDeMascota, Mascota } from './chunk-TB3IYPOD.js';
5
- export { CaraDeMascota, Isotipo, Logo, Mascota } from './chunk-TB3IYPOD.js';
4
+ import { CaraDeMascota, Mascota } from './chunk-YZ2SDOVZ.js';
5
+ export { CaraDeMascota, Isotipo, Logo, Mascota } from './chunk-YZ2SDOVZ.js';
6
6
  export { RUTA_ASSETS, aletas, caras, listaCaras, listaPoses, poses, usoDeCara } from './chunk-E3OMP2DL.js';
7
7
  import { Label } from './chunk-ZEOQKRQ7.js';
8
8
  export { Label } from './chunk-ZEOQKRQ7.js';
@@ -19,6 +19,7 @@ import { cva } from 'class-variance-authority';
19
19
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
20
20
  import { Slot } from '@radix-ui/react-slot';
21
21
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
22
+ import { useId, useState, useEffect, useSyncExternalStore, useRef, useCallback } from 'react';
22
23
  import { es } from 'date-fns/locale';
23
24
  import { DayPicker } from 'react-day-picker';
24
25
  import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
@@ -32,8 +33,8 @@ import * as SeparatorPrimitive from '@radix-ui/react-separator';
32
33
  import * as SwitchPrimitive from '@radix-ui/react-switch';
33
34
  import * as TabsPrimitive from '@radix-ui/react-tabs';
34
35
  import * as ToastPrimitive from '@radix-ui/react-toast';
35
- import { useSyncExternalStore, useRef, useState, useEffect, useCallback, useId } from 'react';
36
36
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
37
+ import { format } from 'date-fns';
37
38
 
38
39
  function Glyph({ children, ...props }) {
39
40
  return /* @__PURE__ */ jsx(
@@ -87,6 +88,10 @@ var Spinner = (props) => /* @__PURE__ */ jsxs(
87
88
  ]
88
89
  }
89
90
  );
91
+ var Camara = (props) => /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
92
+ /* @__PURE__ */ jsx("path", { d: "M2 5.5h2.6l1-1.5h4.8l1 1.5H14v7.5H2z" }),
93
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "9", r: "2.4" })
94
+ ] });
90
95
  var Sol = (props) => /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
91
96
  /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "3" }),
92
97
  /* @__PURE__ */ jsx("path", { d: "M8 1.5v1.2M8 13.3v1.2M14.5 8h-1.2M2.7 8H1.5M12.6 3.4l-.85.85M4.25 11.75l-.85.85M12.6 12.6l-.85-.85M4.25 4.25l-.85-.85" })
@@ -191,7 +196,17 @@ function AccordionContent({
191
196
  children,
192
197
  ...props
193
198
  }) {
194
- return /* @__PURE__ */ jsx(AccordionPrimitive.Content, { className: "overflow-hidden", ...props, children: /* @__PURE__ */ jsx("div", { className: cn("pb-step-md font-sans text-ui text-text-secondary max-w-measure", className), children }) });
199
+ return /* @__PURE__ */ jsx(
200
+ AccordionPrimitive.Content,
201
+ {
202
+ className: cn(
203
+ "overflow-hidden",
204
+ "motion-safe:data-[state=open]:desplegar motion-safe:data-[state=closed]:replegar"
205
+ ),
206
+ ...props,
207
+ children: /* @__PURE__ */ jsx("div", { className: cn("pb-step-md font-sans text-ui text-text-secondary max-w-measure", className), children })
208
+ }
209
+ );
195
210
  }
196
211
  var alert = cva("w-full rounded-card border p-step-md font-sans text-ui", {
197
212
  variants: {
@@ -471,6 +486,68 @@ function AvatarFallback({
471
486
  }
472
487
  );
473
488
  }
489
+ function AvatarUpload({
490
+ src,
491
+ fallback,
492
+ onSelectFile,
493
+ accept = "image/*",
494
+ label = "Cambiar la foto",
495
+ size: size2,
496
+ disabled = false,
497
+ className,
498
+ ...props
499
+ }) {
500
+ const id = useId();
501
+ const [previa, setPrevia] = useState(null);
502
+ useEffect(() => {
503
+ if (!previa) return;
504
+ return () => URL.revokeObjectURL(previa);
505
+ }, [previa]);
506
+ function elegir(evento) {
507
+ const archivo = evento.target.files?.[0];
508
+ if (!archivo) return;
509
+ setPrevia(URL.createObjectURL(archivo));
510
+ onSelectFile?.(archivo);
511
+ evento.target.value = "";
512
+ }
513
+ const imagen = previa ?? src;
514
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative inline-flex", className), ...props, children: [
515
+ /* @__PURE__ */ jsxs(Avatar, { size: size2, children: [
516
+ imagen ? /* @__PURE__ */ jsx(AvatarImage, { src: imagen, alt: "" }) : null,
517
+ /* @__PURE__ */ jsx(AvatarFallback, { children: fallback })
518
+ ] }),
519
+ /* @__PURE__ */ jsxs(
520
+ "label",
521
+ {
522
+ htmlFor: id,
523
+ className: cn(
524
+ "rounded-pill absolute -right-1 -bottom-1 flex size-6 cursor-pointer items-center justify-center",
525
+ "border-hairline bg-surface-raised text-text-secondary border",
526
+ "transition-standard hover:text-accent hover:border-accent",
527
+ // El foco vive en el input, que está oculto: sin esto, tabular hasta
528
+ // el control no se ve por ninguna parte.
529
+ "has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-accent",
530
+ disabled && "pointer-events-none opacity-50"
531
+ ),
532
+ children: [
533
+ /* @__PURE__ */ jsx(Camara, { "aria-hidden": "true" }),
534
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
535
+ /* @__PURE__ */ jsx(
536
+ "input",
537
+ {
538
+ id,
539
+ type: "file",
540
+ accept,
541
+ disabled,
542
+ onChange: elegir,
543
+ className: "sr-only"
544
+ }
545
+ )
546
+ ]
547
+ }
548
+ )
549
+ ] });
550
+ }
474
551
  var badge = cva(
475
552
  ["inline-flex items-center gap-step-xs", "rounded-chip px-step-xs py-0.5", "font-sans text-tag", "transition-standard"],
476
553
  {
@@ -2312,6 +2389,193 @@ function EmptyState({
2312
2389
  }
2313
2390
  );
2314
2391
  }
2392
+ var mismoDia = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2393
+ function aValorDeCampo(fecha) {
2394
+ const dos = (n) => String(n).padStart(2, "0");
2395
+ return `${fecha.getFullYear()}-${dos(fecha.getMonth() + 1)}-${dos(fecha.getDate())}T${dos(
2396
+ fecha.getHours()
2397
+ )}:${dos(fecha.getMinutes())}`;
2398
+ }
2399
+ function EventCalendar({
2400
+ events,
2401
+ onCreateEvent,
2402
+ onUpdateEvent,
2403
+ onDeleteEvent,
2404
+ selected,
2405
+ onSelectDay,
2406
+ formatDay = (dia) => format(dia, "EEEE d 'de' MMMM", { locale: es }),
2407
+ formatTime: formatTime2 = (fecha) => format(fecha, "HH:mm", { locale: es }),
2408
+ emptyMessage = "Nada en este d\xEDa.",
2409
+ className,
2410
+ ...props
2411
+ }) {
2412
+ const id = useId();
2413
+ const [diaInterno, setDiaInterno] = useState(() => selected ?? /* @__PURE__ */ new Date());
2414
+ const dia = selected ?? diaInterno;
2415
+ const [mes, setMes] = useState(() => dia);
2416
+ const [editando, setEditando] = useState(null);
2417
+ const [titulo, setTitulo] = useState("");
2418
+ const [cuando, setCuando] = useState("");
2419
+ const editable = Boolean(onCreateEvent ?? onUpdateEvent);
2420
+ const delDia = events.filter((e) => mismoDia(e.start, dia)).sort((a, b) => a.start.getTime() - b.start.getTime());
2421
+ function elegirDia(nuevo) {
2422
+ if (!nuevo) return;
2423
+ setDiaInterno(nuevo);
2424
+ setMes(nuevo);
2425
+ onSelectDay?.(nuevo);
2426
+ cancelar();
2427
+ }
2428
+ function cancelar() {
2429
+ setEditando(null);
2430
+ setTitulo("");
2431
+ setCuando("");
2432
+ }
2433
+ function editar(evento) {
2434
+ setEditando(evento.id);
2435
+ setTitulo(evento.title);
2436
+ setCuando(aValorDeCampo(evento.start));
2437
+ }
2438
+ function enviar(e) {
2439
+ e.preventDefault();
2440
+ if (!titulo.trim() || !cuando) return;
2441
+ const start = new Date(cuando);
2442
+ if (Number.isNaN(start.getTime())) return;
2443
+ if (editando) {
2444
+ const previo = events.find((ev) => ev.id === editando);
2445
+ if (previo) onUpdateEvent?.({ ...previo, title: titulo.trim(), start });
2446
+ } else {
2447
+ onCreateEvent?.({ title: titulo.trim(), start });
2448
+ }
2449
+ cancelar();
2450
+ }
2451
+ const conEvento = events.map((e) => e.start);
2452
+ return /* @__PURE__ */ jsxs("div", { className: cn("gap-step-lg flex flex-col md:flex-row md:items-start", className), ...props, children: [
2453
+ /* @__PURE__ */ jsx(
2454
+ Calendar,
2455
+ {
2456
+ mode: "single",
2457
+ selected: dia,
2458
+ onSelect: elegirDia,
2459
+ month: mes,
2460
+ onMonthChange: setMes,
2461
+ modifiers: { conEvento },
2462
+ modifiersClassNames: {
2463
+ // El punto va bajo el número, no encima del fondo del día: sobre el
2464
+ // seleccionado —que es bioluz sólido— un punto de acento desaparece.
2465
+ // `after:` en Tailwind v4 ya trae `content: ""`, así que no hace falta
2466
+ // declararlo con un valor arbitrario.
2467
+ conEvento: cn(
2468
+ "after:bg-accent after:absolute after:bottom-1 after:left-1/2",
2469
+ "after:size-1 after:-translate-x-1/2 after:rounded-pill",
2470
+ "[&[data-selected=true]]:after:bg-accent-on"
2471
+ )
2472
+ },
2473
+ className: "shrink-0"
2474
+ }
2475
+ ),
2476
+ /* @__PURE__ */ jsxs(
2477
+ "section",
2478
+ {
2479
+ "aria-labelledby": `${id}-dia`,
2480
+ className: "border-hairline rounded-card p-step-md gap-step-md flex min-w-0 flex-1 flex-col border",
2481
+ children: [
2482
+ /* @__PURE__ */ jsx(Text, { as: "h3", variant: "h3", id: `${id}-dia`, className: "first-letter:uppercase", children: formatDay(dia) }),
2483
+ delDia.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "gap-step-xs flex flex-col", children: delDia.map((evento) => /* @__PURE__ */ jsxs(
2484
+ "li",
2485
+ {
2486
+ className: "border-hairline gap-step-sm py-step-xs flex items-center border-b last:border-b-0",
2487
+ children: [
2488
+ /* @__PURE__ */ jsx(
2489
+ Text,
2490
+ {
2491
+ as: "span",
2492
+ variant: "meta",
2493
+ tone: evento.tone === "warm" ? "warm" : "accent",
2494
+ className: "shrink-0 tabular-nums",
2495
+ children: /* @__PURE__ */ jsx("time", { dateTime: evento.start.toISOString(), children: formatTime2(evento.start) })
2496
+ }
2497
+ ),
2498
+ /* @__PURE__ */ jsx(Text, { as: "span", variant: "ui", className: "min-w-0 flex-1 truncate", children: evento.title }),
2499
+ onUpdateEvent ? /* @__PURE__ */ jsx(
2500
+ Button,
2501
+ {
2502
+ variant: "tertiary",
2503
+ size: "sm",
2504
+ onClick: () => editar(evento),
2505
+ "aria-label": `Editar \xAB${evento.title}\xBB`,
2506
+ children: "./editar"
2507
+ }
2508
+ ) : null,
2509
+ onDeleteEvent ? /* @__PURE__ */ jsxs(AlertDialog, { children: [
2510
+ /* @__PURE__ */ jsx(AlertDialogTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "sm", "aria-label": `Borrar \xAB${evento.title}\xBB`, children: "./borrar" }) }),
2511
+ /* @__PURE__ */ jsxs(AlertDialogContent, { children: [
2512
+ /* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
2513
+ /* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
2514
+ "Borrar \xAB",
2515
+ evento.title,
2516
+ "\xBB"
2517
+ ] }),
2518
+ /* @__PURE__ */ jsxs(AlertDialogDescription, { children: [
2519
+ "Se borra del ",
2520
+ formatDay(evento.start),
2521
+ " a las ",
2522
+ formatTime2(evento.start),
2523
+ ". No se puede deshacer."
2524
+ ] })
2525
+ ] }),
2526
+ /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
2527
+ /* @__PURE__ */ jsx(AlertDialogCancel, { children: "Mejor no" }),
2528
+ /* @__PURE__ */ jsx(AlertDialogAction, { onClick: () => onDeleteEvent(evento.id), children: "Borrar el evento" })
2529
+ ] })
2530
+ ] })
2531
+ ] }) : null
2532
+ ]
2533
+ },
2534
+ evento.id
2535
+ )) }) : (
2536
+ // `EmptyState` no: lleva una cara de la mascota, y la mascota tiene
2537
+ // siete sitios contados donde puede aparecer. Un día vacío de una
2538
+ // agenda no es uno de ellos — y en un mes de treinta días, la mitad lo
2539
+ // serían.
2540
+ /* @__PURE__ */ jsx(Text, { as: "p", variant: "ui", tone: "muted", children: emptyMessage })
2541
+ ),
2542
+ editable ? /* @__PURE__ */ jsxs("form", { onSubmit: enviar, className: "gap-step-sm border-hairline pt-step-md flex flex-col border-t", children: [
2543
+ /* @__PURE__ */ jsxs("div", { className: "gap-step-xs flex flex-col", children: [
2544
+ /* @__PURE__ */ jsx(Label, { htmlFor: `${id}-titulo`, children: "T\xEDtulo" }),
2545
+ /* @__PURE__ */ jsx(
2546
+ Input,
2547
+ {
2548
+ id: `${id}-titulo`,
2549
+ value: titulo,
2550
+ onChange: (e) => setTitulo(e.target.value),
2551
+ placeholder: "Publicar el art\xEDculo",
2552
+ required: true
2553
+ }
2554
+ )
2555
+ ] }),
2556
+ /* @__PURE__ */ jsxs("div", { className: "gap-step-xs flex flex-col", children: [
2557
+ /* @__PURE__ */ jsx(Label, { htmlFor: `${id}-cuando`, children: "Cu\xE1ndo" }),
2558
+ /* @__PURE__ */ jsx(
2559
+ DateField,
2560
+ {
2561
+ id: `${id}-cuando`,
2562
+ withTime: true,
2563
+ value: cuando,
2564
+ onChange: (e) => setCuando(e.target.value),
2565
+ required: true
2566
+ }
2567
+ )
2568
+ ] }),
2569
+ /* @__PURE__ */ jsxs("div", { className: "gap-step-sm flex flex-wrap items-center", children: [
2570
+ /* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", children: editando ? "Guardar" : "A\xF1adir" }),
2571
+ editando ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "tertiary", size: "sm", onClick: cancelar, children: "./cancelar" }) : null
2572
+ ] })
2573
+ ] }) : null
2574
+ ]
2575
+ }
2576
+ )
2577
+ ] });
2578
+ }
2315
2579
  function Footer({
2316
2580
  social,
2317
2581
  year = (/* @__PURE__ */ new Date()).getFullYear(),
@@ -2323,31 +2587,33 @@ function Footer({
2323
2587
  return /* @__PURE__ */ jsx("footer", { className: cn("border-hairline w-full border-t", className), ...props, children: /* @__PURE__ */ jsxs("div", { className: "max-w-wide px-step-md py-step-xl gap-step-lg mx-auto flex flex-col", children: [
2324
2588
  brand2 ? /* @__PURE__ */ jsx("div", { className: "flex items-center", children: brand2 }) : null,
2325
2589
  children ? /* @__PURE__ */ jsx("div", { className: "gap-step-md flex flex-wrap items-center", children }) : null,
2326
- social && social.length > 0 ? (
2327
- // 18px de separación, del documento. No es un escalón de `spacing`:
2328
- // es el ritmo de una fila de iconos, no el de una página.
2329
- /* @__PURE__ */ jsx("ul", { className: "flex items-center gap-[18px]", children: social.map((red) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
2330
- "a",
2331
- {
2332
- href: red.href,
2333
- "aria-label": red.label,
2334
- className: cn(
2335
- "text-text-muted hover:text-accent transition-standard block cursor-pointer text-[19px]",
2336
- "rounded-chip focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
2337
- ),
2338
- children: red.icon
2339
- }
2340
- ) }, red.href)) })
2341
- ) : null,
2342
- /* @__PURE__ */ jsxs(Text, { variant: "meta", tone: "muted", as: "p", children: [
2343
- /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "text-accent", children: [
2344
- "$",
2345
- " "
2346
- ] }),
2347
- "cd ~/",
2348
- naming.domain,
2349
- "/",
2350
- year
2590
+ /* @__PURE__ */ jsxs("div", { className: "gap-step-md flex flex-wrap items-center", children: [
2591
+ social && social.length > 0 ? (
2592
+ // 18px de separación, del documento. No es un escalón de `spacing`:
2593
+ // es el ritmo de una fila de iconos, no el de una página.
2594
+ /* @__PURE__ */ jsx("ul", { className: "flex items-center gap-[18px]", children: social.map((red) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
2595
+ "a",
2596
+ {
2597
+ href: red.href,
2598
+ "aria-label": red.label,
2599
+ className: cn(
2600
+ "text-text-muted hover:text-accent transition-standard block cursor-pointer text-[19px]",
2601
+ "rounded-chip focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
2602
+ ),
2603
+ children: red.icon
2604
+ }
2605
+ ) }, red.href)) })
2606
+ ) : null,
2607
+ /* @__PURE__ */ jsxs(Text, { variant: "meta", tone: "muted", as: "p", className: "ml-auto", children: [
2608
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "text-accent", children: [
2609
+ "$",
2610
+ " "
2611
+ ] }),
2612
+ "cd ~/",
2613
+ naming.domain,
2614
+ "/",
2615
+ year
2616
+ ] })
2351
2617
  ] })
2352
2618
  ] }) });
2353
2619
  }
@@ -2486,7 +2752,10 @@ function Nav({ brand: brand2, children, actions, className, ...props }) {
2486
2752
  ...props,
2487
2753
  children: /* @__PURE__ */ jsxs("div", { className: "max-w-wide px-step-md h-nav gap-step-lg mx-auto flex items-center", children: [
2488
2754
  brand2 ? /* @__PURE__ */ jsx("div", { className: "shrink-0", children: brand2 }) : null,
2489
- children ? /* @__PURE__ */ jsx("nav", { "aria-label": "Principal", className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx("ul", { className: "gap-step-md flex items-center", children }) }) : /* @__PURE__ */ jsx("div", { className: "flex-1" }),
2755
+ children ? /* @__PURE__ */ jsx("nav", { "aria-label": "Principal", className: "ml-auto min-w-0", children: /* @__PURE__ */ jsxs("ul", { className: "gap-step-md flex items-center", children: [
2756
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-text-muted font-mono text-meta select-none", children: "~/" }),
2757
+ children
2758
+ ] }) }) : /* @__PURE__ */ jsx("div", { className: "flex-1" }),
2490
2759
  actions ? /* @__PURE__ */ jsx("div", { className: "gap-step-sm flex shrink-0 items-center", children: actions }) : null
2491
2760
  ] })
2492
2761
  }
@@ -2506,8 +2775,10 @@ function NavItem({ active = false, asChild = false, className, children, ...prop
2506
2775
  ),
2507
2776
  ...props,
2508
2777
  children: [
2509
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-text-muted", children: "./" }),
2510
- children
2778
+ active ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-accent", children: "[" }) : null,
2779
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: active ? "text-accent" : "text-text-muted", children: "./" }),
2780
+ children,
2781
+ active ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-accent", children: "]" }) : null
2511
2782
  ]
2512
2783
  }
2513
2784
  ) });
@@ -2759,10 +3030,28 @@ function SidebarNav({
2759
3030
  }
2760
3031
  );
2761
3032
  }
2762
- function Stat({ value, label, tone = "neutral", progress, className, ...props }) {
3033
+ function Stat({
3034
+ value,
3035
+ label,
3036
+ tone = "neutral",
3037
+ progress,
3038
+ icon,
3039
+ description,
3040
+ className,
3041
+ ...props
3042
+ }) {
2763
3043
  return /* @__PURE__ */ jsxs("div", { className: cn(SUPERFICIE_TARJETA, "p-step-lg gap-step-xs flex flex-col", className), ...props, children: [
3044
+ /* @__PURE__ */ jsxs(Text, { variant: "eyebrow", tone: "muted", as: "p", className: "gap-step-xs flex items-center", children: [
3045
+ icon ? (
3046
+ // `aria-hidden` no hace falta: los glifos del sistema ya lo traen
3047
+ // puesto. Lo que sí hace falta es que no encoja al lado de un título
3048
+ // largo, porque un icono aplastado se lee como otro icono.
3049
+ /* @__PURE__ */ jsx("span", { className: "shrink-0", children: icon })
3050
+ ) : null,
3051
+ label
3052
+ ] }),
2764
3053
  /* @__PURE__ */ jsx(Text, { variant: "stat", as: "p", tone: tone === "alerta" ? "warm" : "accent", children: value }),
2765
- /* @__PURE__ */ jsx(Text, { variant: "eyebrow", tone: "muted", as: "p", children: label }),
3054
+ description ? /* @__PURE__ */ jsx(Text, { variant: "ui", tone: "secondary", as: "p", children: description }) : null,
2766
3055
  typeof progress === "number" ? /* @__PURE__ */ jsx(
2767
3056
  Progress,
2768
3057
  {
@@ -2922,4 +3211,4 @@ var Correo = (props) => /* @__PURE__ */ jsxs(Funcional, { ...props, children: [
2922
3211
  /* @__PURE__ */ jsx("path", { d: "m3.5 7.5 7.4 5.2a2 2 0 0 0 2.2 0l7.4-5.2" })
2923
3212
  ] });
2924
3213
 
2925
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, ArticleCard, AudioPlayer, AuthorCard, Avatar, AvatarFallback, AvatarImage, Badge, Blockquote, Breadcrumb, Button, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CategoryBadge, Checkbox, Code, CodeBlock, CourseCard, DateField, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, Footer, FooterLink, HOVER_TARJETA, Hero, Input, LinkRow, MetricBadge, Nav, NavItem, NewsletterForm, PageHeader, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioGroup2 as RadioGroup, RadioGroupItem, SUPERFICIE_TARJETA, ScrollingProgressBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator3 as Separator, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SidebarItem, SidebarNav, Skeleton, Stat, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, TalkCard, Textarea, ThemeToggle, Toast, ToastAction, ToastDescription, ToastProvider, ToastTitle, ToastViewport, Toaster, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, alert as alertVariants, avatar as avatarVariants, badge as badgeVariants, button as buttonVariants, categoria as categoryBadgeVariants, social_exports as social, toast2 as toast, useTema };
3214
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, ArticleCard, AudioPlayer, AuthorCard, Avatar, AvatarFallback, AvatarImage, AvatarUpload, Badge, Blockquote, Breadcrumb, Button, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CategoryBadge, Checkbox, Code, CodeBlock, CourseCard, DateField, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, EventCalendar, Footer, FooterLink, HOVER_TARJETA, Hero, Input, LinkRow, MetricBadge, Nav, NavItem, NewsletterForm, PageHeader, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioGroup2 as RadioGroup, RadioGroupItem, SUPERFICIE_TARJETA, ScrollingProgressBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator3 as Separator, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SidebarItem, SidebarNav, Skeleton, Stat, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, TalkCard, Textarea, ThemeToggle, ToastAction, Toaster, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, alert as alertVariants, avatar as avatarVariants, badge as badgeVariants, button as buttonVariants, categoria as categoryBadgeVariants, social_exports as social, toast2 as toast, useTema };
@@ -232,6 +232,33 @@
232
232
  animation: arrecife-shimmer 1.4s linear infinite;
233
233
  }
234
234
 
235
+ /* --- la excepción del plegable ----------------------------------------------
236
+ La cuarta y, como las otras tres, no es «queda mejor»: un plegable que revela
237
+ contenido sin transición hace SALTAR todo lo que tiene debajo, y quien acaba
238
+ de pulsar pierde el sitio en la página. Es la misma categoría que el panel
239
+ lateral —un panel que entra desde un borde se desliza por definición— y no la
240
+ de una animación de entrada, que es lo que el sistema no admite: aquí no
241
+ aparece nada nuevo en pantalla, se abre un hueco.
242
+
243
+ La altura la publica Radix en --radix-accordion-content-height. Dura lo que
244
+ dura todo lo demás y usa la misma curva, así que no estrena un tiempo. En el
245
+ componente va detrás de motion-safe. */
246
+ @keyframes arrecife-despliega {
247
+ from { height: 0; }
248
+ to { height: var(--radix-accordion-content-height); }
249
+ }
250
+ @keyframes arrecife-repliega {
251
+ from { height: var(--radix-accordion-content-height); }
252
+ to { height: 0; }
253
+ }
254
+
255
+ @utility desplegar {
256
+ animation: arrecife-despliega var(--duration-standard) var(--ease-standard);
257
+ }
258
+ @utility replegar {
259
+ animation: arrecife-repliega var(--duration-standard) var(--ease-standard);
260
+ }
261
+
235
262
  /* --- la excepción del panel lateral -----------------------------------------
236
263
  Un panel que entra desde un borde se desliza por definición: quieto sería un
237
264
  modal descentrado. Es la segunda y última excepción a "nada de
package/llms.txt CHANGED
@@ -157,6 +157,25 @@ Tiene que ir INLINE. Un `<script src>`, aunque sea síncrono, se descarga, y el
157
157
  parpadeo vuelve. El script reengancha en `astro:after-swap` porque las
158
158
  transiciones de vista reemplazan el `<html>` entero.
159
159
 
160
+ ### Los iconos de redes van agrupados
161
+
162
+ ```tsx
163
+ // ❌ no existe
164
+ import { GitHub } from '@eduardoalvarez/arrecife';
165
+
166
+ // ✅
167
+ import { social } from '@eduardoalvarez/arrecife';
168
+ <social.GitHub />
169
+ ```
170
+
171
+ Los ocho: `GitHub`, `LinkedIn`, `X`, `Instagram`, `Discord`, `YouTube`, `Rss`,
172
+ `Correo`. Van bajo namespace porque uno se llama `X` y sueltos colisiona.
173
+
174
+ Los glifos internos —`Close`, `ChevronDown`, `Sol`— **no se exportan** y no van a
175
+ exportarse: son el juego mínimo de los primitivos. Un componente que necesita un
176
+ icono lo recibe por prop (`Stat` tiene `icon`, cada `Red` de `Footer` tiene el
177
+ suyo). No pidas que se publiquen: pasa el tuyo.
178
+
160
179
  ## Tokens
161
180
 
162
181
  La fuente es un objeto de TypeScript y la salida CSS se genera de él, así que el
@@ -292,7 +311,7 @@ primitiva de Radix se resumen en la línea `Extiende`, y son los de siempre.
292
311
 
293
312
  ## Primitivos
294
313
 
295
- Se importan de `@eduardoalvarez/arrecife`. 115 exportaciones.
314
+ Se importan de `@eduardoalvarez/arrecife`. 110 exportaciones.
296
315
 
297
316
  ### Accordion, AccordionItem, AccordionTrigger, AccordionContent
298
317
 
@@ -378,7 +397,7 @@ El aviso lleva el color en el fondo, no solo en el borde.
378
397
  | `title` | `ReactNode` | | | |
379
398
  | `variant` | `"accent" \| "success" \| "warning" \| "error"` | | `accent` | |
380
399
 
381
- ### Avatar, AvatarImage, AvatarFallback
400
+ ### Avatar, AvatarImage, AvatarFallback, AvatarUpload
382
401
 
383
402
  Fuente: `src/primitives/avatar.tsx`
384
403
 
@@ -399,6 +418,21 @@ Iniciales mientras la imagen carga, o cuando no hay imagen.
399
418
 
400
419
  - Sin props propios: pasa los del elemento o la primitiva que envuelve.
401
420
 
421
+ **AvatarUpload**
422
+ El avatar que se puede cambiar. `Avatar` muestra; este además deja elegir.
423
+
424
+ - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' \| 'children'> & VariantProps<typeof avatar>`
425
+
426
+ | prop | tipo | req. | defecto | qué hace |
427
+ | --- | --- | --- | --- | --- |
428
+ | `accept` | `string` | | `image/*` | Qué acepta el diálogo del sistema. |
429
+ | `disabled` | `boolean \| undefined` | | `false` | |
430
+ | `fallback` | `ReactNode` | | | Iniciales mientras no hay imagen. |
431
+ | `label` | `string` | | `Cambiar la foto` | Nombre accesible del control. Es lo único que lo nombra: no hay texto visible. |
432
+ | `onSelectFile` | `(archivo: File) => void` | | | Se dispara con el archivo elegido. La subida la hace el proyecto. |
433
+ | `size` | `"sm" \| "md" \| "lg" \| "xl"` | | | |
434
+ | `src` | `string` | | | La imagen actual, ya subida. La previsualización local la gana mientras dure. |
435
+
402
436
  ### Badge, CategoryBadge, MetricBadge
403
437
 
404
438
  Fuente: `src/primitives/badge.tsx`
@@ -837,34 +871,6 @@ Fuente: `src/primitives/textarea.tsx`
837
871
  | --- | --- | --- | --- | --- |
838
872
  | `invalid` | `boolean` | | `false` | |
839
873
 
840
- ### ToastViewport, Toast, ToastTitle, ToastDescription, ToastProvider, ToastAction
841
-
842
- Fuente: `src/primitives/toast.tsx`
843
-
844
- **ToastViewport**
845
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
846
-
847
- **Toast**
848
- Sin deslizamiento de entrada: el aviso aparece donde va a quedarse.
849
-
850
- - Extiende: `ComponentPropsWithoutRef<typeof ToastPrimitive.Root> & VariantProps<typeof toast>`
851
-
852
- | prop | tipo | req. | defecto | qué hace |
853
- | --- | --- | --- | --- | --- |
854
- | `variant` | `"success" \| "error" \| "neutral"` | | `neutral` | |
855
-
856
- **ToastTitle**
857
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
858
-
859
- **ToastDescription**
860
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
861
-
862
- **ToastProvider**
863
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
864
-
865
- **ToastAction**
866
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
867
-
868
874
  ### Toaster
869
875
 
870
876
  Fuente: `src/primitives/toaster.tsx`
@@ -910,7 +916,7 @@ Fuente: `src/primitives/typography.tsx`
910
916
 
911
917
  ## Componentes
912
918
 
913
- Se importan de `@eduardoalvarez/arrecife`. 23 exportaciones.
919
+ Se importan de `@eduardoalvarez/arrecife`. 24 exportaciones.
914
920
 
915
921
  ### ArticleCard
916
922
 
@@ -1031,6 +1037,24 @@ La regla más importante de la mascota, por fin como código.
1031
1037
  | `expresion` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | sí | | La cara. Obligatoria: sin ella esto es un párrafo centrado. |
1032
1038
  | `title` | `ReactNode` | sí | | |
1033
1039
 
1040
+ ### EventCalendar
1041
+
1042
+ Fuente: `src/components/event-calendar/index.tsx`
1043
+
1044
+ - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' \| 'children'>`
1045
+
1046
+ | prop | tipo | req. | defecto | qué hace |
1047
+ | --- | --- | --- | --- | --- |
1048
+ | `emptyMessage` | `ReactNode` | | `Nada en este día.` | Texto del panel cuando el día elegido no tiene nada. |
1049
+ | `events` | `readonly EventoCalendario[]` | sí | | |
1050
+ | `formatDay` | `(dia: Date) => string` | | `(dia) => format(dia, "EEEE d de MMMM", { locale: es })` | Encabezado del panel. Por defecto, `es` de date-fns, como `Calendar`. |
1051
+ | `formatTime` | `(fecha: Date) => string` | | `(fecha) => format(fecha, HH:mm, { locale: es })` | |
1052
+ | `onCreateEvent` | `(evento: Omit<EventoCalendario, "id">) => void` | | | Sin él, la agenda es de solo lectura y no se pinta el formulario. |
1053
+ | `onDeleteEvent` | `(id: string) => void` | | | |
1054
+ | `onSelectDay` | `(dia: Date) => void` | | | |
1055
+ | `onUpdateEvent` | `(evento: EventoCalendario) => void` | | | |
1056
+ | `selected` | `Date` | | | Día seleccionado, si el proyecto lo controla. Sin él, empieza en hoy. |
1057
+
1034
1058
  ### Footer, FooterLink
1035
1059
 
1036
1060
  Fuente: `src/components/footer/index.tsx`
@@ -1198,6 +1222,8 @@ Una métrica grande: el número en la escala `stat` y su nombre debajo.
1198
1222
 
1199
1223
  | prop | tipo | req. | defecto | qué hace |
1200
1224
  | --- | --- | --- | --- | --- |
1225
+ | `description` | `ReactNode` | | | La bajada: el matiz que el número solo no da. «12 aplicaciones» no dice si son muchas, y aquí es donde se dice. |
1226
+ | `icon` | `ReactNode` | | | Glifo al lado del título, a 1em. Hereda `currentColor`, así que sigue al tono del título y no hay que teñirlo aparte. |
1201
1227
  | `label` | `ReactNode` | sí | | Qué se está contando. Va en mono versalitas. |
1202
1228
  | `progress` | `number` | | | Con `progress`, la métrica se lee como avance y añade la barra. |
1203
1229
  | `tone` | `"neutral" \| "alerta"` | | `neutral` | `alerta` solo cuando el número ES el problema. |
@@ -1274,6 +1300,7 @@ El wordmark sale de `naming.wordmark`, no de una cadena escrita a mano, y siempr
1274
1300
  | prop | tipo | req. | defecto | qué hace |
1275
1301
  | --- | --- | --- | --- | --- |
1276
1302
  | `basePath` | `string` | | `RUTA_ASSETS` | |
1303
+ | `conLema` | `boolean \| undefined` | | `false` | Añade el lema bajo el wordmark, separado de la aleta por una divisoria. |
1277
1304
  | `sobre` | `"oscuro" \| "claro"` | | `oscuro` | |
1278
1305
  | `soloIsotipo` | `boolean \| undefined` | | `false` | Oculta el wordmark y deja solo la aleta, para barras muy estrechas. |
1279
1306
 
@@ -1513,7 +1540,7 @@ Tipos (6): `DatosArticulo`, `DatosBase`, `DatosCharla`, `DatosCurso`, `DatosDefe
1513
1540
  | `toast` | `(mensaje: ReactNode, opciones?: ToastOptions \| undefined): string` | Lanza un aviso. Devuelve su id, que es lo que hay que guardar para cerrarlo a mano —el caso de «guardando…» que se reemplaza cuando termina la petición. |
1514
1541
  | `useTema` | `(): Tema` | El tema puesto ahora mismo, para un proyecto que necesite ramificar en React —un logo distinto por modo, una imagen que no tiene versión clara—. |
1515
1542
 
1516
- Tipos (58): `AccordionProps`, `AccordionTriggerProps`, `AlertProps`, `ArticleCardProps`, `AudioPlayerMode`, `AudioPlayerProps`, `AuthorCardProps`, `AvatarProps`, `BadgeProps`, `BlockquoteProps`, `BreadcrumbProps`, `ButtonProps`, `CalendarProps`, `CategoryBadgeProps`, `CheckboxProps`, `CodeBlockProps`, `CodeProps`, `CourseCardProps`, `DateFieldProps`, `EmptyStateProps`, `Entrada`, `FooterLinkProps`, `FooterProps`, `HeroProps`, `InputProps`, `LabelProps`, `LinkRowProps`, `MetricBadgeProps`, `Migaja`, `NavItemProps`, `NavProps`, `NewsletterFormProps`, `NewsletterState`, `PageHeaderProps`, `PaginationLinkProps`, `PopoverContentProps`, `ProgressProps`, `RadioGroupItemProps`, `RadioGroupProps`, `Red`, `ScrollingProgressBarProps`, `SeparatorProps`, `SheetContentProps`, `SidebarItemProps`, `SidebarNavProps`, `SkeletonProps`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ThemeToggleProps`, `ToastOptions`, `ToastProps`, `ToastVariant`, `ToasterProps`.
1543
+ Tipos (60): `AccordionProps`, `AccordionTriggerProps`, `AlertProps`, `ArticleCardProps`, `AudioPlayerMode`, `AudioPlayerProps`, `AuthorCardProps`, `AvatarProps`, `AvatarUploadProps`, `BadgeProps`, `BlockquoteProps`, `BreadcrumbProps`, `ButtonProps`, `CalendarProps`, `CategoryBadgeProps`, `CheckboxProps`, `CodeBlockProps`, `CodeProps`, `CourseCardProps`, `DateFieldProps`, `EmptyStateProps`, `Entrada`, `EventCalendarProps`, `EventoCalendario`, `FooterLinkProps`, `FooterProps`, `HeroProps`, `InputProps`, `LabelProps`, `LinkRowProps`, `MetricBadgeProps`, `Migaja`, `NavItemProps`, `NavProps`, `NewsletterFormProps`, `NewsletterState`, `PageHeaderProps`, `PaginationLinkProps`, `PopoverContentProps`, `ProgressProps`, `RadioGroupItemProps`, `RadioGroupProps`, `Red`, `ScrollingProgressBarProps`, `SeparatorProps`, `SheetContentProps`, `SidebarItemProps`, `SidebarNavProps`, `SkeletonProps`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ThemeToggleProps`, `ToastOptions`, `ToastVariant`, `ToasterProps`.
1517
1544
 
1518
1545
  # Dónde mirar si esto no basta
1519
1546
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduardoalvarez/arrecife",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Librería de componentes de la identidad visual de Eduardo Álvarez",
5
5
  "license": "MIT",
6
6
  "author": "Eduardo Esteban Álvarez Castañeda <soy@eduardoalvarez.dev>",