@edc-motor/ui 0.5.20 → 0.5.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edc-motor/ui",
3
- "version": "0.5.20",
3
+ "version": "0.5.22",
4
4
  "description": "EdC Motor — componentes públicos Vue 3 + tokens SCSS para webs de juegos de mesa (paquete fuente: lo compila el consumidor con Vite)",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -62,6 +62,8 @@ export {
62
62
  type SplashRouterLike,
63
63
  } from './lib/splash'
64
64
  export { readCache, writeCache } from './lib/localCache'
65
+ export { createSwrGet, type SwrGetter } from './lib/swr'
66
+ export { isStandalonePwa } from './lib/standalone'
65
67
  export { useToast, type Toast } from './composables/useToast'
66
68
  export { useConfirm, type ConfirmOptions } from './composables/useConfirm'
67
69
  export { useTheme, type ThemeMode } from './composables/useTheme'
package/src/lib/splash.ts CHANGED
@@ -33,6 +33,16 @@ import type { AxiosInstance } from 'axios'
33
33
  const SPLASH_ID = 'edc-splash'
34
34
  const DONE_CLASS = 'edc-splash--done'
35
35
 
36
+ // Peticiones DE FONDO: con `edcBackground: true` en el config de axios, la
37
+ // petición no cuenta para el velo — es relleno o refresco (catálogos con su
38
+ // propia presentación de carga, revalidaciones SWR, sondeos), no «la página
39
+ // aún no puede pintarse». Así el velo queda solo para las cargas de página.
40
+ declare module 'axios' {
41
+ interface AxiosRequestConfig {
42
+ edcBackground?: boolean
43
+ }
44
+ }
45
+
36
46
  export interface WatchSplashOptions {
37
47
  /** Cliente(s) axios cuyas peticiones marcan el arranque (createApi). */
38
48
  api?: AxiosInstance | AxiosInstance[]
@@ -114,17 +124,19 @@ export function watchSplash(options: WatchSplashOptions = {}): void {
114
124
 
115
125
  for (const api of apis) {
116
126
  api.interceptors.request.use((config) => {
117
- inflight++
118
- clearTimeout(quietTimer)
127
+ if (!config.edcBackground) {
128
+ inflight++
129
+ clearTimeout(quietTimer)
130
+ }
119
131
  return config
120
132
  })
121
133
  api.interceptors.response.use(
122
134
  (response) => {
123
- if (--inflight <= 0) armQuiet()
135
+ if (!response.config.edcBackground && --inflight <= 0) armQuiet()
124
136
  return response
125
137
  },
126
- (error) => {
127
- if (--inflight <= 0) armQuiet()
138
+ (error: { config?: { edcBackground?: boolean } }) => {
139
+ if (!error?.config?.edcBackground && --inflight <= 0) armQuiet()
128
140
  return Promise.reject(error)
129
141
  },
130
142
  )
@@ -150,7 +162,9 @@ export function setupNavigationSplash(
150
162
  if (typeof document === 'undefined') return
151
163
  if (!splashEl()) return
152
164
 
153
- const showDelayMs = options.showDelayMs ?? 120
165
+ // 250 ms: una API razonable responde antes y el velo ni aparece; por
166
+ // debajo saltaba en CADA navegación de producción (feo y alarmante).
167
+ const showDelayMs = options.showDelayMs ?? 250
154
168
  const maxWaitMs = options.maxWaitMs ?? 8000
155
169
 
156
170
  let navPending = false
@@ -0,0 +1,18 @@
1
+ // ¿Corre la web como PWA INSTALADA (ventana propia, sin pestañas ni barra)?
2
+ // Importa para los PDFs: en la app instalada no hay «pestaña nueva» ni visor
3
+ // con controles — navegar a un PDF inline deja la ventana en blanco sin
4
+ // escape mientras bajan decenas de MB. En standalone se sirve la descarga
5
+ // nativa (Content-Disposition: attachment): el gestor de descargas del
6
+ // sistema da progreso y la app sigue viva.
7
+ export function isStandalonePwa(): boolean {
8
+ if (typeof window === 'undefined') return false
9
+ // display-mode cubre Chrome/Edge/Android; navigator.standalone es el
10
+ // equivalente histórico de iOS Safari.
11
+ return (
12
+ window.matchMedia('(display-mode: standalone)').matches ||
13
+ window.matchMedia('(display-mode: minimal-ui)').matches ||
14
+ window.matchMedia('(display-mode: fullscreen)').matches ||
15
+ ('standalone' in window.navigator &&
16
+ Boolean((window.navigator as { standalone?: boolean }).standalone))
17
+ )
18
+ }
package/src/lib/swr.ts ADDED
@@ -0,0 +1,73 @@
1
+ import type { AxiosInstance, AxiosRequestConfig } from 'axios'
2
+
3
+ // Caché SWR en MEMORIA para los GET de contenido de la web pública
4
+ // (stale-while-revalidate): la primera visita a una URL espera a la red
5
+ // (y el velo de navegación puede aparecer); las siguientes se sirven de la
6
+ // memoria AL INSTANTE — sin velo, sin negro — mientras una revalidación DE
7
+ // FONDO (edcBackground: no cuenta para el velo) trae lo fresco y solo
8
+ // re-aplica si algo cambió. La memoria vive lo que la pestaña: nada que
9
+ // invalidar entre sesiones.
10
+ //
11
+ // Uso: const swrGet = createSwrGet(api) // una vez, junto al cliente
12
+ // await swrGet<Payload>('/pages/home', undefined, (data) => { ... })
13
+ // El callback puede llegar DOS veces (caché y luego fresco): debe ser
14
+ // idempotente y, si la vista pudo cambiar mientras tanto (requestId),
15
+ // comprobar dentro que sigue vigente.
16
+
17
+ export interface SwrGetter {
18
+ <T>(
19
+ url: string,
20
+ config: AxiosRequestConfig | undefined,
21
+ onData: (data: T, fresh: boolean) => void,
22
+ ): Promise<void>
23
+ /** Vacía la memoria (p. ej. al cerrar sesión, si el contenido depende de ella). */
24
+ clear(): void
25
+ }
26
+
27
+ export function createSwrGet(api: AxiosInstance): SwrGetter {
28
+ const memory = new Map<string, unknown>()
29
+
30
+ // La clave incluye los params por defecto del cliente (el `?locale` que
31
+ // inyecta el store de locales) además de los de la petición: la misma URL
32
+ // en otro idioma es OTRA entrada.
33
+ function keyFor(url: string, config?: AxiosRequestConfig): string {
34
+ const params: Record<string, unknown> = {
35
+ ...(api.defaults.params as Record<string, unknown> | undefined),
36
+ ...(config?.params as Record<string, unknown> | undefined),
37
+ }
38
+ const query = Object.keys(params)
39
+ .sort()
40
+ .map((k) => `${k}=${JSON.stringify(params[k])}`)
41
+ .join('&')
42
+ return `${url}?${query}`
43
+ }
44
+
45
+ async function run<T>(
46
+ url: string,
47
+ config: AxiosRequestConfig | undefined,
48
+ onData: (data: T, fresh: boolean) => void,
49
+ ): Promise<void> {
50
+ const key = keyFor(url, config)
51
+ if (memory.has(key)) {
52
+ const cached = memory.get(key) as T
53
+ onData(cached, false)
54
+ void api
55
+ .get<T>(url, { ...config, edcBackground: true })
56
+ .then(({ data }) => {
57
+ memory.set(key, data)
58
+ if (JSON.stringify(data) !== JSON.stringify(cached)) onData(data, true)
59
+ })
60
+ .catch(() => {
61
+ // la revalidación es oportunista: si falla, se queda lo cacheado
62
+ })
63
+ return
64
+ }
65
+ const { data } = await api.get<T>(url, config)
66
+ memory.set(key, data)
67
+ onData(data, true)
68
+ }
69
+
70
+ const getter = run as SwrGetter
71
+ getter.clear = () => memory.clear()
72
+ return getter
73
+ }