innoboxrr-form-core 2.2.0 → 2.4.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 CHANGED
@@ -1,49 +1,75 @@
1
1
  # innoboxrr-form-core
2
2
 
3
- Lo que comparten [`innoboxrr-form-elements`](../form-elements) (Vue) y
4
- [`innoboxrr-react-form-elements`](../react-form-elements) (React) y no depende
5
- de ningún framework.
3
+ Lo que comparten [`innoboxrr-form-elements`](../form-elements) (Vue),
4
+ [`innoboxrr-react-form-elements`](../react-form-elements) (React) y los dos
5
+ datatables, y no depende de ningún framework: el tema, los iconos, las hojas de
6
+ estilo, los archivos y las zonas horarias.
6
7
 
7
- Estaba duplicado en los dos paquetes, que es exactamente como dos copias
8
- empiezan a divergir.
8
+ Estaba duplicado en los paquetes, que es exactamente como dos copias empiezan a
9
+ divergir.
9
10
 
10
11
  ```
11
12
  npm i innoboxrr-form-core
12
13
  ```
13
14
 
14
- ## Tema
15
+ ## Estilos
16
+
17
+ ```js
18
+ import 'innoboxrr-form-core/styles'
19
+ ```
20
+
21
+ Una sola importación trae las tres hojas: `tokens.css` (las variables),
22
+ `layout.css` (las utilidades de maquetación) y `components.css` (los
23
+ componentes). Nada de lo generado necesita UIkit, Tailwind ni Font Awesome.
24
+
25
+ Colores, formas, densidad, capas y movimiento son variables CSS. Para cambiar el
26
+ aspecto de todo, se redefinen:
15
27
 
16
- Antes cada componente llevaba su cadena CSS incrustada como valor por defecto
17
- de `customClass` —repetida en los dos paquetes— y el código que genera
18
- `larapack-generator` esperaba `inputClass` y `buttonClass` de un mixin global
19
- que la aplicación anfitriona tenía que registrar sin que nada lo dijera.
28
+ ```css
29
+ :root {
30
+ --fe-primary: #7c3aed;
31
+ --fe-radius: 10px;
32
+ --fe-density: 0.875;
33
+ }
34
+ ```
35
+
36
+ El modo oscuro responde a la preferencia del sistema y a un `data-theme="dark"`
37
+ en la raíz; `data-theme="light"` fuerza el claro aunque el sistema esté en
38
+ oscuro.
39
+
40
+ ## Tema
20
41
 
21
- Ahora es un mapa de tokens que la aplicación ajusta **una vez**, al arrancar:
42
+ Un mapa de tokens que la aplicación ajusta **una vez**, al arrancar. Cada token
43
+ apunta a una clase del sistema propio (`fe-*`), así que el caso normal es no
44
+ tocarlo; sirve para apuntar un token a las clases de otro sistema visual cuando
45
+ la aplicación ya tiene el suyo:
22
46
 
23
47
  ```js
24
48
  import { setTheme } from 'innoboxrr-form-core'
25
49
 
26
50
  setTheme({
27
51
  input: 'form-control',
28
- select: 'form-select',
29
52
  button: 'btn btn-primary',
30
53
  })
31
54
  ```
32
55
 
33
- Los tokens:
34
-
35
56
  | Grupo | Tokens |
36
57
  |---|---|
37
58
  | Envoltorios | `field`, `fieldInner`, `label`, `help`, `helpIcon`, `error` |
38
59
  | Controles | `input`, `select`, `textarea`, `checkbox`, `radio`, `file` |
39
- | Botones | `button`, `buttonSecondary`, `buttonDanger`, `buttonLink` |
40
-
41
- Los valores de fábrica son los de UIkit + Tailwind que ya usaba el ecosistema,
42
- así que actualizar no cambia nada de aspecto.
43
-
44
- `customClass` sigue existiendo y **reemplaza** al token, para el caso puntual.
45
- Antes unos controles lo reemplazaban y otros lo sumaban a una clase incrustada;
46
- ahora se comportan todos igual.
60
+ | Botones | `button`, `buttonSecondary`, `buttonDanger`, `buttonLink`, `iconButton` |
61
+ | Navegación | `breadcrumb`, `breadcrumbLink`, `breadcrumbCurrent`, `breadcrumbSeparator`, `actionMenu`, `actionMenuItem`, `actionMenuDanger`, `kbd` |
62
+ | Superficies | `surface`, `surfaceRaised`, `toolbar`, `badge`, `badgePrimary`, `badgeSuccess`, `badgeDanger`, `badgeWarning` |
63
+ | Esqueletos | `skeleton`, `skeletonText`, `skeletonCircle`, `skeletonBlock` |
64
+ | Diálogos y drawers | `overlay`, `dialog`, `dialogSmall`, `dialogLarge`, `dialogHeader`, `dialogTitle`, `dialogBody`, `dialogFooter`, `drawer`, `drawerStart`, `drawerHeader`, `drawerTitle`, `drawerBody`, `drawerFooter` |
65
+ | Menús | `menu`, `menuList`, `menuItem`, `menuItemDanger`, `menuSeparator`, `menuLabel` |
66
+ | Paleta de comandos | `command`, `commandInput`, `commandList`, `commandGroup`, `commandItem`, `commandEmpty` |
67
+ | Avisos | `toast`, `toastSuccess`, `toastDanger`, `toastWarning`, `toastTitle`, `toastClose`, `toastRegion` |
68
+ | Tabla | `table`, `tableNumeric`, `tableSticky`, `tableSelect`, `tableResizer`, `tableEmpty`, `bulkBar`, `bulkCount` |
69
+ | Edición en línea | `editable` |
70
+ | Shell de aplicación | `shell`, `shellHeader`, `shellSidebar`, `shellMain` |
71
+
72
+ `customClass` **reemplaza** al token, para el caso puntual:
47
73
 
48
74
  ```js
49
75
  import { getTheme, resetTheme, onThemeChange, classFor } from 'innoboxrr-form-core'
@@ -55,9 +81,57 @@ resetTheme() // vuelve a fábrica (sobre todo para pruebas)
55
81
  onThemeChange(fn) // devuelve la función para dejar de escuchar
56
82
  ```
