innoboxrr-form-core 2.3.0 → 2.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.
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Avisos y confirmaciones de toda la aplicación, como estado de módulo.
3
+ *
4
+ * Hasta ahora no había forma de avisar: un formulario generado se tragaba
5
+ * cualquier error que no fuera de validación, la tabla solo lo escribía en la
6
+ * consola y la confirmación de borrar era un SweetAlert de otro paquete. Cada
7
+ * pieza que quisiera decir algo tendría que haber montado su propio aviso.
8
+ *
9
+ * Vive aquí, fuera de Vue y de React, por la misma razón que el tema: los dos
10
+ * paquetes de componentes pintan la misma cola, y quien avisa —un store, un
11
+ * contrato de modelo, la tabla— no tiene por qué saber qué framework hay
12
+ * detrás.
13
+ *
14
+ * import { notify, confirmAction } from 'innoboxrr-form-core'
15
+ *
16
+ * notify({ message: 'Producto creado', variant: 'success' })
17
+ *
18
+ * if (await confirmAction({ message: '¿Borrar el producto?', variant: 'danger' })) {
19
+ * // …
20
+ * }
21
+ */
22
+
23
+ /**
24
+ * @typedef {'info'|'success'|'warning'|'danger'} Variant
25
+ * @typedef {{ id: number, title: string|null, message: string, variant: Variant, duration: number }} Toast
26
+ * @typedef {{ id: number, title: string, message: string, confirmLabel: string, cancelLabel: string, variant: 'primary'|'danger' }} Confirmation
27
+ */
28
+
29
+ const VARIANTS = ['info', 'success', 'warning', 'danger']
30
+
31
+ const DURATION = 5000
32
+
33
+ /**
34
+ * Más avisos a la vez no se leen: tapan la pantalla. El que sobra es el más
35
+ * antiguo.
36
+ */
37
+ const MAX_TOASTS = 5
38
+
39
+ // AVISOS
40
+
41
+ /** @type {Toast[]} */
42
+ let toasts = []
43
+
44
+ let nextToast = 1
45
+
46
+ /** @type {Map<number, ReturnType<typeof setTimeout>>} */
47
+ const timers = new Map()
48
+
49
+ /** @type {Set<(toasts: Toast[]) => void>} */
50
+ const toastListeners = new Set()
51
+
52
+ const emitToasts = () => toastListeners.forEach((listener) => listener(toasts))
53
+
54
+ /**
55
+ * Muestra un aviso y devuelve su id.
56
+ *
57
+ * Un aviso de peligro no se cierra solo por defecto: quien no lo leyó a tiempo
58
+ * no sabría qué falló. El resto se va a los cinco segundos. `duration: 0` lo
59
+ * deja hasta que se cierre a mano.
60
+ *
61
+ * @param {string|{ message: string, title?: string, variant?: Variant, duration?: number }} options
62
+ * @returns {number}
63
+ */
64
+ export function notify(options) {
65
+ const input = typeof options === 'string' ? { message: options } : (options ?? {})
66
+ const variant = VARIANTS.includes(input.variant) ? input.variant : 'info'
67
+ const duration = input.duration ?? (variant === 'danger' ? 0 : DURATION)
68
+
69
+ /** @type {Toast} */
70
+ const toast = {
71
+ id: nextToast++,
72
+ title: input.title ?? null,
73
+ message: String(input.message ?? ''),
74
+ variant,
75
+ duration,
76
+ }
77
+
78
+ // Se reemplaza el array en vez de mutarlo: React compara la instantánea
79
+ // por referencia y, con el mismo array, no repintaría.
80
+ toasts = [...toasts, toast]
81
+
82
+ while (toasts.length > MAX_TOASTS) {
83
+ forget(toasts[0].id)
84
+ toasts = toasts.slice(1)
85
+ }
86
+
87
+ if (duration > 0) {
88
+ timers.set(toast.id, setTimeout(() => dismiss(toast.id), duration))
89
+ }
90
+
91
+ emitToasts()
92
+
93
+ return toast.id
94
+ }
95
+
96
+ /**
97
+ * @param {string} message
98
+ * @param {{ title?: string, duration?: number }} [options]
99
+ */
100
+ export function notifySuccess(message, options = {}) {
101
+ return notify({ ...options, message, variant: 'success' })
102
+ }
103
+
104
+ /**
105
+ * @param {string} message
106
+ * @param {{ title?: string, duration?: number }} [options]
107
+ */
108
+ export function notifyError(message, options = {}) {
109
+ return notify({ ...options, message, variant: 'danger' })
110
+ }
111
+
112
+ /**
113
+ * @param {number} id
114
+ */
115
+ export function dismiss(id) {
116
+ forget(id)
117
+
118
+ const remaining = toasts.filter((toast) => toast.id !== id)
119
+
120
+ if (remaining.length !== toasts.length) {
121
+ toasts = remaining
122
+ emitToasts()
123
+ }
124
+ }
125
+
126
+ /** @returns {Toast[]} */
127
+ export function getToasts() {
128
+ return toasts
129
+ }
130
+
131
+ /**
132
+ * @param {(toasts: Toast[]) => void} listener
133
+ * @returns {() => void} para dejar de escuchar
134
+ */
135
+ export function onToastsChange(listener) {
136
+ toastListeners.add(listener)
137
+
138
+ return () => toastListeners.delete(listener)
139
+ }
140
+
141
+ /**
142
+ * Vacía la cola. Sobre todo para las pruebas: sin esto un aviso de una prueba
143
+ * aparecería en la siguiente.
144
+ */
145
+ export function resetToasts() {
146
+ timers.forEach((timer) => clearTimeout(timer))
147
+ timers.clear()
148
+
149
+ toasts = []
150
+ nextToast = 1
151
+
152
+ emitToasts()
153
+ }
154
+
155
+ /**
156
+ * @param {number} id
157
+ */
158
+ function forget(id) {
159
+ clearTimeout(timers.get(id))
160
+ timers.delete(id)
161
+ }
162
+
163
+ // CONFIRMACIONES
164
+
165
+ /** @type {Confirmation|null} */
166
+ let confirmation = null
167
+
168
+ /** @type {((value: boolean) => void)|null} */
169
+ let resolver = null
170
+
171
+ let nextConfirmation = 1
172
+
173
+ /** @type {Set<(confirmation: Confirmation|null) => void>} */
174
+ const confirmationListeners = new Set()
175
+
176
+ const emitConfirmation = () => confirmationListeners.forEach((listener) => listener(confirmation))
177
+
178
+ /**
179
+ * Pregunta y espera la respuesta: `true` si se confirma, `false` si no.
180
+ *
181
+ * La pinta el ConfirmHostComponent que la aplicación monta una vez. Si no hay
182
+ * ninguno escuchando se usa `window.confirm`, que es feo pero contesta: una
183
+ * promesa que no se resolviera nunca dejaría colgada la acción que la espera.
184
+ *
185
+ * Una pregunta nueva con otra pendiente da la anterior por cancelada. Dos
186
+ * confirmaciones a la vez no tienen una respuesta clara.
187
+ *
188
+ * @param {string|{ message: string, title?: string, confirmLabel?: string, cancelLabel?: string, variant?: 'primary'|'danger' }} options
189
+ * @returns {Promise<boolean>}
190
+ */
191
+ export function confirmAction(options) {
192
+ const input = typeof options === 'string' ? { message: options } : (options ?? {})
193
+ const message = String(input.message ?? '')
194
+
195
+ if (confirmationListeners.size === 0) {
196
+ const native = typeof window !== 'undefined' ? window.confirm : undefined
197
+
198
+ return Promise.resolve(typeof native === 'function' ? Boolean(native.call(window, message)) : false)
199
+ }
200
+
201
+ if (resolver) {
202
+ settle(false)
203
+ }
204
+
205
+ return new Promise((resolve) => {
206
+ resolver = resolve
207
+
208
+ confirmation = {
209
+ id: nextConfirmation++,
210
+ title: input.title ?? '¿Confirmas?',
211
+ message,
212
+ confirmLabel: input.confirmLabel ?? 'Confirmar',
213
+ cancelLabel: input.cancelLabel ?? 'Cancelar',
214
+ variant: input.variant === 'danger' ? 'danger' : 'primary',
215
+ }
216
+
217
+ emitConfirmation()
218
+ })
219
+ }
220
+
221
+ /**
222
+ * La respuesta a la confirmación pendiente. La llama el componente que la pinta.
223
+ *
224
+ * @param {boolean} value
225
+ */
226
+ export function resolveConfirmation(value) {
227
+ if (resolver) {
228
+ settle(Boolean(value))
229
+ }
230
+ }
231
+
232
+ /** @returns {Confirmation|null} */
233
+ export function getConfirmation() {
234
+ return confirmation
235
+ }
236
+
237
+ /**
238
+ * @param {(confirmation: Confirmation|null) => void} listener
239
+ * @returns {() => void} para dejar de escuchar
240
+ */
241
+ export function onConfirmationChange(listener) {
242
+ confirmationListeners.add(listener)
243
+
244
+ return () => confirmationListeners.delete(listener)
245
+ }
246
+
247
+ /**
248
+ * Cancela lo pendiente. Sobre todo para las pruebas.
249
+ */
250
+ export function resetConfirmation() {
251
+ if (resolver) {
252
+ settle(false)
253
+
254
+ return
255
+ }
256
+
257
+ confirmation = null
258
+ emitConfirmation()
259
+ }
260
+
261
+ /**
262
+ * @param {boolean} value
263
+ */
264
+ function settle(value) {
265
+ const resolve = resolver
266
+
267
+ resolver = null
268
+ confirmation = null
269
+
270
+ emitConfirmation()
271
+
272
+ resolve?.(value)
273
+ }
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,6 +62,18 @@ 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',
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);