@sfperusacdev/sf-ui 0.3.1 → 0.3.3

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.
@@ -195,6 +195,14 @@
195
195
  --sf-success: #16a34a;
196
196
  --sf-warning: #d97706;
197
197
 
198
+ /* Título e icono de la cabecera de DetailSection con tono. Los tonos de base dan de 2,5:1 a 3,8:1 sobre la tarjeta
199
+ * con el fondo tenue del tono; estos, de 4,9:1 a 7,2:1. No se aclaran --sf-info y compañía: también son fondo de
200
+ * botones y barras con texto blanco. */
201
+ --sf-detail-section-info-text: #60a5fa;
202
+ --sf-detail-section-danger-text: #f87171;
203
+ --sf-detail-section-warning-text: #fbbf24;
204
+ --sf-detail-section-success-text: #4ade80;
205
+
198
206
  /* Paleta de categorías sobre fondo oscuro: los mismos 10 tonos de sf-tokens.css, aclarados para dar al menos
199
207
  * 3:1 sobre la tarjeta (#1e293b) y sobre la superficie alterna (#0f172a). */
200
208
  --sf-cat-1: #7aa2f0;
@@ -82,7 +82,6 @@
82
82
 
83
83
  :root[data-density='compacta'] .sf-metric-row--tile .sf-metric-row__label {
84
84
  font-size: 0.8125rem;
85
- white-space: nowrap;
86
85
  }
87
86
 
