@innertia-solutions/innertia-nuxt 0.1.11 → 0.1.13
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/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Attention.vue +269 -0
- package/components/Auth/LoginForm.vue +1 -1
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Organization/Picker.vue +3 -3
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useAuth.js +1 -1
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useContext.js +53 -5
- package/composables/useFile.js +189 -0
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useModal.js +71 -0
- package/composables/useOrganization.ts +3 -3
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
- package/composables/useApp.ts +0 -46
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# innertia-nuxt: Rename apps → contexts
|
|
2
|
+
|
|
3
|
+
**Fecha:** 2026-05-27
|
|
4
|
+
**Estado:** Aprobado para implementación
|
|
5
|
+
**Tipo:** Refactor — rename puro, sin cambios de comportamiento
|
|
6
|
+
**Scope:** innertia-nuxt library
|
|
7
|
+
|
|
8
|
+
## Problema
|
|
9
|
+
|
|
10
|
+
El concepto de "apps" (áreas del sistema a las que un usuario tiene acceso) colisiona semánticamente con "apps" en el contexto de frontend (aplicaciones, secciones de front). Renombrar a "contexts" elimina la ambigüedad y alinea la librería con el rename ya realizado en innertia-laravel (donde `user_apps`, `HasApps`, `appKeys()` etc. ya se renombraron a `user_contexts`, `HasContexts`, `contextKeys()`).
|
|
11
|
+
|
|
12
|
+
El backend ya devuelve `availableContexts` y `currentContext` — el frontend ya usaba esos nombres en el store. Este rename hace que la **capa de configuración y composables** sea consistente con la API que ya consume.
|
|
13
|
+
|
|
14
|
+
## Alcance
|
|
15
|
+
|
|
16
|
+
Rename puro. **Cero cambios de comportamiento.** Todo lo que hoy funciona, sigue funcionando igual — solo cambian los nombres.
|
|
17
|
+
|
|
18
|
+
**Fuera de scope:**
|
|
19
|
+
- `menuApps` en `app.config.ts` y `backoffice.vue` — semánticamente correcto como "secciones del menú", no relacionado con el sistema de user contexts. Se mantiene intacto.
|
|
20
|
+
- Lógica de organizations, mobile guard, o cualquier otra feature.
|
|
21
|
+
|
|
22
|
+
## Mapa de renombrado
|
|
23
|
+
|
|
24
|
+
### `app.config.ts` — config key + tipos
|
|
25
|
+
|
|
26
|
+
| Antes | Después |
|
|
27
|
+
|-------|---------|
|
|
28
|
+
| `innertia.apps: {}` | `innertia.contexts: {}` |
|
|
29
|
+
| `AppDefinition` interface | `ContextDefinition` |
|
|
30
|
+
| `AppMobileMode` type | `ContextMobileMode` |
|
|
31
|
+
|
|
32
|
+
`menuApps` **no se toca**.
|
|
33
|
+
|
|
34
|
+
### `composables/useContext.js` — fusión de useApp + useContext
|
|
35
|
+
|
|
36
|
+
`useApp.ts` se elimina. `useContext.js` absorbe su lógica y expone la unión completa:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const {
|
|
40
|
+
// — absorbido de useApp —
|
|
41
|
+
current, // ContextDefinition | null (contexto según URL actual)
|
|
42
|
+
all, // ContextDefinition[] (todos los declarados en config)
|
|
43
|
+
accessible, // ContextDefinition[] (filtrados por availableContexts del user)
|
|
44
|
+
canAccess, // (key: string) => boolean
|
|
45
|
+
contexts, // Record<string, ContextDefinition> (dict raw del config)
|
|
46
|
+
|
|
47
|
+
// — ya existía en useContext —
|
|
48
|
+
currentContext, // string | null
|
|
49
|
+
availableContexts, // string[]
|
|
50
|
+
switchContext, // (target: string) => Promise<{success, reason?}>
|
|
51
|
+
confirmSwitch, // (target: string) => Promise<{success}>
|
|
52
|
+
hasAccessToContext, // (context: string) => boolean
|
|
53
|
+
} = useContext()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Regla de resolución de la parte absorbida:** lee de `appConfig.innertia.contexts` (antes `appConfig.innertia.apps`). La lógica interna es idéntica a la de `useApp.ts` actual.
|
|
57
|
+
|
|
58
|
+
### `composables/useApp.ts`
|
|
59
|
+
|
|
60
|
+
**Eliminado.** Toda su funcionalidad vive en `useContext.js`.
|
|
61
|
+
|
|
62
|
+
### `composables/useAuth.js`
|
|
63
|
+
|
|
64
|
+
Login POST body:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
// Antes
|
|
68
|
+
api.post(`${context}/auth/login`, { email, password, app: context })
|
|
69
|
+
|
|
70
|
+
// Después
|
|
71
|
+
api.post(`${context}/auth/login`, { email, password, context })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
El backend (innertia-laravel) ya espera el campo `context`. Este fix cierra la inconsistencia.
|
|
75
|
+
|
|
76
|
+
### `middleware/03.apps.global.ts` → `middleware/03.contexts.global.ts`
|
|
77
|
+
|
|
78
|
+
- Renombrar el archivo.
|
|
79
|
+
- Internamente: `apps` variables → `contexts`, `AppDefinition` import → `ContextDefinition`, `appConfig.innertia.apps` → `appConfig.innertia.contexts`.
|
|
80
|
+
- La lógica de los 6 escenarios (ruta pública, login path, sin auth, sin contextos, sin acceso, sync) no cambia.
|
|
81
|
+
|
|
82
|
+
### `composables/useOrganization.ts`
|
|
83
|
+
|
|
84
|
+
- `useApp()` → `useContext()`
|
|
85
|
+
- Las propiedades que consume (`current`, referenciada internamente como `currentApp`) siguen existiendo con el mismo nombre en el composable fusionado.
|
|
86
|
+
|
|
87
|
+
### `composables/useMobileGuard.ts`
|
|
88
|
+
|
|
89
|
+
- `useApp()` → `useContext()`
|
|
90
|
+
- Propiedades consumidas: `all`, `current`, `accessible` — todas disponibles en el composable fusionado sin cambios de nombre.
|
|
91
|
+
|
|
92
|
+
### `CHANGELOG.md`
|
|
93
|
+
|
|
94
|
+
Agregar entrada `[Unreleased]` con sección `### BREAKING CHANGES`.
|
|
95
|
+
|
|
96
|
+
## Archivos a modificar (8)
|
|
97
|
+
|
|
98
|
+
1. `app.config.ts` — config key + tipos
|
|
99
|
+
2. `composables/useContext.js` — absorber useApp + actualizar internals
|
|
100
|
+
3. `composables/useApp.ts` — **eliminar**
|
|
101
|
+
4. `composables/useAuth.js` — fix body del login
|
|
102
|
+
5. `middleware/03.apps.global.ts` → **renombrar** a `03.contexts.global.ts` + editar internals
|
|
103
|
+
6. `composables/useOrganization.ts` — `useApp()` → `useContext()`
|
|
104
|
+
7. `composables/useMobileGuard.ts` — `useApp()` → `useContext()`
|
|
105
|
+
8. `CHANGELOG.md` — BREAKING CHANGE entry
|
|
106
|
+
|
|
107
|
+
## BREAKING CHANGES para productos consumidores
|
|
108
|
+
|
|
109
|
+
- Config `innertia.contexts` en vez de `innertia.apps`
|
|
110
|
+
- Tipo `ContextDefinition` en vez de `AppDefinition`
|
|
111
|
+
- Tipo `ContextMobileMode` en vez de `AppMobileMode`
|
|
112
|
+
- `useApp()` eliminado → usar `useContext()` (que ahora expone también `current`, `all`, `accessible`, `canAccess`, `contexts`)
|
|
113
|
+
- `performLogin` body: campo `context` en vez de `app`
|
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Middleware global que se ejecuta en cada navegación.
|
|
3
3
|
*
|
|
4
|
-
* Lee la declaración de
|
|
5
|
-
* no declara
|
|
4
|
+
* Lee la declaración de contextos desde `appConfig.innertia.contexts`. Si el
|
|
5
|
+
* producto no declara contextos, este middleware no hace nada (feature inactivo).
|
|
6
6
|
*
|
|
7
|
-
* Cuando hay
|
|
8
|
-
*
|
|
9
|
-
* 1. Si la URL no cae en ningún
|
|
10
|
-
* 2. Si la URL es la ruta de login del
|
|
7
|
+
* Cuando hay contextos declarados, resuelve qué contexto está intentando acceder
|
|
8
|
+
* el usuario según el prefijo de URL y:
|
|
9
|
+
* 1. Si la URL no cae en ningún contexto declarado → pasa (deja seguir)
|
|
10
|
+
* 2. Si la URL es la ruta de login del contexto → pasa (sin chequeo de permisos)
|
|
11
11
|
* 3. Si el usuario no está autenticado → deja que el middleware `auth` redirija
|
|
12
12
|
* 4. Si `availableContexts` está vacío (SSR sin hidratar) → intenta fetchMe()
|
|
13
13
|
* y si falla, pasa sin redirigir (mejor renderizar que redirigir mal)
|
|
14
|
-
* 5. Si el usuario está autenticado pero NO tiene acceso al contexto
|
|
15
|
-
* redirige al primer
|
|
16
|
-
* 6. Si todo OK pero `currentContext !==
|
|
17
|
-
* en el authStore y recarga permisos vía fetchMe()
|
|
14
|
+
* 5. Si el usuario está autenticado pero NO tiene acceso al contexto →
|
|
15
|
+
* redirige al primer contexto accesible (o a su loginPath si no tiene ninguno)
|
|
16
|
+
* 6. Si todo OK pero `currentContext !== targetContext.context` → sincroniza el
|
|
17
|
+
* contexto en el authStore y recarga permisos vía fetchMe()
|
|
18
18
|
*
|
|
19
19
|
* Esto es lo que permite el "cambio de contexto implícito por URL" sin botón.
|
|
20
20
|
*
|
|
21
21
|
* Numeración 03. para correr DESPUÉS de los middlewares saas
|
|
22
22
|
* (01.detect-subdomain, 02.validate-tenant).
|
|
23
23
|
*/
|
|
24
|
-
import type {
|
|
24
|
+
import type { ContextDefinition } from '../app.config'
|
|
25
25
|
|
|
26
26
|
export default defineNuxtRouteMiddleware(async (to) => {
|
|
27
27
|
const appConfig = useAppConfig()
|
|
28
|
-
const
|
|
29
|
-
const list = Object.values(
|
|
28
|
+
const contexts = (appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
|
|
29
|
+
const list = Object.values(contexts)
|
|
30
30
|
|
|
31
|
-
// Si el producto no declaró
|
|
31
|
+
// Si el producto no declaró contextos, el feature está inactivo
|
|
32
32
|
if (list.length === 0) return
|
|
33
33
|
|
|
34
|
-
// ¿La ruta cae bajo algún
|
|
35
|
-
const
|
|
36
|
-
to.path ===
|
|
34
|
+
// ¿La ruta cae bajo algún contexto?
|
|
35
|
+
const targetContext = list.find(ctx =>
|
|
36
|
+
to.path === ctx.path || to.path.startsWith(ctx.path + '/')
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
if (!
|
|
39
|
+
if (!targetContext) return // ruta pública / fuera de contextos
|
|
40
40
|
|
|
41
|
-
// La ruta de login del propio
|
|
42
|
-
if (to.path ===
|
|
41
|
+
// La ruta de login del propio contexto no requiere permisos
|
|
42
|
+
if (to.path === targetContext.loginPath) return
|
|
43
43
|
|
|
44
44
|
const authStore = useAuthStore()
|
|
45
45
|
|
|
@@ -66,9 +66,9 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
66
66
|
// Si tras hidratar seguimos sin contextos, NO redirigimos.
|
|
67
67
|
if (userContexts.length === 0) return
|
|
68
68
|
|
|
69
|
-
// ¿Tiene acceso al contexto
|
|
70
|
-
if (!userContexts.includes(
|
|
71
|
-
const fallback = list.find(
|
|
69
|
+
// ¿Tiene acceso al contexto?
|
|
70
|
+
if (!userContexts.includes(targetContext.context)) {
|
|
71
|
+
const fallback = list.find(ctx => userContexts.includes(ctx.context))
|
|
72
72
|
if (fallback) {
|
|
73
73
|
return navigateTo(fallback.home, { replace: true })
|
|
74
74
|
}
|
|
@@ -76,8 +76,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Sincronizar currentContext silenciosamente si cambia
|
|
79
|
-
if (authStore.currentContext !==
|
|
80
|
-
authStore.setCurrentContext(
|
|
79
|
+
if (authStore.currentContext !== targetContext.context) {
|
|
80
|
+
authStore.setCurrentContext(targetContext.context)
|
|
81
81
|
try {
|
|
82
82
|
const { fetchMe } = useAuth()
|
|
83
83
|
await fetchMe()
|
package/nuxt.config.ts
CHANGED
|
@@ -13,7 +13,7 @@ export default defineNuxtConfig({
|
|
|
13
13
|
{ path: './components', pathPrefix: true, prefix: '' },
|
|
14
14
|
],
|
|
15
15
|
imports: {
|
|
16
|
-
dirs: ['stores', 'composables'],
|
|
16
|
+
dirs: ['stores', 'composables', 'composables/useWorkflow'],
|
|
17
17
|
presets: [
|
|
18
18
|
{
|
|
19
19
|
from: '@tanstack/vue-query',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@innertia-solutions/innertia-nuxt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Innertia Solutions — Nuxt layer unificada: core, auth, multitenancy, theme y app contexts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nuxt",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"./theme.css": "./theme.css"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
+
"@pinia/nuxt": ">=0.11.0",
|
|
27
28
|
"nuxt": ">=4.0.0",
|
|
28
|
-
"vue": ">=3.5.0",
|
|
29
29
|
"pinia": ">=3.0.0",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"pinia-plugin-persistedstate": ">=4.0.0",
|
|
31
|
+
"vue": ">=3.5.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@nuxtjs/seo": "^3.4.0",
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
"@tailwindcss/vite": "^4.0.0",
|
|
40
40
|
"@tanstack/vue-query": "^5.100.10",
|
|
41
41
|
"@tanstack/vue-table": "^8.21.3",
|
|
42
|
+
"@vue-flow/background": "^1.3.2",
|
|
43
|
+
"@vue-flow/controls": "^1.1.3",
|
|
44
|
+
"@vue-flow/core": "^1.48.2",
|
|
42
45
|
"@vueuse/core": "^14.3.0",
|
|
46
|
+
"apexcharts": "^5.13.0",
|
|
43
47
|
"date-fns": "^4.1.0",
|
|
44
48
|
"date-fns-tz": "^3.2.0",
|
|
45
49
|
"dayjs": "^1.11.0",
|
|
@@ -48,13 +52,14 @@
|
|
|
48
52
|
"pusher-js": "^8.5.0",
|
|
49
53
|
"tailwindcss": "^4.0.0",
|
|
50
54
|
"uuid": "^13.0.0",
|
|
51
|
-
"vanilla-calendar-pro": "^3.1.0"
|
|
55
|
+
"vanilla-calendar-pro": "^3.1.0",
|
|
56
|
+
"vue3-apexcharts": "^1.11.1"
|
|
52
57
|
},
|
|
53
58
|
"devDependencies": {
|
|
54
|
-
"nuxt": "^4.4.2",
|
|
55
|
-
"vue": "^3.5.0",
|
|
56
59
|
"@pinia/nuxt": "^0.11.3",
|
|
60
|
+
"nuxt": "^4.4.2",
|
|
57
61
|
"pinia": "^3.0.4",
|
|
58
|
-
"pinia-plugin-persistedstate": "^4.7.1"
|
|
62
|
+
"pinia-plugin-persistedstate": "^4.7.1",
|
|
63
|
+
"vue": "^3.5.0"
|
|
59
64
|
}
|
|
60
65
|
}
|
package/stores/modal.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// stores/modal.js
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { markRaw } from 'vue'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cola de modales programáticos. Renderizada por <ModalHost />.
|
|
7
|
+
*
|
|
8
|
+
* Cada entry: { id, kind: 'confirm'|'alert'|'prompt'|'component',
|
|
9
|
+
* props: object, component?: Component, resolve: fn }
|
|
10
|
+
*
|
|
11
|
+
* `component` se guarda con markRaw para que Pinia no lo reactivee.
|
|
12
|
+
*/
|
|
13
|
+
let _id = 0
|
|
14
|
+
const nextId = () => `m${++_id}`
|
|
15
|
+
|
|
16
|
+
export const useModalStore = defineStore('modal', {
|
|
17
|
+
state: () => ({
|
|
18
|
+
queue: [],
|
|
19
|
+
}),
|
|
20
|
+
actions: {
|
|
21
|
+
push(entry) {
|
|
22
|
+
this.queue.push(entry)
|
|
23
|
+
return entry.id
|
|
24
|
+
},
|
|
25
|
+
remove(id) {
|
|
26
|
+
const idx = this.queue.findIndex(e => e.id === id)
|
|
27
|
+
if (idx !== -1) this.queue.splice(idx, 1)
|
|
28
|
+
},
|
|
29
|
+
resolve(id, value) {
|
|
30
|
+
const entry = this.queue.find(e => e.id === id)
|
|
31
|
+
if (!entry) return
|
|
32
|
+
entry.resolve(value)
|
|
33
|
+
this.remove(id)
|
|
34
|
+
},
|
|
35
|
+
/** Crea entry + devuelve Promise que se resuelve con .resolve(id, value) */
|
|
36
|
+
open({ kind, props = {}, component = null }) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
this.push({
|
|
39
|
+
id: nextId(),
|
|
40
|
+
kind,
|
|
41
|
+
props,
|
|
42
|
+
component: component ? markRaw(component) : null,
|
|
43
|
+
resolve,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
})
|
package/stores/toast.js
CHANGED
|
@@ -1,129 +1,168 @@
|
|
|
1
1
|
// stores/toast.js
|
|
2
2
|
import { defineStore } from 'pinia'
|
|
3
|
-
import {
|
|
3
|
+
import { markRaw } from 'vue'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Store unificado de toasts. Soporta tres variants:
|
|
7
|
+
*
|
|
8
|
+
* - 'alert' → toast simple con severity + título opcional + mensaje
|
|
9
|
+
* - 'notification' → más prominente, soporta acción y no auto-dismiss
|
|
10
|
+
* - 'process' → con barra de progreso, no auto-dismiss
|
|
11
|
+
*
|
|
12
|
+
* Y 6 posiciones: top-left|center|right · bottom-left|center|right
|
|
13
|
+
*
|
|
14
|
+
* Action opcional para cualquier variant:
|
|
15
|
+
* action: { label: 'Deshacer', onClick: () => { ... } }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const POSITIONS = ['top-left','top-center','top-right','bottom-left','bottom-center','bottom-right']
|
|
19
|
+
const SEVERITIES = ['success','error','warning','info']
|
|
20
|
+
|
|
21
|
+
const DEFAULTS = {
|
|
22
|
+
variant: 'alert',
|
|
23
|
+
severity: 'info',
|
|
24
|
+
position: 'top-right',
|
|
25
|
+
duration: 5000,
|
|
26
|
+
closable: true,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let _id = 0
|
|
30
|
+
const nextId = () => `t${++_id}`
|
|
31
|
+
|
|
32
|
+
function normalize(input, override = {}) {
|
|
33
|
+
const cfg = typeof input === 'string' ? { message: input } : { ...input }
|
|
34
|
+
const merged = { ...DEFAULTS, ...cfg, ...override }
|
|
35
|
+
if (!POSITIONS.includes(merged.position)) merged.position = 'top-right'
|
|
36
|
+
if (merged.action && typeof merged.action.onClick === 'function') {
|
|
37
|
+
// Pinia evita reactivear funciones — markRaw el objeto
|
|
38
|
+
merged.action = markRaw(merged.action)
|
|
39
|
+
}
|
|
40
|
+
return { ...merged, id: nextId(), createdAt: Date.now() }
|
|
13
41
|
}
|
|
14
42
|
|
|
15
43
|
export const useToastStore = defineStore('toast', {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this._scheduleRemoval(toast)
|
|
54
|
-
},
|
|
55
|
-
error(config) {
|
|
56
|
-
const normalizedConfig = typeof config === 'string' ? { message: config } : config
|
|
57
|
-
const toast = {
|
|
58
|
-
...toastConfig,
|
|
59
|
-
id: uuid(),
|
|
60
|
-
severity: 'danger',
|
|
61
|
-
icon: 'ti ti-exclamation-circle',
|
|
62
|
-
title: 'Atención',
|
|
63
|
-
position: 'top-right',
|
|
64
|
-
...normalizedConfig,
|
|
65
|
-
}
|
|
66
|
-
this.toasts[toast.position].push(toast)
|
|
67
|
-
this._scheduleRemoval(toast)
|
|
68
|
-
},
|
|
69
|
-
show(config) {
|
|
70
|
-
const toast = {
|
|
71
|
-
id: uuid(),
|
|
72
|
-
severity: 'info',
|
|
73
|
-
position: 'top-right',
|
|
74
|
-
...toastConfig,
|
|
75
|
-
...config,
|
|
76
|
-
}
|
|
77
|
-
this.toasts[toast.position].push(toast)
|
|
78
|
-
this._scheduleRemoval(toast)
|
|
79
|
-
},
|
|
80
|
-
update(id, data) {
|
|
81
|
-
for (const key in this.toasts) {
|
|
82
|
-
const idx = this.toasts[key].findIndex(t => t.id === id)
|
|
83
|
-
if (idx !== -1) {
|
|
84
|
-
this.toasts[key][idx] = { ...this.toasts[key][idx], ...data }
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
remove(id) {
|
|
90
|
-
if (this.timeouts[id] !== undefined) {
|
|
91
|
-
clearTimeout(this.timeouts[id])
|
|
92
|
-
delete this.timeouts[id]
|
|
93
|
-
}
|
|
44
|
+
state: () => ({
|
|
45
|
+
toasts: Object.fromEntries(POSITIONS.map(p => [p, []])),
|
|
46
|
+
timeouts: {},
|
|
47
|
+
}),
|
|
48
|
+
getters: {
|
|
49
|
+
flat(state) {
|
|
50
|
+
return POSITIONS.flatMap(p => state.toasts[p])
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
actions: {
|
|
54
|
+
_push(toast) {
|
|
55
|
+
this.toasts[toast.position].push(toast)
|
|
56
|
+
this._scheduleRemoval(toast)
|
|
57
|
+
return toast.id
|
|
58
|
+
},
|
|
59
|
+
_scheduleRemoval(toast) {
|
|
60
|
+
if (!toast.duration || toast.duration <= 0) return
|
|
61
|
+
const tid = setTimeout(() => this.dismiss(toast.id), toast.duration)
|
|
62
|
+
this.timeouts[toast.id] = tid
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// ── API pública ──────────────────────────────────────────────────
|
|
66
|
+
show(config) {
|
|
67
|
+
return this._push(normalize(config))
|
|
68
|
+
},
|
|
69
|
+
success(input, config = {}) {
|
|
70
|
+
return this._push(normalize(input, { ...config, severity: 'success' }))
|
|
71
|
+
},
|
|
72
|
+
error(input, config = {}) {
|
|
73
|
+
return this._push(normalize(input, { ...config, severity: 'error', duration: config.duration ?? 7000 }))
|
|
74
|
+
},
|
|
75
|
+
warning(input, config = {}) {
|
|
76
|
+
return this._push(normalize(input, { ...config, severity: 'warning' }))
|
|
77
|
+
},
|
|
78
|
+
info(input, config = {}) {
|
|
79
|
+
return this._push(normalize(input, { ...config, severity: 'info' }))
|
|
80
|
+
},
|
|
94
81
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
_scheduleRemoval(toast) {
|
|
100
|
-
if (toast.duration && toast.duration > 0) {
|
|
101
|
-
const timeoutId = setTimeout(() => {
|
|
102
|
-
this.remove(toast.id)
|
|
103
|
-
}, toast.duration)
|
|
82
|
+
/** Notification — variant prominente, NO auto-dismiss por defecto. */
|
|
83
|
+
notify(config = {}) {
|
|
84
|
+
return this._push(normalize({ ...config, variant: 'notification', duration: config.duration ?? 0 }))
|
|
85
|
+
},
|
|
104
86
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
87
|
+
/** Process — toast con barra de progreso. Devuelve id para actualizar. */
|
|
88
|
+
process(title, config = {}) {
|
|
89
|
+
return this._push(normalize({
|
|
90
|
+
...config,
|
|
91
|
+
variant: 'process',
|
|
92
|
+
title,
|
|
93
|
+
progress: 0,
|
|
94
|
+
progressLabel: config.progressLabel ?? 'Iniciando…',
|
|
95
|
+
duration: 0,
|
|
96
|
+
closable: config.closable ?? false,
|
|
97
|
+
}))
|
|
98
|
+
},
|
|
108
99
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
100
|
+
update(id, patch) {
|
|
101
|
+
for (const p of POSITIONS) {
|
|
102
|
+
const idx = this.toasts[p].findIndex(t => t.id === id)
|
|
103
|
+
if (idx !== -1) {
|
|
104
|
+
this.toasts[p][idx] = { ...this.toasts[p][idx], ...patch }
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
updateProgress(id, progress, progressLabel) {
|
|
110
|
+
const patch = { progress: Math.min(100, Math.max(0, progress)) }
|
|
111
|
+
if (progressLabel != null) patch.progressLabel = progressLabel
|
|
112
|
+
this.update(id, patch)
|
|
113
|
+
},
|
|
114
|
+
completeProcess(id, message = 'Completado') {
|
|
115
|
+
this.update(id, {
|
|
116
|
+
progress: 100,
|
|
117
|
+
progressLabel: message,
|
|
118
|
+
severity: 'success',
|
|
119
|
+
closable: true,
|
|
120
|
+
})
|
|
121
|
+
// Auto-dismiss después de 2.5s
|
|
122
|
+
const tid = setTimeout(() => this.dismiss(id), 2500)
|
|
123
|
+
this.timeouts[id] = tid
|
|
124
|
+
},
|
|
125
|
+
failProcess(id, message = 'Error') {
|
|
126
|
+
this.update(id, {
|
|
127
|
+
progressLabel: message,
|
|
128
|
+
severity: 'error',
|
|
129
|
+
closable: true,
|
|
130
|
+
duration: 7000,
|
|
131
|
+
})
|
|
132
|
+
const tid = setTimeout(() => this.dismiss(id), 7000)
|
|
133
|
+
this.timeouts[id] = tid
|
|
134
|
+
},
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
dismiss(id) {
|
|
137
|
+
if (this.timeouts[id] != null) {
|
|
138
|
+
clearTimeout(this.timeouts[id])
|
|
139
|
+
delete this.timeouts[id]
|
|
140
|
+
}
|
|
141
|
+
for (const p of POSITIONS) {
|
|
142
|
+
const idx = this.toasts[p].findIndex(t => t.id === id)
|
|
143
|
+
if (idx !== -1) {
|
|
144
|
+
this.toasts[p].splice(idx, 1)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
}
|
|
128
148
|
},
|
|
149
|
+
clear(position) {
|
|
150
|
+
if (position && this.toasts[position]) {
|
|
151
|
+
for (const t of this.toasts[position]) {
|
|
152
|
+
if (this.timeouts[t.id]) { clearTimeout(this.timeouts[t.id]); delete this.timeouts[t.id] }
|
|
153
|
+
}
|
|
154
|
+
this.toasts[position] = []
|
|
155
|
+
} else {
|
|
156
|
+
for (const p of POSITIONS) {
|
|
157
|
+
for (const t of this.toasts[p]) {
|
|
158
|
+
if (this.timeouts[t.id]) { clearTimeout(this.timeouts[t.id]); delete this.timeouts[t.id] }
|
|
159
|
+
}
|
|
160
|
+
this.toasts[p] = []
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// ── Compat (deprecated) ────────────────────────────────────────
|
|
166
|
+
remove(id) { this.dismiss(id) },
|
|
167
|
+
},
|
|
129
168
|
})
|
package/composables/useApp.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { AppDefinition } from '../app.config'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Devuelve metadata del app actual + listas filtradas por permisos del usuario.
|
|
5
|
-
* Lee la declaración de apps desde `appConfig.innertia.apps` (configurable por
|
|
6
|
-
* el producto en su `nuxt.config.ts`).
|
|
7
|
-
*
|
|
8
|
-
* Uso típico:
|
|
9
|
-
* const { current, accessible } = useApp()
|
|
10
|
-
* <h1>Estás en: {{ current?.label }}</h1>
|
|
11
|
-
*/
|
|
12
|
-
export function useApp() {
|
|
13
|
-
const route = useRoute()
|
|
14
|
-
const authStore = useAuthStore()
|
|
15
|
-
const appConfig = useAppConfig()
|
|
16
|
-
|
|
17
|
-
/** Diccionario de apps declarados por el producto. */
|
|
18
|
-
const apps = computed<Record<string, AppDefinition>>(() =>
|
|
19
|
-
(appConfig.innertia?.apps ?? {}) as Record<string, AppDefinition>
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
/** Todos los apps declarados, en orden de declaración. */
|
|
23
|
-
const all = computed<AppDefinition[]>(() => Object.values(apps.value))
|
|
24
|
-
|
|
25
|
-
/** App actual, determinado por el prefijo de la ruta. `null` si la URL no cae en ningún app. */
|
|
26
|
-
const current = computed<AppDefinition | null>(() => {
|
|
27
|
-
return all.value.find(app =>
|
|
28
|
-
route.path === app.path || route.path.startsWith(app.path + '/')
|
|
29
|
-
) ?? null
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
/** Apps a los que el usuario autenticado tiene acceso (filtrado por `availableContexts`). */
|
|
33
|
-
const accessible = computed<AppDefinition[]>(() => {
|
|
34
|
-
const ctxs = (authStore.availableContexts ?? []) as string[]
|
|
35
|
-
return all.value.filter(app => ctxs.includes(app.context))
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
/** Helper: ¿el usuario puede acceder a este app? */
|
|
39
|
-
function canAccess(appKey: string): boolean {
|
|
40
|
-
const app = apps.value[appKey]
|
|
41
|
-
if (!app) return false
|
|
42
|
-
return ((authStore.availableContexts ?? []) as string[]).includes(app.context)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { current, all, accessible, canAccess, apps }
|
|
46
|
-
}
|