@edc-motor/ui 0.5.18 → 0.5.19

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.18",
3
+ "version": "0.5.19",
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
@@ -53,6 +53,7 @@ export { default as LocaleSelector } from './components/LocaleSelector.vue'
53
53
  export { default as AppBreadcrumbs, type Crumb } from './components/AppBreadcrumbs.vue'
54
54
  export { default as PreviewGrid, type PreviewGridItem } from './components/PreviewGrid.vue'
55
55
  export { createApi, type CreateApiOptions } from './lib/createApi'
56
+ export { watchSplash, dismissSplash, type WatchSplashOptions } from './lib/splash'
56
57
  export { useToast, type Toast } from './composables/useToast'
57
58
  export { useConfirm, type ConfirmOptions } from './composables/useConfirm'
58
59
  export { useTheme, type ThemeMode } from './composables/useTheme'
@@ -0,0 +1,93 @@
1
+ import type { AxiosInstance } from 'axios'
2
+
3
+ // Splash de arranque. La SPA sirve un cascarón instantáneo y pide TODO lo
4
+ // real (settings, menús, sesión, contenido de la vista) a la API después de
5
+ // montar: hasta que responde, cada componente pinta su estado por defecto.
6
+ // En local (API a ~1 ms) ese fotograma provisional no llega a verse; en
7
+ // producción sí. El remedio: el index.html estático trae un velo a pantalla
8
+ // completa (#edc-splash, HTML autosuficiente pintado desde el fotograma
9
+ // cero, antes incluso de descargar el bundle) y este módulo lo retira
10
+ // cuando el arranque termina de verdad.
11
+ //
12
+ // ¿Y cuándo termina «de verdad»? En vez de instrumentar cada vista, se
13
+ // observa el cliente axios: se cuentan las peticiones en vuelo y el splash
14
+ // cae en el PRIMER REPOSO DE RED (cero peticiones durante `quietMs`). Eso
15
+ // cubre solo la cascada inicial —locales → settings → datos de la primera
16
+ // vista, encadenados por microtareas que siempre ganan al temporizador— y
17
+ // no exige tocar las vistas. Un tope (`maxWaitMs`) garantiza que una API
18
+ // caída nunca deja el velo puesto.
19
+ //
20
+ // Uso (main.ts, ANTES de app.mount() — si se llama después, las peticiones
21
+ // del onMounted ya habrían salido sin contar):
22
+ //
23
+ // watchSplash({ api })
24
+ // app.mount('#app')
25
+
26
+ const SPLASH_ID = 'edc-splash'
27
+
28
+ export interface WatchSplashOptions {
29
+ /** Cliente(s) axios cuyas peticiones marcan el arranque (createApi). */
30
+ api?: AxiosInstance | AxiosInstance[]
31
+ /** Reposo de red que da el arranque por terminado (ms). */
32
+ quietMs?: number
33
+ /** Tope duro: el splash cae aunque la red siga ocupada o caída (ms). */
34
+ maxWaitMs?: number
35
+ }
36
+
37
+ /** Retira el splash con su fundido (idempotente; sin splash, no hace nada). */
38
+ export function dismissSplash(): void {
39
+ const el = document.getElementById(SPLASH_ID)
40
+ if (!el || el.classList.contains('edc-splash--done')) return
41
+ el.classList.add('edc-splash--done')
42
+ const remove = () => el.remove()
43
+ el.addEventListener('transitionend', remove, { once: true })
44
+ // Por si no hay transición (prefers-reduced-motion, CSS recortado).
45
+ setTimeout(remove, 600)
46
+ }
47
+
48
+ /** Observa el arranque y retira el splash en el primer reposo de red. */
49
+ export function watchSplash(options: WatchSplashOptions = {}): void {
50
+ if (typeof document === 'undefined') return
51
+ if (!document.getElementById(SPLASH_ID)) return
52
+
53
+ const quietMs = options.quietMs ?? 200
54
+ const maxWaitMs = options.maxWaitMs ?? 8000
55
+ const apis = Array.isArray(options.api) ? options.api : options.api ? [options.api] : []
56
+
57
+ let inflight = 0
58
+ let quietTimer: ReturnType<typeof setTimeout> | undefined
59
+
60
+ const done = () => {
61
+ clearTimeout(quietTimer)
62
+ // Doble rAF: el render definitivo llega a pintarse BAJO el velo antes
63
+ // del fundido (sin esto el fundido podría destapar un frame a medias).
64
+ requestAnimationFrame(() => requestAnimationFrame(dismissSplash))
65
+ }
66
+ const armQuiet = () => {
67
+ clearTimeout(quietTimer)
68
+ quietTimer = setTimeout(done, quietMs)
69
+ }
70
+
71
+ for (const api of apis) {
72
+ api.interceptors.request.use((config) => {
73
+ inflight++
74
+ clearTimeout(quietTimer)
75
+ return config
76
+ })
77
+ api.interceptors.response.use(
78
+ (response) => {
79
+ if (--inflight <= 0) armQuiet()
80
+ return response
81
+ },
82
+ (error) => {
83
+ if (--inflight <= 0) armQuiet()
84
+ return Promise.reject(error)
85
+ },
86
+ )
87
+ }
88
+
89
+ // Armado inicial: si el arranque no llega a pedir nada (o no se pasó
90
+ // `api`), el splash cae solo tras el primer reposo.
91
+ armQuiet()
92
+ setTimeout(dismissSplash, maxWaitMs)
93
+ }