@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/app.config.ts +10 -10
  3. package/components/Attention.vue +269 -0
  4. package/components/Auth/LoginForm.vue +1 -1
  5. package/components/Chart/index.vue +193 -0
  6. package/components/Container.vue +116 -0
  7. package/components/CopyButton.vue +98 -0
  8. package/components/Files/Card.vue +215 -0
  9. package/components/Files/Preview.vue +675 -0
  10. package/components/Files/Upload.vue +408 -0
  11. package/components/Files/UploadModal.vue +83 -0
  12. package/components/Forms/InputPin.vue +162 -0
  13. package/components/InfoToolbar.vue +54 -0
  14. package/components/Kanban/Standard.vue +588 -0
  15. package/components/Loader.vue +358 -0
  16. package/components/MobileBlocker.vue +5 -5
  17. package/components/MobileLoginPicker.vue +13 -13
  18. package/components/Modal/Base.vue +6 -0
  19. package/components/Modal/Confirm.vue +89 -0
  20. package/components/Modal/DeleteConfirm.vue +14 -31
  21. package/components/Modal/Form.vue +76 -0
  22. package/components/Modal/Host.vue +58 -0
  23. package/components/Modal/Prompt.vue +74 -0
  24. package/components/Modal.vue +119 -34
  25. package/components/Organization/Picker.vue +3 -3
  26. package/components/Table/Standard.vue +198 -25
  27. package/components/Table/index.vue +169 -369
  28. package/components/Table/views/ViewCards.vue +74 -0
  29. package/components/Table/views/ViewDatabase.vue +236 -0
  30. package/components/Table/views/ViewTable.vue +299 -0
  31. package/components/Toast/Alert.vue +16 -111
  32. package/components/Toast/Container.vue +4 -29
  33. package/components/Toast/Notification.vue +8 -43
  34. package/components/Toast/Process.vue +8 -86
  35. package/components/Toast/Stack.vue +59 -0
  36. package/components/Toast/Standard.vue +115 -0
  37. package/components/Tree/Standard.vue +49 -4
  38. package/components/Workflow/Board.vue +356 -0
  39. package/components/Workflow/Builder.vue +454 -0
  40. package/components/Workflow/Form.vue +526 -0
  41. package/components/Workflow/Viewer.vue +225 -0
  42. package/components/Workflow/edges/TransitionEdge.vue +84 -0
  43. package/components/Workflow/nodes/StepNode.vue +88 -0
  44. package/composables/useAuth.js +1 -1
  45. package/composables/useClipboard.js +79 -0
  46. package/composables/useConfetti.js +194 -0
  47. package/composables/useContext.js +53 -5
  48. package/composables/useFile.js +189 -0
  49. package/composables/useMobileGuard.ts +21 -21
  50. package/composables/useModal.js +71 -0
  51. package/composables/useOrganization.ts +3 -3
  52. package/composables/useToast.js +38 -65
  53. package/composables/useWorkflow/form.js +162 -0
  54. package/composables/useWorkflow/index.js +18 -0
  55. package/composables/useWorkflow/view.js +220 -0
  56. package/composables/useWorkflowLayout.js +162 -0
  57. package/css/tokens.css +10 -10
  58. package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
  59. package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
  60. package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
  61. package/nuxt.config.ts +1 -1
  62. package/package.json +13 -8
  63. package/stores/modal.js +48 -0
  64. package/stores/toast.js +156 -117
  65. 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 apps desde `appConfig.innertia.apps`. Si el producto
5
- * no declara apps, este middleware no hace nada (feature inactivo).
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 apps declaradas, resuelve qué "app" (contexto) está intentando
8
- * acceder el usuario según el prefijo de URL y:
9
- * 1. Si la URL no cae en ningún app declarado → pasa (deja seguir)
10
- * 2. Si la URL es la ruta de login del app → pasa (sin chequeo de permisos)
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 del app →
15
- * redirige al primer app accesible (o a su loginPath si no tiene ninguno)
16
- * 6. Si todo OK pero `currentContext !== app.context` → sincroniza el contexto
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 { AppDefinition } from '../app.config'
24
+ import type { ContextDefinition } from '../app.config'
25
25
 