57
83
 
58
- Los dos paquetes de componentes se suscriben, así que **un `setTheme` en
59
- caliente repinta lo que ya esté montado**: leerlo directamente no lo haría,
60
- porque el tema es estado de módulo, fuera del framework.
84
+ Los paquetes de componentes se suscriben, así que **un `setTheme` en caliente
85
+ repinta lo que ya esté montado**.
86
+
87
+ ## Piezas de escritorio
88
+
89
+ Diálogos, drawers, menús y avisos se apoyan en lo que ya hace el navegador, sin
90
+ librerías de interfaz: `<dialog>` con `showModal()` pone la capa superior, el
91
+ fondo inerte, el foco atrapado, Escape para cerrar y la devolución del foco; el
92
+ atributo `popover` pone el cierre al pulsar fuera. Estas hojas solo los dibujan.
93
+ Los componentes de Vue y React envuelven exactamente este marcado.
94
+
95
+ ```html
96
+ <dialog class="fe-drawer">
97
+ <header class="fe-drawer-header">
98
+ <h2 class="fe-drawer-title">Nuevo producto</h2>
99
+ <button class="fe-icon-button" aria-label="Cerrar">…</button>
100
+ </header>
101
+ <div class="fe-drawer-body">…</div>
102
+ <footer class="fe-drawer-footer">…</footer>
103
+ </dialog>
104
+ ```
105
+
106
+ - **Diálogo**: `fe-dialog`, con `fe-dialog-sm` o `fe-dialog-lg` para el ancho.
107
+ - **Drawer**: `fe-drawer` a la derecha; `fe-drawer-start` lo pega a la izquierda.
108
+ - **Paleta de comandos**: `<dialog class="fe-dialog fe-command">` con
109
+ `fe-command-input` y una `fe-command-list` de `fe-command-item`; el elemento
110
+ activo lleva `aria-selected="true"`.
111
+ - **Avisos**: una `fe-toast-region` con `popover="manual"`, para seguir encima
112
+ de un drawer abierto con `showModal()`.
113
+ - **Menús**: `fe-menu` con `popover`, colocado con Floating UI.
114
+ - **Tabla**: `fe-table-select` para la columna de casillas, `data-selected` en
115
+ la fila, `fe-bulk-bar` para lo que se hace con la selección y
116
+ `fe-table-resizer` para cambiar el ancho de una columna.
117
+
118
+ Un `<dialog>` cerrado está oculto porque el navegador le pone `display: none`;
119
+ por eso ninguna regla fija `display` fuera del estado `[open]`. Las capas que no
120
+ viven en la capa superior —la cabecera fija de una tabla, un overlay dibujado a
121
+ mano, los tooltips— siguen la escala `--fe-z-*`, nunca un número suelto.
122
+
123
+ ## Iconos
124
+
125
+ Un nombre semántico y un mapa que decide de dónde sale el dibujo. Los valores son
126
+ nombres de Iconify (`coleccion:icono`):
127
+
128
+ ```js
129
+ import { setIcons, iconFor } from 'innoboxrr-form-core'
130
+
131
+ setIcons({ plus: 'lucide:plus', delete: 'lucide:trash-2' })
132
+ iconFor('plus') // 'lucide:plus'
133
+ iconFor('mdi:home') // un nombre completo pasa tal cual
134
+ ```
61
135
 
62
136
  ## Archivos
63
137
 
@@ -76,10 +150,6 @@ hay que meter en el `FormData`.
76
150
 
77
151
  `validateFiles` es la misma función devolviendo una promesa, por compatibilidad.
78
152
 
79
- La vista previa de lo que no es imagen es un `data:` URI. Antes era una URL de
80
- `icon-library.com`: cada archivo disparaba una petición a un tercero que puede
81
- caerse o cambiar.
82
-
83
153
  ## Zonas horarias
84
154
 