88
87
  :root[data-density='compacta'] .sf-metric-row--tile .sf-metric-row__description {
@@ -155,6 +155,17 @@
155
155
  color: color-mix(in srgb, var(--sf-donut-label-color) 48%, #0b1f29);
156
156
  }
157
157
 
158
+ /* Íconos de baldosa (procesados, en espera, validadas…): el centrado se oscurece como el texto; en el «leading», el
159
+ círculo de color lleva el ícono en blanco, así que es el círculo el que se oscurece. */
160
+ :root[data-skin]:not([data-theme='dark']) .sf-metric-tile--centered .sf-metric-tile__icon {
161
+ color: color-mix(in srgb, var(--sf-metric-tile-color) 50%, #0b1f29);
162
+ }
163
+
164
+ :root[data-skin]:not([data-theme='dark']) .sf-metric-tile--leading .sf-metric-tile__icon {
165
+ background: color-mix(in srgb, var(--sf-metric-tile-color) 55%, #0b1f29);
166
+ color: #ffffff;
167
+ }
168
+
158
169
  :root[data-skin]:not([data-theme='dark']) .sf-metric-row__tile-icon {
159
170
  border-color: color-mix(in srgb, var(--sf-metric-color, var(--sf-accent)) 60%, #0b1f29);
160
171
  color: color-mix(in srgb, var(--sf-metric-color, var(--sf-accent)) 48%, #0b1f29);
@@ -192,6 +203,111 @@
192
203
  color: var(--sf-accent-text);
193
204
  }
194
205
 
206
+ /* Página activa del paginador en claro: en classic, soft y neon el número va en blanco sobre el acento, y el de ERP
207
+ (#ec7211) o Neón (#0d9488) no llega a 4,5:1. premium y matrix tienen sus propios colores y no se tocan. */
208
+ :root[data-skin='erp']:not([data-theme='dark']) .sf-pagination--classic .sf-pagination__page--active,
209
+ :root[data-skin='erp']:not([data-theme='dark']) .sf-pagination--classic .sf-pagination__page--active:hover,
210
+ :root[data-skin='erp']:not([data-theme='dark']) .sf-pagination--soft .sf-pagination__page--active,
211
+ :root[data-skin='erp']:not([data-theme='dark']) .sf-pagination--soft .sf-pagination__page--active:hover,
212
+ :root[data-skin='erp']:not([data-theme='dark']) .sf-pagination--neon .sf-pagination__page--active,
213
+ :root[data-skin='erp']:not([data-theme='dark']) .sf-pagination--neon .sf-pagination__page--active:hover {
214
+ background: #b35400;
215
+ border-color: #b35400;
216
+ }
217
+
218
+ :root[data-skin='neon']:not([data-theme='dark']) .sf-pagination--classic .sf-pagination__page--active,
219
+ :root[data-skin='neon']:not([data-theme='dark']) .sf-pagination--classic .sf-pagination__page--active:hover,
220
+ :root[data-skin='neon']:not([data-theme='dark']) .sf-pagination--soft .sf-pagination__page--active,
221
+ :root[data-skin='neon']:not([data-theme='dark']) .sf-pagination--soft .sf-pagination__page--active:hover,
222
+ :root[data-skin='neon']:not([data-theme='dark']) .sf-pagination--neon .sf-pagination__page--active,
223
+ :root[data-skin='neon']:not([data-theme='dark']) .sf-pagination--neon .sf-pagination__page--active:hover {
224
+ background: #0f766e;
225
+ border-color: #0f766e;
226
+ }
227
+
228
+ /* —— Detalles y capas (medido en el prototipo ADN con capas-temas, 6 temas) ——
229
+ Botones de estado: el texto no sale de --sf-btn-primary-text, que en los oscuros es casi negro. */
230
+ :root[data-skin] .sf-btn--success {
231
+ background: #15803d;
232
+ color: #ffffff;
233
+ }
234
+
235
+ :root[data-skin] .sf-btn--success:hover:not(:disabled) {
236
+ background: #166534;
237
+ }
238
+
239
+ :root[data-skin] .sf-btn--danger-solid {
240
+ background: #b91c1c;
241
+ color: #ffffff;
242
+ }
243
+
244
+ :root[data-skin] .sf-btn--danger-solid:hover:not(:disabled) {
245
+ background: #991b1b;
246
+ }
247
+
248
+ /* Claro: rojo, acentos de aviso, asterisco y cierre de los avisos a 4,5:1 sobre blanco o su tinte. */
249
+ :root[data-skin]:not([data-theme='dark']) {
250
+ --sf-alert-info-accent: #1d4ed8;
251
+ --sf-alert-success-accent: #15803d;
252
+ --sf-alert-warning-accent: #b45309;
253
+ --sf-alert-danger-accent: #b91c1c;
254
+ }
255
+
256
+ :root[data-skin]:not([data-theme='dark']) .sf-btn--danger {
257
+ background: #ffffff;
258
+ color: #b91c1c;
259
+ }
260
+
261
+ :root[data-skin]:not([data-theme='dark']) .sf-field-required {
262
+ color: #b91c1c;
263
+ }
264
+
265
+ :root[data-skin]:not([data-theme='dark']) .sf-table thead th:not([class*='sf-table__th--']) {
266
+ color: #475569;
267
+ }
268
+
269
+ :root[data-skin]:not([data-theme='dark']) .sf-toast__close {
270
+ color: #475569;
271
+ }
272
+
273
+ :root[data-skin]:not([data-theme='dark']) .sf-toast--warning .sf-toast__icon {
274
+ color: #b45309;
275
+ }
276
+
277
+ /* El encabezado sin variante no tiene fondo propio: el subtítulo va sobre el blanco del modal, no sobre el marino. */
278
+ :root[data-skin]:not([data-theme='dark']) .sf-modal-header:not(.sf-modal-header--dark):not(.sf-modal-header--brand) .sf-modal-header__subtitle {
279
+ color: var(--sf-text-muted);
280
+ }
281
+
282
+ /* Oscuro: los mismos elementos con los tonos claros de su color, sobre el fondo pizarra o negro. */
283
+ :root[data-skin][data-theme='dark'] {
284
+ --sf-alert-info-accent: #60a5fa;
285
+ --sf-alert-success-accent: #4ade80;
286
+ --sf-alert-warning-accent: #fbbf24;
287
+ --sf-alert-danger-accent: #f87171;
288
+ }
289
+
290
+ :root[data-skin][data-theme='dark'] .sf-btn--danger {
291
+ background: transparent;
292
+ color: #f87171;
293
+ }
294
+
295
+ :root[data-skin][data-theme='dark'] .sf-field-required {
296
+ color: #f87171;
297
+ }
298
+
299
+ :root[data-skin][data-theme='dark'] .sf-table thead th:not([class*='sf-table__th--']) {
300
+ color: var(--sf-text-muted);
301
+ }
302
+
303
+ :root[data-skin][data-theme='dark'] .sf-badge--info:not(.sf-badge--solid) {
304
+ color: #93c5fd;
305
+ }
306
+
307
+ :root[data-skin][data-theme='dark'] .sf-toast__close {
308
+ color: var(--sf-text-muted);
309
+ }
310
+
195
311
  /* Modo oscuro con cualquier tema: el texto pintado con un color de categoría se aclara para leerse sobre el fondo
196
312
  pizarra o negro; el amarillo de advertencia suave y los rellenos de barra también suben de luz. */
197
313
  :root[data-skin][data-theme='dark'] .sf-relation-graph--orbit .sf-relation-graph__relation {
@@ -0,0 +1,36 @@
1
+ import { ReactNode } from 'react';
2
+ export type InPageDetailState<T> = {
3
+ origin: string;
4
+ value: T;
5
+ };
6
+ export type InPageDetailController<T> = {
7
+ current: InPageDetailState<T> | null;
8
+ /** Abre el detalle de `value`, originado en `origin` (la tarjeta o cifra). Si el abierto tiene cambios, pregunta. */
9
+ open: (origin: string, value: T) => void;
10
+ /** Cierra y devuelve el foco al control que abrió el detalle (o a su tarjeta). Si hay cambios, pregunta. */
11
+ close: () => void;
12
+ /** Cambia el detalle sin preguntar: para pasar a la ficha de lo que se acaba de guardar. */
13
+ replace: (origin: string, value: T) => void;
14
+ /** El valor abierto si su origen es `origin`; si no, `null`. */
15
+ valueFor: (origin: string) => T | null;
16
+ };
17
+ /**
18
+ * Estado de un detalle que se abre en la página, bajo la tarjeta que lo originó (regla de ventanas §6). Abrir otro o
19
+ * cerrar pasa por `useProtectedExit` con el ámbito `scope`; la captura del detalle se registra en ese mismo ámbito con
20
+ * `useUnsavedChanges`.
21
+ */
22
+ export declare const useInPageDetail: <T>(scope?: string) => InPageDetailController<T>;
23
+ export type InPageDetailZoneProps = {
24
+ /** Cambia cuando cambia lo que se muestra: la zona vuelve a recibir el foco. */
25
+ zoneKey: string;
26
+ /** Nombre accesible de la región («Evento Lote 12»). */
27
+ label: string;
28
+ onClose: () => void;
29
+ children: ReactNode;
30
+ className?: string;
31
+ };
32
+ /**
33
+ * Contenedor del detalle: se desplaza a la vista, recibe el foco con su nombre y Escape lo cierra. Si el control
34
+ * enfocado dentro desaparece o se deshabilita (Reprocesar, Validar), el foco vuelve a la zona en vez de caer a <body>.
35
+ */
36
+ export declare const InPageDetailZone: ({ zoneKey, label, onClose, children, className }: InPageDetailZoneProps) => import('react').JSX.Element;
@@ -44,3 +44,5 @@ export type { WeekdayToggleStripProps, WeekdayOption } from './WeekdayToggleStri
44
44
  export type { UnitSelectorOption, UnitSelectorProps } from './UnitSelector';
45
45
  export { AppShell, AppBarContainer } from './AppShell';
46
46
  export type { AppShellCategory, AppShellNavItem, AppShellProps, AppShellBranchSelector, AppShellExitToModulesAction, AppShellHelpAction, AppShellLayout, AppShellModuleLauncher, AppShellModuleLauncherItem, AppShellThemeToggle, AppShellTopBarContext, AppShellUserMenu, AppBarContainerProps, } from './AppShell';
47
+ export { InPageDetailZone, useInPageDetail } from './InPageDetail';
48
+ export type { InPageDetailController, InPageDetailState, InPageDetailZoneProps } from './InPageDetail';
@@ -0,0 +1,35 @@
1
+ import { ReactNode } from 'react';
2
+ export type UnsavedChangesEntry = {
3
+ /** Qué se pierde, en palabras del usuario: «la edición del conductor». */
4
+ description: string;
5
+ /** Deja la captura como estaba antes de editar. */
6
+ discard: () => void;
7
+ };
8
+ export type ProtectedExit = {
9
+ /**
10
+ * Ejecuta `action`, o pregunta antes si el ámbito (`null`: cualquiera) tiene cambios sin guardar. «Descartar
11
+ * cambios» descarta esos ámbitos y ejecuta `action`; «Seguir editando», Escape o el fondo llaman a `onCancel` y dejan
12
+ * la captura como está. Mientras la pregunta está abierta, otra petición se rechaza (su `onCancel`).
13
+ */
14
+ protect: (scope: string | null, action: () => void, onCancel?: () => void) => void;
15
+ /** Hay cambios sin guardar en el ámbito (`null` o sin argumento: en cualquiera). */
16
+ hasChanges: (scope?: string | null) => boolean;
17
+ };
18
+ export type UnsavedChangesProviderProps = {
19
+ children: ReactNode;
20
+ };
21
+ /**
22
+ * Un solo lugar que sabe qué capturas tienen cambios sin guardar y una sola pregunta para salir de ellas. Las
23
+ * pantallas registran su captura con `useUnsavedChanges`; los cierres, cambios de registro y el adaptador del
24
+ * enrutador de la app piden salir con `useProtectedExit().protect`. Mientras haya cambios, recargar o cerrar la
25
+ * pestaña muestra el aviso nativo del navegador.
26
+ */
27
+ export declare const UnsavedChangesProvider: ({ children }: UnsavedChangesProviderProps) => import('react').JSX.Element;
28
+ /**
29
+ * Registra una captura mientras `dirty` sea verdadero; al guardar, descartar o desmontar se borra sola. Un ámbito por
30
+ * captura visible (la ficha, una edición en línea, un paso): dos capturas abiertas son dos ámbitos.
31
+ */
32
+ export declare const useUnsavedChanges: (scope: string, { dirty, description, discard }: UnsavedChangesEntry & {
33
+ dirty: boolean;
34
+ }) => void;
35
+ export declare const useProtectedExit: () => ProtectedExit;
@@ -25,3 +25,5 @@ export { CommandPalette } from './CommandPalette';
25
25
  export type { CommandPaletteItem, CommandPaletteProps } from './CommandPalette';
26
26
  export { ProductTour } from './ProductTour';
27
27
  export type { ProductTourProps, ProductTourStep } from './ProductTour';
28
+ export { UnsavedChangesProvider, useProtectedExit, useUnsavedChanges } from './UnsavedChanges';
29
+ export type { ProtectedExit, UnsavedChangesEntry, UnsavedChangesProviderProps } from './UnsavedChanges';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,11 @@
1
1
  import { ExportTableColumn, ExportTableCsvOptions } from './exportTableCsv';
2
- export type ExportTableXlsxOptions<T> = ExportTableCsvOptions<T>;
2
+ export type ExportTableXlsxOptions<T> = ExportTableCsvOptions<T> & {
3
+ /**
4
+ * Líneas que van antes del encabezado, una por fila (alcance, filtros, fecha). Sin `notes` el archivo no cambia.
5
+ * Sirve para que el archivo diga qué conjunto exporta cuando la pantalla muestra solo una parte.
6
+ */
7
+ notes?: string[];
8
+ };
3
9
  /** Exporta filas a Excel (.xls SpreadsheetML) sin peer externo. */
4
10
  export declare const exportTableXlsx: <T>(rows: T[], options: ExportTableXlsxOptions<T>) => void;
5
11
  export type { ExportTableColumn };
@@ -1,4 +1,5 @@
1
1
  export { toStringArray } from './toStringArray';
2
+ export { restoreFocus } from './restoreFocus';
2
3
  export { readQueryValue, updateQueryValue, removeQueryValue } from './queryParams';
3
4
  export { exportTableCsv } from './exportTableCsv';
4
5
  export type { ExportTableColumn, ExportTableCsvOptions } from './exportTableCsv';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Devuelve el foco al cerrar un detalle o un panel: al control que lo abrió si sigue usable; si no, a otro control de
3
+ * su tarjeta (`context`), al encabezado de esa tarjeta y, por último, al `h1` de la página. Nunca mueve el foco a
4
+ * `<body>`; si no encuentra destino, no lo mueve (y si ya estaba en `<body>`, ahí sigue). El encabezado que recibe el
5
+ * foco queda con `tabindex="-1"`. Ignora lo que esté dentro de una zona de detalle, que se cierra.
6
+ * Devuelve el elemento enfocado, o `null`.
7
+ */
8
+ export declare const restoreFocus: (trigger: HTMLElement | null, context?: HTMLElement | null) => HTMLElement | null;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sfperusacdev/sf-ui",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "SF UI — librería de componentes React (inputs, tablas, hooks) para aplicaciones SF Perú.",
5
5
  "type": "module",
6
6
  "main": "./dist/sf-ui.umd.cjs",