26
26
  export default defineNuxtRouteMiddleware(async (to) => {
27
27
  const appConfig = useAppConfig()
28
- const apps = (appConfig.innertia?.apps ?? {}) as Record<string, AppDefinition>
29
- const list = Object.values(apps)
28
+ const contexts = (appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
29
+ const list = Object.values(contexts)
30
30
 
31
- // Si el producto no declaró apps, el feature está inactivo
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 app?
35
- const targetApp = list.find(app =>
36
- to.path === app.path || to.path.startsWith(app.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 (!targetApp) return // ruta pública / fuera de apps
39
+ if (!targetContext) return // ruta pública / fuera de contextos
40
40
 
41
- // La ruta de login del propio app no requiere permisos del contexto
42
- if (to.path === targetApp.loginPath) return
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 del app?
70
- if (!userContexts.includes(targetApp.context)) {
71
- const fallback = list.find(app => userContexts.includes(app.context))
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 !== targetApp.context) {
80
- authStore.setCurrentContext(targetApp.context)
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.11",
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
- "@pinia/nuxt": ">=0.11.0",
31
- "pinia-plugin-persistedstate": ">=4.0.0"
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
  }
@@ -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 { v4 as uuid } from 'uuid'
3
+ import { markRaw } from 'vue'
4
4
 
5
- const toastConfig = {
6
- severity: 'info',
7
- title: null,
8
- message: null,
9
- duration: 5000,
10
- progress: true,
11
- position: 'top-right',
12
- icon: null,
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
- state: () => ({
17
- toasts: {
18
- 'top-left': [],
19
- 'top-center': [],
20
- 'top-right': [],
21
- 'bottom-left': [],
22
- 'bottom-center': [],
23
- 'bottom-right': [],
24
- },
25
- timeouts: {}, // id → timeoutId (plain object, JSON-safe)
26
- }),
27
- actions: {
28
- success(config) {
29
- const normalizedConfig = typeof config === 'string' ? { message: config } : config
30
- const toast = {
31
- ...toastConfig,
32
- id: uuid(),
33
- severity: 'success',
34
- icon: 'ti ti-circle-check text-green-500',
35
- position: 'top-center',
36
- ...normalizedConfig,
37
- }
38
- this.toasts[toast.position].push(toast)
39
- this._scheduleRemoval(toast)
40
- },
41
- info(config) {
42
- const normalizedConfig = typeof config === 'string' ? { message: config } : config
43
- const toast = {
44
- ...toastConfig,
45
- id: uuid(),
46
- severity: 'info',
47
- icon: 'ti ti-info-circle',
48
- title: 'Info',
49
- position: 'top-right',
50
- ...normalizedConfig,
51
- }
52
- this.toasts[toast.position].push(toast)
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
- for (const key in this.toasts) {
96
- this.toasts[key] = this.toasts[key].filter(t => t.id !== id)
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
- this.timeouts[toast.id] = timeoutId
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
- // Método específico para actualizar progreso de un toast tipo process
110
- updateProgress(id, progress, progressLabel = null) {
111
- const updateData = { progress }
112
- if (progressLabel) {
113
- updateData.progressLabel = progressLabel
114
- }
115
- this.update(id, updateData)
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
- // Método para completar un proceso
119
- completeProcess(id, message = 'Completado') {
120
- this.update(id, {
121
- progress: 100,
122
- progressLabel: message,
123
- duration: 3000, // Auto-remove después de completar
124
- })
125
- // Programar removal para el proceso completado
126
- this._scheduleRemoval({ id, duration: 3000 })
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
  })
@@ -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
- }