85
155
  ```js
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "innoboxrr-form-core",
3
- "version": "2.2.0",
4
- "description": "El nucleo agnostico de los formularios innoboxrr: tema, archivos y zonas horarias.",
3
+ "version": "2.4.0",
4
+ "description": "El nucleo agnostico de la interfaz innoboxrr: tema, iconos, estilos, archivos y zonas horarias.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -104,7 +104,7 @@
104
104
  [data-tooltip]::after {
105
105
  content: attr(data-tooltip);
106
106
  position: absolute;
107
- z-index: 60;
107
+ z-index: var(--fe-z-tooltip);
108
108
  bottom: calc(100% + var(--fe-space-2));
109
109
  left: 50%;
110
110
  transform: translateX(-50%);
@@ -329,6 +329,38 @@
329
329
  font-size: var(--fe-text-sm);
330
330
  }
331
331
 
332
+ /* Un botón que solo lleva un icono: cerrar un diálogo, abrir el menú de una
333
+ fila. Cuadrado, para que el objetivo no dependa del ancho del glifo. */
334
+ .fe-icon-button {
335
+ display: inline-flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ width: var(--fe-control-height);
339
+ height: var(--fe-control-height);
340
+ padding: 0;
341
+ color: var(--fe-text-muted);
342
+ background: none;
343
+ border: 1px solid transparent;
344
+ border-radius: var(--fe-radius);
345
+ cursor: pointer;
346
+ transition: background var(--fe-transition), color var(--fe-transition);
347
+ }
348
+
349
+ .fe-icon-button:hover:not(:disabled) {
350
+ color: var(--fe-text);
351
+ background: var(--fe-surface-hover);
352
+ }
353
+
354
+ .fe-icon-button:focus-visible {
355
+ outline: 2px solid var(--fe-focus);
356
+ outline-offset: 2px;
357
+ }
358
+
359
+ .fe-icon-button:disabled {
360
+ opacity: 0.55;
361
+ cursor: not-allowed;
362
+ }
363
+
332
364
  /* El icono que va dentro del control, a la izquierda. Sustituye a
333
365
  `uk-form-icon`, que además de la clase traía el componente de iconos de
334
366
  UIkit. */
@@ -433,6 +465,22 @@
433
465
  text-decoration: underline;
434
466
  }
435
467
 
468
+ /* La tecla de un atajo, en un menú o en la paleta de comandos. */
469
+ .fe-kbd {
470
+ display: inline-flex;
471
+ align-items: center;
472
+ min-width: 1.25rem;
473
+ height: 1.25rem;
474
+ padding: 0 var(--fe-space-1);
475
+ font-family: var(--fe-font-mono);
476
+ font-size: var(--fe-text-xs);
477
+ color: var(--fe-text-muted);
478
+ background: var(--fe-surface-sunk);
479
+ border: 1px solid var(--fe-border);
480
+ border-bottom-width: 2px;
481
+ border-radius: var(--fe-radius-sm);
482
+ }
483
+
436
484
  /* ============================================================
437
485
  SUPERFICIES
438
486
  ============================================================ */
@@ -474,6 +522,30 @@
474
522
  color: var(--fe-text-muted);
475
523
  }
476
524
 
525
+ .fe-badge-primary {
526
+ background: var(--fe-primary-soft);
527
+ color: var(--fe-primary);
528
+ }
529
+
530
+ .fe-badge-success {
531
+ background: var(--fe-success-soft);
532
+ color: var(--fe-success);
533
+ }
534
+
535
+ .fe-badge-danger {
536
+ background: var(--fe-danger-soft);
537
+ color: var(--fe-danger);
538
+ }
539
+
540
+ .fe-badge-warning {
541
+ background: var(--fe-warning-soft);
542
+ color: var(--fe-warning);
543
+ }
544
+
545
+ /* ============================================================
546
+ ESQUELETOS
547
+ ============================================================ */
548
+
477
549
  /* Un esqueleto en vez de un spinner: mantiene la forma de lo que va a
478
550
  llegar, así que la página no salta al cargar. */
479
551
  .fe-skeleton {
@@ -488,6 +560,32 @@
488
560
  animation: fe-skeleton 1.4s ease infinite;
489
561
  }
490
562
 
563
+ /* Las tres formas que aparecen en una vista de administración: una línea de
564
+ texto, un avatar y un bloque. Sin ellas cada vista inventaba su altura. */
565
+ .fe-skeleton-text {
566
+ display: block;
567
+ width: 100%;
568
+ height: 0.875em;
569
+ }
570
+
571
+ .fe-skeleton-text + .fe-skeleton-text {
572
+ margin-top: var(--fe-space-2);
573
+ }
574
+
575
+ .fe-skeleton-circle {
576
+ display: inline-block;
577
+ width: 2.5rem;
578
+ aspect-ratio: 1;
579
+ border-radius: var(--fe-radius-full);
580
+ }
581
+
582
+ .fe-skeleton-block {
583
+ display: block;
584
+ width: 100%;
585
+ height: 6rem;
586
+ border-radius: var(--fe-radius);
587
+ }
588
+
491
589
  @keyframes fe-skeleton {
492
590
  0% {
493
591
  background-position: 100% 50%;
@@ -497,35 +595,181 @@
497
595
  }
498
596
  }
499
597
 
598
+ /* ============================================================
599
+ DIALOGOS Y DRAWERS
600
+ ============================================================ */
601
+
602
+ /*
603
+ * Sobre el elemento <dialog> del navegador, abierto con showModal():
604
+ *
605
+ * <dialog class="fe-dialog">
606
+ * <header class="fe-dialog-header">…</header>
607
+ * <div class="fe-dialog-body">…</div>
608
+ * <footer class="fe-dialog-footer">…</footer>
609
+ * </dialog>
610
+ *
611
+ * El navegador pone lo que antes había que programar: la capa superior sin
612
+ * z-index, el fondo inerte, el foco atrapado dentro, Escape para cerrar y la
613
+ * devolución del foco al cerrar. Aquí solo se dibuja.
614
+ *
615
+ * Nada de lo de aquí fija `display` en el estado cerrado: un <dialog> sin
616
+ * `open` está oculto porque el navegador le pone display: none, y una regla
617
+ * que lo pisara lo dejaría visible siempre.
618
+ */
619
+
500
620
  .fe-overlay {
501
621
  position: fixed;
502
622
  inset: 0;
503
- background: rgba(16, 18, 27, 0.45);
623
+ z-index: var(--fe-z-overlay);
624
+ background: var(--fe-overlay);
504
625
  }
505
626
 
506
627
  .fe-dialog {
628
+ width: min(var(--fe-dialog-width), calc(100vw - 2 * var(--fe-space-4)));
629
+ max-height: 85vh;
630
+ padding: 0;
631
+ color: var(--fe-text);
507
632
  background: var(--fe-surface-raised);
508
633
  border: 1px solid var(--fe-border);
509
634
  border-radius: var(--fe-radius-lg);
510
635
  box-shadow: var(--fe-shadow-lg);
511
- max-height: 85vh;
512
636
  overflow: auto;
513
637
  }
514
638
 
639
+ .fe-dialog[open] {
640
+ display: flex;
641
+ flex-direction: column;
642
+ animation: fe-dialog-in var(--fe-duration) var(--fe-ease-out);
643
+ }
644
+
645
+ .fe-dialog::backdrop,
646
+ .fe-drawer::backdrop {
647
+ background: var(--fe-overlay);
648
+ animation: fe-backdrop-in var(--fe-duration) var(--fe-ease-out);
649
+ }
650
+
651
+ .fe-dialog-sm {
652
+ --fe-dialog-width: 24rem;
653
+ }
654
+
655
+ .fe-dialog-lg {
656
+ --fe-dialog-width: 48rem;
657
+ }
658
+
659
+ .fe-dialog-header,
660
+ .fe-drawer-header {
661
+ display: flex;
662
+ align-items: center;
663
+ justify-content: space-between;
664
+ gap: var(--fe-space-3);
665
+ padding: var(--fe-space-4) var(--fe-space-5);
666
+ border-bottom: 1px solid var(--fe-border);
667
+ }
668
+
669
+ .fe-dialog-title,
670
+ .fe-drawer-title {
671
+ margin: 0;
672
+ font-size: var(--fe-text-lg);
673
+ font-weight: 600;
674
+ color: var(--fe-text);
675
+ }
676
+
677
+ .fe-dialog-body,
678
+ .fe-drawer-body {
679
+ padding: var(--fe-space-5);
680
+ overflow: auto;
681
+ }
682
+
683
+ .fe-dialog-footer,
684
+ .fe-drawer-footer {
685
+ display: flex;
686
+ flex-wrap: wrap;
687
+ align-items: center;
688
+ justify-content: flex-end;
689
+ gap: var(--fe-space-2);
690
+ padding: var(--fe-space-3) var(--fe-space-5);
691
+ border-top: 1px solid var(--fe-border);
692
+ }
693
+
694
+ /* Un drawer es un <dialog> pegado a un borde. El formulario de crear o
695
+ editar se abre aquí y la tabla sigue a la vista, detrás. */
515
696
  .fe-drawer {
516
- position: fixed;
517
- top: 0;
518
- right: 0;
519
- bottom: 0;
520
- width: min(32rem, 100vw);
697
+ width: min(var(--fe-drawer-width), 100vw);
698
+ max-width: 100vw;
699
+ height: 100dvh;
700
+ max-height: none;
701
+ margin: 0 0 0 auto;
702
+ padding: 0;
703
+ color: var(--fe-text);
521
704
  background: var(--fe-surface);
705
+ border: none;
522
706
  border-left: 1px solid var(--fe-border);
523
707
  box-shadow: var(--fe-shadow-lg);
708
+ overflow: hidden;
709
+ }
710
+
711
+ .fe-drawer[open] {
524
712
  display: flex;
525
713
  flex-direction: column;
526
- overflow: auto;
714
+ animation: fe-drawer-in var(--fe-duration) var(--fe-ease-out);
527
715
  }
528
716
 
717
+ .fe-drawer-body {
718
+ flex: 1 1 auto;
719
+ }
720
+
721
+ .fe-drawer-start {
722
+ margin: 0 auto 0 0;
723
+ border-left: none;
724
+ border-right: 1px solid var(--fe-border);
725
+ }
726
+
727
+ .fe-drawer-start[open] {
728
+ animation-name: fe-drawer-start-in;
729
+ }
730
+
731
+ @keyframes fe-dialog-in {
732
+ from {
733
+ opacity: 0;
734
+ transform: translateY(0.5rem) scale(0.98);
735
+ }
736
+ to {
737
+ opacity: 1;
738
+ transform: none;
739
+ }
740
+ }
741
+
742
+ @keyframes fe-drawer-in {
743
+ from {
744
+ transform: translateX(100%);
745
+ }
746
+ to {
747
+ transform: none;
748
+ }
749
+ }
750
+
751
+ @keyframes fe-drawer-start-in {
752
+ from {
753
+ transform: translateX(-100%);
754
+ }
755
+ to {
756
+ transform: none;
757
+ }
758
+ }
759
+
760
+ @keyframes fe-backdrop-in {
761
+ from {
762
+ opacity: 0;
763
+ }
764
+ to {
765
+ opacity: 1;
766
+ }
767
+ }
768
+
769
+ /* ============================================================
770
+ MENUS
771
+ ============================================================ */
772
+
529
773
  .fe-menu {
530
774
  min-width: 11rem;
531
775
  padding: var(--fe-space-1);
@@ -535,6 +779,29 @@
535
779
  box-shadow: var(--fe-shadow);
536
780
  }
537
781
 
782
+ /*
783
+ * Un menú que vive en la capa superior del navegador, con el atributo
784
+ * `popover`. El navegador ya se encarga de abrirlo, cerrarlo al pulsar fuera,
785
+ * cerrarlo con Escape y sacarlo por encima de todo; aquí solo hay que quitarle
786
+ * los estilos que trae de fábrica para que Floating UI pueda colocarlo.
787
+ */
788
+ .fe-menu[popover] {
789
+ position: fixed;
790
+ inset: auto;
791
+ margin: 0;
792
+ overflow: visible;
793
+ }
794
+
795
+ .fe-menu[popover]:popover-open {
796
+ animation: fe-menu-in var(--fe-duration-fast) ease;
797
+ }
798
+
799
+ .fe-menu-list {
800
+ list-style: none;
801
+ margin: 0;
802
+ padding: 0;
803
+ }
804
+
538
805
  .fe-menu-item {
539
806
  display: flex;
540
807
  align-items: center;
@@ -544,6 +811,7 @@
544
811
  font-size: var(--fe-text-sm);
545
812
  color: var(--fe-text);
546
813
  text-align: left;
814
+ text-decoration: none;
547
815
  background: none;
548
816
  border: none;
549
817
  border-radius: var(--fe-radius-sm);
@@ -555,17 +823,159 @@
555
823
  background: var(--fe-surface-hover);
556
824
  }
557
825
 
826
+ .fe-menu-item:focus-visible {
827
+ outline: 2px solid var(--fe-focus);
828
+ outline-offset: -2px;
829
+ }
830
+
831
+ .fe-menu-item-danger {
832
+ color: var(--fe-danger);
833
+ }
834
+
835
+ .fe-menu-item-danger:hover,
836
+ .fe-menu-item-danger[data-highlighted] {
837
+ background: var(--fe-danger-soft);
838
+ }
839
+
840
+ /* Un elemento que no se puede usar se ve y se explica, no desaparece: quien no
841
+ tiene permiso tiene que saber que la acción existe. */
842
+ .fe-menu-item:disabled,
843
+ .fe-menu-item[aria-disabled='true'] {
844
+ color: var(--fe-text-subtle);
845
+ background: none;
846
+ cursor: not-allowed;
847
+ }
848
+
849
+ .fe-menu-item .fe-kbd {
850
+ margin-left: auto;
851
+ }
852
+
853
+ .fe-menu-separator {
854
+ height: 1px;
855
+ margin: var(--fe-space-1) 0;
856
+ background: var(--fe-border);
857
+ border: none;
858
+ }
859
+
860
+ .fe-menu-label {
861
+ padding: var(--fe-space-2) var(--fe-space-3) var(--fe-space-1);
862
+ font-size: var(--fe-text-xs);
863
+ font-weight: 500;
864
+ letter-spacing: 0.05em;
865
+ text-transform: uppercase;
866
+ color: var(--fe-text-subtle);
867
+ }
868
+
869
+ @keyframes fe-menu-in {
870
+ from {
871
+ opacity: 0;
872
+ }
873
+ to {
874
+ opacity: 1;
875
+ }
876
+ }
877
+
878
+ /* ============================================================
879
+ PALETA DE COMANDOS
880
+ ============================================================ */
881
+
882
+ /*
883
+ * Un <dialog class="fe-dialog fe-command"> con un buscador y una lista. Se
884
+ * abre arriba y no centrado: el ojo va a donde se escribe, y la lista crece
885
+ * hacia abajo sin mover el buscador.
886
+ */
887
+
888
+ .fe-command {
889
+ --fe-dialog-width: var(--fe-command-width);
890
+ margin: 12vh auto auto;
891
+ max-height: 70vh;
892
+ }
893
+
894
+ .fe-command-input {
895
+ width: 100%;
896
+ min-height: calc(var(--fe-control-height) * 1.4);
897
+ padding: 0 var(--fe-space-5);
898
+ font-family: var(--fe-font);
899
+ font-size: var(--fe-text-lg);
900
+ color: var(--fe-text);
901
+ background: none;
902
+ border: none;
903
+ border-bottom: 1px solid var(--fe-border);
904
+ }
905
+
906
+ .fe-command-input:focus {
907
+ outline: none;
908
+ }
909
+
910
+ .fe-command-input::placeholder {
911
+ color: var(--fe-text-subtle);
912
+ }
913
+
914
+ .fe-command-list {
915
+ list-style: none;
916
+ margin: 0;
917
+ padding: var(--fe-space-2);
918
+ overflow: auto;
919
+ }
920
+
921
+ .fe-command-group {
922
+ padding: var(--fe-space-2) var(--fe-space-3) var(--fe-space-1);
923
+ font-size: var(--fe-text-xs);
924
+ font-weight: 500;
925
+ letter-spacing: 0.05em;
926
+ text-transform: uppercase;
927
+ color: var(--fe-text-subtle);
928
+ }
929
+
930
+ .fe-command-item {
931
+ display: flex;
932
+ align-items: center;
933
+ gap: var(--fe-space-3);
934
+ width: 100%;
935
+ padding: var(--fe-space-2) var(--fe-space-3);
936
+ font-size: var(--fe-text-base);
937
+ color: var(--fe-text);
938
+ text-align: left;
939
+ background: none;
940
+ border: none;
941
+ border-radius: var(--fe-radius);
942
+ cursor: pointer;
943
+ }
944
+
945
+ .fe-command-item[aria-selected='true'],
946
+ .fe-command-item[data-highlighted] {
947
+ background: var(--fe-primary-soft);
948
+ color: var(--fe-text);
949
+ }
950
+
951
+ .fe-command-item .fe-kbd {
952
+ margin-left: auto;
953
+ }
954
+
955
+ .fe-command-empty {
956
+ padding: var(--fe-space-6) var(--fe-space-4);
957
+ font-size: var(--fe-text-sm);
958
+ text-align: center;
959
+ color: var(--fe-text-muted);
960
+ }
961
+
962
+ /* ============================================================
963
+ AVISOS
964
+ ============================================================ */
965
+
558
966
  .fe-toast {
559
967
  display: flex;
560
968
  align-items: flex-start;
561
969
  gap: var(--fe-space-3);
562
970
  padding: var(--fe-space-3) var(--fe-space-4);
563
971
  font-size: var(--fe-text-sm);
972
+ color: var(--fe-text);
564
973
  background: var(--fe-surface-raised);
565
974
  border: 1px solid var(--fe-border);
566
975
  border-left: 3px solid var(--fe-primary);
567
976
  border-radius: var(--fe-radius);
568
977
  box-shadow: var(--fe-shadow);
978
+ animation: fe-toast-in var(--fe-duration) var(--fe-ease-out);
569
979
  }
570
980
 
571
981
  .fe-toast-danger {
@@ -576,6 +986,61 @@
576
986
  border-left-color: var(--fe-success);
577
987
  }
578
988
 
989
+ .fe-toast-warning {
990
+ border-left-color: var(--fe-warning);
991
+ }
992
+
993
+ .fe-toast-title {
994
+ display: block;
995
+ font-weight: 600;
996
+ }
997
+
998
+ .fe-toast-close {
999
+ margin-left: auto;
1000
+ }
1001
+
1002
+ /*
1003
+ * La región donde se apilan los avisos. Va con el atributo popover="manual"
1004
+ * para vivir en la capa superior: un aviso que llega mientras hay un drawer
1005
+ * abierto con showModal() quedaría debajo del fondo inerte, y justo ahí es
1006
+ * donde se guarda un formulario.
1007
+ */
1008
+ .fe-toast-region {
1009
+ position: fixed;
1010
+ right: var(--fe-space-4);
1011
+ bottom: var(--fe-space-4);
1012
+ z-index: var(--fe-z-toast);
1013
+ display: flex;
1014
+ flex-direction: column;
1015
+ gap: var(--fe-space-2);
1016
+ width: min(24rem, calc(100vw - 2 * var(--fe-space-4)));
1017
+ pointer-events: none;
1018
+ }
1019
+
1020
+ .fe-toast-region[popover] {
1021
+ inset: auto var(--fe-space-4) var(--fe-space-4) auto;
1022
+ margin: 0;
1023
+ padding: 0;
1024
+ overflow: visible;
1025
+ background: transparent;
1026
+ border: none;
1027
+ }
1028
+
1029
+ .fe-toast-region > * {
1030
+ pointer-events: auto;
1031
+ }
1032
+
1033
+ @keyframes fe-toast-in {
1034
+ from {
1035
+ opacity: 0;
1036
+ transform: translateY(0.5rem);
1037
+ }
1038
+ to {
1039
+ opacity: 1;
1040
+ transform: none;
1041
+ }
1042
+ }
1043
+
579
1044
  /* ============================================================
580
1045
  TABLA
581
1046
  ============================================================ */
@@ -587,6 +1052,7 @@
587
1052
  }
588
1053
 
589
1054
  .fe-table th {
1055
+ position: relative;
590
1056
  padding: var(--fe-space-2) var(--fe-space-3);
591
1057
  font-size: var(--fe-text-xs);
592
1058
  font-weight: 500;
@@ -618,18 +1084,202 @@
618
1084
  text-align: right;
619
1085
  }
620
1086
 
1087
+ /* Una columna ordenable lo dice antes de pulsarla. */
1088
+ .fe-table th[aria-sort] {
1089
+ cursor: pointer;
1090
+ user-select: none;
1091
+ }
1092
+
1093
+ .fe-table th[aria-sort='ascending'],
1094
+ .fe-table th[aria-sort='descending'] {
1095
+ color: var(--fe-text);
1096
+ }
1097
+
1098
+ /* La cabecera se queda arriba al desplazar un listado largo. */
1099
+ .fe-table-sticky thead th {
1100
+ position: sticky;
1101
+ top: 0;
1102
+ z-index: var(--fe-z-sticky);
1103
+ background: var(--fe-surface);
1104
+ }
1105
+
1106
+ /* La columna de las casillas de selección: tan estrecha como la casilla. */
1107
+ .fe-table-select {
1108
+ width: 1%;
1109
+ white-space: nowrap;
1110
+ }
1111
+
1112
+ /* El tirador para cambiar el ancho de una columna. */
1113
+ .fe-table-resizer {
1114
+ position: absolute;
1115
+ top: 0;
1116
+ right: 0;
1117
+ width: 6px;
1118
+ height: 100%;
1119
+ cursor: col-resize;
1120
+ user-select: none;
1121
+ touch-action: none;
1122
+ }
1123
+
1124
+ .fe-table-resizer:hover,
1125
+ .fe-table-resizer[data-resizing='true'] {
1126
+ background: var(--fe-focus);
1127
+ }
1128
+
1129
+ /* Una tabla sin filas dice por qué, en el sitio de las filas. */
1130
+ .fe-table-empty {
1131
+ padding: var(--fe-space-6) var(--fe-space-4);
1132
+ text-align: center;
1133
+ color: var(--fe-text-muted);
1134
+ }
1135
+
1136
+ /*
1137
+ * La barra que aparece al seleccionar filas: cuántas hay y qué se puede hacer
1138
+ * con todas a la vez. Sustituye a la barra de herramientas mientras dura la
1139
+ * selección, en el mismo sitio, para que la tabla no salte.
1140
+ */
1141
+ .fe-bulk-bar {
1142
+ display: flex;
1143
+ flex-wrap: wrap;
1144
+ align-items: center;
1145
+ gap: var(--fe-space-3);
1146
+ padding: var(--fe-space-2) var(--fe-space-4);
1147
+ color: var(--fe-text);
1148
+ background: var(--fe-primary-soft);
1149
+ border-radius: var(--fe-radius);
1150
+ }
1151
+
1152
+ .fe-bulk-count {
1153
+ font-size: var(--fe-text-sm);
1154
+ font-weight: 500;
1155
+ font-variant-numeric: tabular-nums;
1156
+ }
1157
+
1158
+ /* ============================================================
1159
+ EDICION EN LINEA
1160
+ ============================================================ */
1161
+
1162
+ /* Un valor que se edita donde está. Se distingue del texto normal solo al
1163
+ pasar o enfocar, para que una tabla editable no parezca un formulario. */
1164
+ .fe-editable {
1165
+ display: inline-flex;
1166
+ align-items: center;
1167
+ gap: var(--fe-space-1);
1168
+ min-height: 1.5em;
1169
+ padding: 0 var(--fe-space-1);
1170
+ margin: 0 calc(-1 * var(--fe-space-1));
1171
+ border-radius: var(--fe-radius-sm);
1172
+ border-bottom: 1px dashed transparent;
1173
+ cursor: text;
1174
+ }
1175
+
1176
+ .fe-editable:hover {
1177
+ border-bottom-color: var(--fe-border-strong);
1178
+ }
1179
+
1180
+ .fe-editable:focus-visible {
1181
+ outline: 2px solid var(--fe-focus);
1182
+ outline-offset: 1px;
1183
+ }
1184
+
1185
+ .fe-editable[data-saving='true'] {
1186
+ opacity: 0.6;
1187
+ cursor: progress;
1188
+ }
1189
+
1190
+ .fe-editable[data-invalid='true'] {
1191
+ border-bottom-color: var(--fe-danger);
1192
+ }
1193
+
1194
+ /* ============================================================
1195
+ SHELL DE APLICACION
1196
+ ============================================================ */
1197
+
1198
+ /*
1199
+ * La forma de una aplicación de escritorio: cabecera, barra lateral y el área
1200
+ * de trabajo, que es lo único que cambia al navegar.
1201
+ *
1202
+ * <div class="fe-shell">
1203
+ * <header class="fe-shell-header">…</header>
1204
+ * <nav class="fe-shell-sidebar">…</nav>
1205
+ * <main class="fe-shell-main">…</main>
1206
+ * </div>
1207
+ */
1208
+
1209
+ .fe-shell {
1210
+ display: grid;
1211
+ grid-template-columns: var(--fe-sidebar-width) minmax(0, 1fr);
1212
+ grid-template-rows: auto minmax(0, 1fr);
1213
+ min-height: 100dvh;
1214
+ color: var(--fe-text);
1215
+ background: var(--fe-bg);
1216
+ }
1217
+
1218
+ .fe-shell-header {
1219
+ grid-column: 1 / -1;
1220
+ display: flex;
1221
+ align-items: center;
1222
+ gap: var(--fe-space-3);
1223
+ padding: var(--fe-space-2) var(--fe-space-4);
1224
+ background: var(--fe-surface);
1225
+ border-bottom: 1px solid var(--fe-border);
1226
+ }
1227
+
1228
+ .fe-shell-sidebar {
1229
+ padding: var(--fe-space-3);
1230
+ overflow: auto;
1231
+ background: var(--fe-surface);
1232
+ border-right: 1px solid var(--fe-border);
1233
+ }
1234
+
1235
+ .fe-shell-main {
1236
+ min-width: 0;
1237
+ padding: var(--fe-space-5);
1238
+ overflow: auto;
1239
+ }
1240
+
1241
+ /* En pantallas estrechas la barra lateral sale del flujo y se abre encima. */
1242
+ @media (max-width: 959px) {
1243
+ .fe-shell {
1244
+ grid-template-columns: minmax(0, 1fr);
1245
+ }
1246
+
1247
+ .fe-shell-sidebar {
1248
+ display: none;
1249
+ }
1250
+
1251
+ .fe-shell[data-sidebar-open='true'] .fe-shell-sidebar {
1252
+ display: block;
1253
+ position: fixed;
1254
+ inset: 0 auto 0 0;
1255
+ z-index: var(--fe-z-overlay);
1256
+ width: min(var(--fe-sidebar-width), 85vw);
1257
+ box-shadow: var(--fe-shadow-lg);
1258
+ }
1259
+ }
1260
+
621
1261
  @media (prefers-reduced-motion: reduce) {
622
1262
  .fe-skeleton {
623
1263
  animation: none;
624
1264
  }
625
1265
 
1266
+ .fe-dialog[open],
1267
+ .fe-drawer[open],
1268
+ .fe-dialog::backdrop,
1269
+ .fe-drawer::backdrop,
1270
+ .fe-menu[popover]:popover-open,
1271
+ .fe-toast {
1272
+ animation: none;
1273
+ }
1274
+
626
1275
  .fe-input,
627
1276
  .fe-select,
628
1277
  .fe-textarea,
629
1278
  .fe-button,
630
1279
  .fe-button-secondary,
631
1280
  .fe-button-danger,
632
- .fe-button-link {
1281
+ .fe-button-link,
1282
+ .fe-icon-button {
633
1283
  transition: none;
634
1284
  }
635
1285
  }
package/src/icons.js CHANGED
@@ -41,8 +41,12 @@ export const defaultIcons = {
41
41
  hide: 'fa6-solid:eye-slash',
42
42
  actions: 'fa6-solid:gears',
43
43
  refresh: 'fa6-solid:rotate',
44
+ restore: 'fa6-solid:rotate-left',
44
45
  search: 'fa6-solid:magnifying-glass',
45
46
  filter: 'fa6-solid:filter',
47
+ more: 'fa6-solid:ellipsis',
48
+ external: 'fa6-solid:arrow-up-right-from-square',
49
+ logout: 'fa6-solid:right-from-bracket',
46
50
 
47
51
  // ESTADO
48
52
  help: 'fa6-solid:circle-question',
@@ -58,12 +62,35 @@ export const defaultIcons = {
58
62
  up: 'fa6-solid:chevron-up',
59
63
  down: 'fa6-solid:chevron-down',
60
64
  close: 'fa6-solid:xmark',
65
+ home: 'fa6-solid:house',
66
+ menu: 'fa6-solid:bars',
67
+ sidebar: 'fa6-solid:table-columns',
68
+ command: 'fa6-solid:terminal',
69
+ settings: 'fa6-solid:gear',
70
+
71
+ // SELECCION Y ORDEN
72
+ check: 'fa6-solid:check',
73
+ minus: 'fa6-solid:minus',
74
+ sort: 'fa6-solid:sort',
75
+ sortUp: 'fa6-solid:sort-up',
76
+ sortDown: 'fa6-solid:sort-down',
61
77
 
62
78
  // OBJETOS
63
79
  box: 'fa6-solid:box',
64
80
  users: 'fa6-solid:users',
65
81
  gift: 'fa6-solid:gift',
66
82
  file: 'fa6-solid:file',
83
+ media: 'fa6-solid:photo-film',
84
+
85
+ // EDICION
86
+ save: 'fa6-solid:floppy-disk',
87
+ copy: 'fa6-solid:clone',
88
+ drag: 'fa6-solid:grip-vertical',
89
+
90
+ // GRABACION
91
+ record: 'fa6-solid:microphone',
92
+ pause: 'fa6-solid:pause',
93
+ play: 'fa6-solid:play',
67
94
  }
68
95
 
69
96
  /** @type {IconMap} */
package/src/theme.js CHANGED
@@ -52,6 +52,7 @@ export const defaultTheme = {
52
52
  buttonSecondary: 'fe-button-secondary',
53
53
  buttonDanger: 'fe-button-danger',
54
54
  buttonLink: 'fe-button-link',
55
+ iconButton: 'fe-icon-button',
55
56
 
56
57
  // NAVEGACION Y ACCIONES
57
58
  breadcrumb: 'fe-breadcrumb',
@@ -61,25 +62,81 @@ export const defaultTheme = {
61
62
  actionMenu: 'fe-action-menu',
62
63
  actionMenuItem: 'fe-action-item',
63
64
  actionMenuDanger: 'fe-action-danger',
65
+ kbd: 'fe-kbd',
64
66
 
65
67
  // SUPERFICIES Y PIEZAS DE APLICACION
66
68
  surface: 'fe-surface',
67
69
  surfaceRaised: 'fe-surface-raised',
68
70
  toolbar: 'fe-toolbar',
69
71
  badge: 'fe-badge',
72
+ badgePrimary: 'fe-badge-primary',
73
+ badgeSuccess: 'fe-badge-success',
74
+ badgeDanger: 'fe-badge-danger',
75
+ badgeWarning: 'fe-badge-warning',
70
76
  skeleton: 'fe-skeleton',
77
+ skeletonText: 'fe-skeleton-text',
78
+ skeletonCircle: 'fe-skeleton-circle',
79
+ skeletonBlock: 'fe-skeleton-block',
80
+
81
+ // DIALOGOS Y DRAWERS, sobre <dialog> y showModal()
71
82
  overlay: 'fe-overlay',
72
83
  dialog: 'fe-dialog',
84
+ dialogSmall: 'fe-dialog-sm',
85
+ dialogLarge: 'fe-dialog-lg',
86
+ dialogHeader: 'fe-dialog-header',
87
+ dialogTitle: 'fe-dialog-title',
88
+ dialogBody: 'fe-dialog-body',
89
+ dialogFooter: 'fe-dialog-footer',
73
90
  drawer: 'fe-drawer',
91
+ drawerStart: 'fe-drawer-start',
92
+ drawerHeader: 'fe-drawer-header',
93
+ drawerTitle: 'fe-drawer-title',
94
+ drawerBody: 'fe-drawer-body',
95
+ drawerFooter: 'fe-drawer-footer',
96
+
97
+ // MENUS, sobre el atributo popover
74
98
  menu: 'fe-menu',
99
+ menuList: 'fe-menu-list',
75
100
  menuItem: 'fe-menu-item',
101
+ menuItemDanger: 'fe-menu-item-danger',
102
+ menuSeparator: 'fe-menu-separator',
103
+ menuLabel: 'fe-menu-label',
104
+
105
+ // PALETA DE COMANDOS
106
+ command: 'fe-command',
107
+ commandInput: 'fe-command-input',
108
+ commandList: 'fe-command-list',
109
+ commandGroup: 'fe-command-group',
110
+ commandItem: 'fe-command-item',
111
+ commandEmpty: 'fe-command-empty',
112
+
113
+ // AVISOS
76
114
  toast: 'fe-toast',
77
115
  toastDanger: 'fe-toast-danger',
78
116
  toastSuccess: 'fe-toast-success',
117
+ toastWarning: 'fe-toast-warning',
118
+ toastTitle: 'fe-toast-title',
119
+ toastClose: 'fe-toast-close',
120
+ toastRegion: 'fe-toast-region',
79
121
 
80
122
  // TABLA
81
123
  table: 'fe-table',
82
124
  tableNumeric: 'fe-numeric',
125
+ tableSticky: 'fe-table-sticky',
126
+ tableSelect: 'fe-table-select',
127
+ tableResizer: 'fe-table-resizer',
128
+ tableEmpty: 'fe-table-empty',
129
+ bulkBar: 'fe-bulk-bar',
130
+ bulkCount: 'fe-bulk-count',
131
+
132
+ // EDICION EN LINEA
133
+ editable: 'fe-editable',
134
+
135
+ // SHELL DE APLICACION
136
+ shell: 'fe-shell',
137
+ shellHeader: 'fe-shell-header',
138
+ shellSidebar: 'fe-shell-sidebar',
139
+ shellMain: 'fe-shell-main',
83
140
  }
84
141
 
85
142
  /** @type {Theme} */
package/src/tokens.css CHANGED
@@ -51,8 +51,15 @@
51
51
  --fe-warning: #8a6212;
52
52
  --fe-warning-soft: #f8f0dd;
53
53
 
54
+ --fe-info: #1f6fb2;
55
+ --fe-info-soft: #e5f0f9;
56
+
54
57
  --fe-focus: #4b5bd7;
55
58
 
59
+ /* Lo que queda detrás de un diálogo o un drawer abierto. Era un rgba
60
+ escrito dentro de .fe-overlay, igual en claro que en oscuro. */
61
+ --fe-overlay: rgba(16, 18, 27, 0.45);
62
+
56
63
  /* ---------- FORMA ---------- */
57
64
 
58
65
  --fe-radius-sm: 4px;
@@ -97,6 +104,30 @@
97
104
  /* ---------- MOVIMIENTO ---------- */
98
105
 
99
106
  --fe-transition: 120ms ease;
107
+ --fe-duration-fast: 120ms;
108
+ --fe-duration: 200ms;
109
+ /* Rápido al empezar y suave al llegar: lo que se abre tiene que sentirse
110
+ inmediato, no animado. */
111
+ --fe-ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
112
+
113
+ /* ---------- CAPAS ---------- */
114
+
115
+ /* Un orden y no un número por componente. Lo que se abre con showModal() o
116
+ con el atributo popover ya vive en la capa superior del navegador y no
117
+ necesita z-index; esto es para lo que no: la cabecera fija de una tabla,
118
+ un overlay dibujado a mano, los tooltips. */
119
+ --fe-z-sticky: 10;
120
+ --fe-z-dropdown: 40;
121
+ --fe-z-overlay: 50;
122
+ --fe-z-tooltip: 60;
123
+ --fe-z-toast: 70;
124
+
125
+ /* ---------- MEDIDAS DE APLICACION ---------- */
126
+
127
+ --fe-dialog-width: 32rem;
128
+ --fe-drawer-width: 32rem;
129
+ --fe-command-width: 40rem;
130
+ --fe-sidebar-width: 15rem;
100
131
  }
101
132
 
102
133
  /* La elección explícita de claro gana sobre un sistema en oscuro, de ahí el
@@ -133,8 +164,13 @@
133
164
  --fe-warning: #d9ac53;
134
165
  --fe-warning-soft: #2a2417;
135
166
 
167
+ --fe-info: #7cb3e6;
168
+ --fe-info-soft: #16222e;
169
+
136
170
  --fe-focus: #8b95f0;
137
171
 
172
+ --fe-overlay: rgba(0, 0, 0, 0.6);
173
+
138
174
  --fe-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
139
175
  --fe-shadow: 0 4px 12px rgba(0, 0, 0, 0.45);
140
176
  --fe-shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55);
@@ -173,8 +209,13 @@
173
209
  --fe-warning: #d9ac53;
174
210
  --fe-warning-soft: #2a2417;
175
211
 
212
+ --fe-info: #7cb3e6;
213
+ --fe-info-soft: #16222e;
214
+
176
215
  --fe-focus: #8b95f0;
177
216
 
217
+ --fe-overlay: rgba(0, 0, 0, 0.6);
218
+
178
219
  --fe-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
179
220
  --fe-shadow: 0 4px 12px rgba(0, 0, 0, 0.45);
180
221
  --fe-shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55);