@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
@@ -1,11 +1,46 @@
1
- // useAuthStore, useApi, useAuth auto-imported
1
+ // useAuthStore, useApi, useAuth, useRoute, useAppConfig auto-imported
2
2
  import { computed } from 'vue'
3
+ import type { ContextDefinition } from '../app.config'
3
4
 
4
5
  export function useContext() {
6
+ const route = useRoute()
5
7
  const authStore = useAuthStore()
8
+ const appConfig = useAppConfig()
6
9
  const api = useApi()
7
10
  const { fetchMe } = useAuth()
8
11
 
12
+ // ── Config-level: definiciones declaradas en appConfig.innertia.contexts ───
13
+
14
+ /** Diccionario de contextos declarados por el producto. */
15
+ const contexts = computed<Record<string, ContextDefinition>>(() =>
16
+ (appConfig.innertia?.contexts ?? {}) as Record<string, ContextDefinition>
17
+ )
18
+
19
+ /** Todos los contextos declarados, en orden de declaración. */
20
+ const all = computed<ContextDefinition[]>(() => Object.values(contexts.value))
21
+
22
+ /** Contexto actual según URL. `null` si la URL no cae en ningún contexto declarado. */
23
+ const current = computed<ContextDefinition | null>(() => {
24
+ return all.value.find(ctx =>
25
+ route.path === ctx.path || route.path.startsWith(ctx.path + '/')
26
+ ) ?? null
27
+ })
28
+
29
+ /** Contextos a los que el usuario autenticado tiene acceso (filtrado por `availableContexts`). */
30
+ const accessible = computed<ContextDefinition[]>(() => {
31
+ const ctxs = (authStore.availableContexts ?? []) as string[]
32
+ return all.value.filter(ctx => ctxs.includes(ctx.context))
33
+ })
34
+
35
+ /** Helper: ¿el usuario puede acceder a este contexto (por clave de config)? */
36
+ function canAccess(key: string): boolean {
37
+ const ctx = contexts.value[key]
38
+ if (!ctx) return false
39
+ return ((authStore.availableContexts ?? []) as string[]).includes(ctx.context)
40
+ }
41
+
42
+ // ── Runtime: contexto activo + switching ───────────────────────────────────
43
+
9
44
  const currentContext = computed(() => authStore.currentContext)
10
45
  const availableContexts = computed(() => authStore.availableContexts)
11
46
 
@@ -15,7 +50,7 @@ export function useContext() {
15
50
  * { success: false, reason: 'no_permission' } — user cannot switch
16
51
  * { success: true, requiresConfirmation: true } — show confirmation UI
17
52
  */
18
- async function switchContext(targetContext) {
53
+ async function switchContext(targetContext: string) {
19
54
  const data = await api.get(`auth/context/${targetContext}/check`)
20
55
  if (!data.hasAccess) {
21
56
  return { success: false, reason: 'no_permission' }
@@ -27,7 +62,7 @@ export function useContext() {
27
62
  * Execute the context switch after user confirmation.
28
63
  * Updates store and reloads permissions via fetchMe.
29
64
  */
30
- async function confirmSwitch(targetContext) {
65
+ async function confirmSwitch(targetContext: string) {
31
66
  authStore.setCurrentContext(targetContext)
32
67
  await fetchMe()
33
68
  return { success: true }
@@ -36,9 +71,22 @@ export function useContext() {
36
71
  /**
37
72
  * Quick synchronous check — is this context in the available list?
38
73
  */
39
- function hasAccessToContext(context) {
74
+ function hasAccessToContext(context: string) {
40
75
  return authStore.availableContexts.includes(context)
41
76
  }
42
77
 
43
- return { currentContext, availableContexts, switchContext, confirmSwitch, hasAccessToContext }
78
+ return {
79
+ // config-level
80
+ contexts,
81
+ all,
82
+ current,
83
+ accessible,
84
+ canAccess,
85
+ // runtime
86
+ currentContext,
87
+ availableContexts,
88
+ switchContext,
89
+ confirmSwitch,
90
+ hasAccessToContext,
91
+ }
44
92
  }
@@ -0,0 +1,189 @@
1
+ import {
2
+ IconFile, IconPhoto, IconVideo, IconMusic, IconFileZip,
3
+ IconFileTypePdf, IconFileTypeXls, IconFileTypeCsv,
4
+ IconFileTypeDocx, IconFileTypePpt, IconFileTypeTxt,
5
+ } from '@tabler/icons-vue'
6
+
7
+ /**
8
+ * useFile — API unificada para trabajar con archivos del backend innertia-laravel.
9
+ *
10
+ * Cubre:
11
+ * - upload(endpoint, file|files, { field, onProgress, extraData, signal })
12
+ * - download(fileId, filename?) — trigger descarga programática
13
+ * - view(fileId) — abrir inline en otra pestaña
14
+ * - viewUrl(fileId) / downloadUrl(fileId) — URLs absolutas a las rutas de innertia-laravel
15
+ * - formatSize(bytes) — helper humano-legible
16
+ * - iconFor(mimeType) — devuelve el componente Tabler para el mime
17
+ *
18
+ * Autenticación: los interceptores de useApi (auth Bearer, X-Tenant, X-Organization)
19
+ * se aplican automáticamente al XHR de upload.
20
+ */
21
+ export function useFile() {
22
+ const config = useRuntimeConfig()
23
+ const baseUrl = config.public.apiBaseUrl || '/api'
24
+
25
+ // useRequestInterceptors es el mismo que usa useApi() — comparte auth/tenant/org headers.
26
+ const { run } = useRequestInterceptors()
27
+
28
+ // ── URLs ───────────────────────────────────────────────────────────────────
29
+ /** GET /files/{id} — inline view (respeta permisos del backend). */
30
+ const viewUrl = (fileId) => `${baseUrl}/files/${fileId}`
31
+ /** GET /files/{id}/download — force download (respeta permisos). */
32
+ const downloadUrl = (fileId) => `${baseUrl}/files/${fileId}/download`
33
+
34
+ // ── Helpers ────────────────────────────────────────────────────────────────
35
+ const formatSize = (bytes) => {
36
+ if (bytes == null) return ''
37
+ if (bytes < 1024) return `${bytes} B`
38
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
39
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
40
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
41
+ }
42
+
43
+ /** Devuelve el componente Tabler para un mime type. */
44
+ const iconFor = (mimeType) => {
45
+ if (!mimeType) return IconFile
46
+ if (mimeType.startsWith('image/')) return IconPhoto
47
+ if (mimeType.startsWith('video/')) return IconVideo
48
+ if (mimeType.startsWith('audio/')) return IconMusic
49
+ if (mimeType === 'application/pdf') return IconFileTypePdf
50
+ if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return IconFileTypeXls
51
+ if (mimeType === 'text/csv') return IconFileTypeCsv
52
+ if (mimeType.includes('word') || mimeType.includes('document')) return IconFileTypeDocx
53
+ if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return IconFileTypePpt
54
+ if (mimeType.startsWith('text/')) return IconFileTypeTxt
55
+ if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) return IconFileZip
56
+ return IconFile
57
+ }
58
+
59
+ // ── View / Download ────────────────────────────────────────────────────────
60
+ /** Abre el file inline en una nueva pestaña (usa la auth de cookie). */
61
+ const view = (fileId) => {
62
+ if (typeof window === 'undefined') return
63
+ window.open(viewUrl(fileId), '_blank', 'noopener,noreferrer')
64
+ }
65
+
66
+ /**
67
+ * Trigger descarga programática. Funciona con archivos auth/restricted
68
+ * porque el browser envía cookies automáticamente.
69
+ */
70
+ const download = (fileId, filename) => {
71
+ if (typeof window === 'undefined') return
72
+ const a = document.createElement('a')
73
+ a.href = downloadUrl(fileId)
74
+ if (filename) a.download = filename
75
+ document.body.appendChild(a)
76
+ a.click()
77
+ document.body.removeChild(a)
78
+ }
79
+
80
+ // ── Upload (con progress vía XHR) ──────────────────────────────────────────
81
+ /**
82
+ * Upload un archivo (o lista) a un endpoint del producto. El endpoint
83
+ * recibe `multipart/form-data` con el campo `field` y debe persistir el
84
+ * archivo (típicamente `File::fromRequest($request, 'file')` del lado Laravel).
85
+ *
86
+ * Para varios archivos pasa un array — el campo se envía como `field[]`.
87
+ * Devuelve el JSON parseado del response (típicamente el File record creado).
88
+ *
89
+ * Opciones:
90
+ * field — nombre del campo. Default 'file'.
91
+ * onProgress — callback(percent: number 0-100)
92
+ * extraData — campos adicionales para incluir en el FormData
93
+ * signal — AbortSignal para cancelar
94
+ */
95
+ const upload = (endpoint, file, options = {}) => {
96
+ const field = options.field ?? 'file'
97
+ const files = Array.isArray(file) ? file : [file]
98
+ const formData = new FormData()
99
+ const fieldName = files.length > 1 ? `${field}[]` : field
100
+
101
+ for (const f of files) formData.append(fieldName, f)
102
+
103
+ if (options.extraData) {
104
+ for (const [k, v] of Object.entries(options.extraData)) {
105
+ if (v == null) continue
106
+ formData.append(k, typeof v === 'object' ? JSON.stringify(v) : String(v))
107
+ }
108
+ }
109
+
110
+ // Recolectar headers vía los interceptores (auth, tenant, org).
111
+ // NO incluimos Content-Type — el browser lo setea con el boundary correcto.
112
+ const headers = {
113
+ 'Accept': 'application/json',
114
+ 'X-Innertia-Source': import.meta.server ? 'ssr' : 'client',
115
+ }
116
+ run(headers, {})
117
+ delete headers['Content-Type']
118
+
119
+ const cleanPath = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint
120
+ const url = `${baseUrl}/${cleanPath}`
121
+
122
+ return new Promise((resolve, reject) => {
123
+ const xhr = new XMLHttpRequest()
124
+ xhr.open('POST', url)
125
+ xhr.withCredentials = true
126
+
127
+ for (const [k, v] of Object.entries(headers)) {
128
+ try { xhr.setRequestHeader(k, v) } catch (_) { /* algunos headers son read-only */ }
129
+ }
130
+
131
+ xhr.upload.addEventListener('progress', (e) => {
132
+ if (e.lengthComputable && options.onProgress) {
133
+ options.onProgress(Math.round((e.loaded / e.total) * 100), e.loaded, e.total)
134
+ }
135
+ })
136
+
137
+ // Cuando el upload del cliente terminó (100%) pero el server aún no
138
+ // respondió — útil para mostrar estado "Procesando…" en la UI.
139
+ xhr.upload.addEventListener('loadend', () => {
140
+ options.onUploaded?.()
141
+ })
142
+
143
+ xhr.addEventListener('load', () => {
144
+ const status = xhr.status
145
+ const ctype = xhr.getResponseHeader('content-type') ?? ''
146
+ const body = ctype.includes('application/json')
147
+ ? safeJsonParse(xhr.responseText)
148
+ : xhr.responseText
149
+
150
+ if (status >= 200 && status < 300) {
151
+ resolve(body)
152
+ } else {
153
+ const err = new Error(`Upload failed (${status})`)
154
+ err.status = status
155
+ err.data = body
156
+ reject(err)
157
+ }
158
+ })
159
+
160
+ xhr.addEventListener('error', () => reject(new Error('Network error during upload')))
161
+ xhr.addEventListener('abort', () => reject(Object.assign(new Error('Upload aborted'), { aborted: true })))
162
+ xhr.addEventListener('timeout', () => reject(new Error('Upload timeout')))
163
+
164
+ if (options.signal) {
165
+ if (options.signal.aborted) {
166
+ xhr.abort()
167
+ return
168
+ }
169
+ options.signal.addEventListener('abort', () => xhr.abort())
170
+ }
171
+
172
+ xhr.send(formData)
173
+ })
174
+ }
175
+
176
+ return {
177
+ upload,
178
+ download,
179
+ view,
180
+ viewUrl,
181
+ downloadUrl,
182
+ formatSize,
183
+ iconFor,
184
+ }
185
+ }
186
+
187
+ function safeJsonParse(text) {
188
+ try { return JSON.parse(text) } catch { return text }
189
+ }
@@ -1,61 +1,61 @@
1
1
  import { useMediaQuery } from '@vueuse/core'
2
- import type { AppDefinition } from '../app.config'
2
+ import type { ContextDefinition } from '../app.config'
3
3
 
4
- const PICKER_CHOICE_COOKIE = 'innertia_mobile_app_choice'
4
+ const PICKER_CHOICE_COOKIE = 'innertia_mobile_context_choice'
5
5
  const PICKER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 días
6
6
 
7
7
  /**
8
8
  * Detección reactiva de viewport mobile + lógica de landing.
9
9
  *
10
10
  * Reglas para `landing` (qué mostrar cuando el usuario entra a `/` en mobile):
11
- * - 0 apps mobile-friendly → blocker "abre en escritorio"
12
- * - 1 app mobile-friendly → redirect directo al login de ese app
13
- * - 2+ apps mobile-friendly → picker (con cookie para recordar última elección)
11
+ * - 0 contextos mobile-friendly → blocker "abre en escritorio"
12
+ * - 1 contexto mobile-friendly → redirect directo al login de ese contexto
13
+ * - 2+ contextos mobile-friendly → picker (con cookie para recordar última elección)
14
14
  *
15
15
  * El breakpoint se lee desde `appConfig.innertia.mobile.breakpoint` (default 1024).
16
16
  */
17
17
  export function useMobileGuard() {
18
- const { all, current, accessible } = useApp()
18
+ const { all, current, accessible } = useContext()
19
19
  const authStore = useAuthStore()
20
20
  const appConfig = useAppConfig()
21
21
 
22
22
  const breakpoint: number = appConfig.innertia?.mobile?.breakpoint ?? 1024
23
23
  const isMobile = useMediaQuery(`(max-width: ${breakpoint - 1}px)`)
24
24
 
25
- /** Apps con mode 'allow' en mobile. */
26
- const mobileApps = computed<AppDefinition[]>(() =>
25
+ /** Contextos con mode 'allow' en mobile. */
26
+ const mobileContexts = computed<ContextDefinition[]>(() =>
27
27
  all.value.filter(a => a.mobile?.mode === 'allow')
28
28
  )
29
29
 
30
- /** Apps mobile-friendly + accesibles para el usuario autenticado. */
31
- const mobileAccessibleApps = computed<AppDefinition[]>(() =>
30
+ /** Contextos mobile-friendly + accesibles para el usuario autenticado. */
31
+ const mobileAccessibleContexts = computed<ContextDefinition[]>(() =>
32
32
  accessible.value.filter(a => a.mobile?.mode === 'allow')
33
33
  )
34
34
 
35
- /** ¿El app actual está bloqueado en mobile? */
36
- const isCurrentAppBlocked = computed<boolean>(() => {
35
+ /** ¿El contexto actual está bloqueado en mobile? */
36
+ const isCurrentContextBlocked = computed<boolean>(() => {
37
37
  if (!isMobile.value) return false
38
38
  if (!current.value) return false
39
39
  return current.value.mobile?.mode === 'block'
40
40
  })
41
41
 
42
42
  /**
43
- * Para el blocker: si el usuario está autenticado y tiene OTRO app mobile-friendly,
43
+ * Para el blocker: si el usuario está autenticado y tiene OTRO contexto mobile-friendly,
44
44
  * lo ofrecemos como fallback ("continuar en X").
45
45
  */
46
- const mobileFallbackApp = computed<AppDefinition | null>(() => {
46
+ const mobileFallbackContext = computed<ContextDefinition | null>(() => {
47
47
  if (!authStore.isAuthenticated()) return null
48
- return mobileAccessibleApps.value[0] ?? null
48
+ return mobileAccessibleContexts.value[0] ?? null
49
49
  })
50
50
 
51
51
  // ── Cookie helpers para recordar última elección del picker ─────────────────
52
- function rememberPickerChoice(appKey: string) {
52
+ function rememberPickerChoice(contextKey: string) {
53
53
  if (!(appConfig.innertia?.mobile?.rememberChoice ?? true)) return
54
54
  const cookie = useCookie<string | null>(PICKER_CHOICE_COOKIE, {
55
55
  maxAge: PICKER_COOKIE_MAX_AGE,
56
56
  sameSite: 'lax',
57
57
  })
58
- cookie.value = appKey
58
+ cookie.value = contextKey
59
59
  }
60
60
 
61
61
  function getRememberedPickerChoice(): string | null {
@@ -70,10 +70,10 @@ export function useMobileGuard() {
70
70
 
71
71
  return {
72
72
  isMobile,
73
- mobileApps,
74
- mobileAccessibleApps,
75
- isCurrentAppBlocked,
76
- mobileFallbackApp,
73
+ mobileContexts,
74
+ mobileAccessibleContexts,
75
+ isCurrentContextBlocked,
76
+ mobileFallbackContext,
77
77
  rememberPickerChoice,
78
78
  getRememberedPickerChoice,
79
79
  clearRememberedPickerChoice,
@@ -0,0 +1,71 @@
1
+ // composables/useModal.js
2
+ import { useModalStore } from '../stores/modal'
3
+
4
+ /**
5
+ * useModal() — API promise-based para modales programáticos.
6
+ *
7
+ * Requiere <ModalHost /> montado una vez en el layout root.
8
+ *
9
+ * ```js
10
+ * const { confirm, alert, prompt, open } = useModal()
11
+ *
12
+ * if (await confirm({ severity: 'danger', message: '¿Eliminar?' })) {
13
+ * // user confirmed
14
+ * }
15
+ *
16
+ * await alert({ severity: 'success', message: 'Listo' })
17
+ *
18
+ * const name = await prompt({ title: 'Nombre', placeholder: 'Tu nombre' })
19
+ *
20
+ * const result = await open(MyCustomModal, { foo: 'bar' })
21
+ * ```
22
+ */
23
+ export function useModal() {
24
+ const store = useModalStore()
25
+
26
+ /** Confirmación. Resuelve true/false. */
27
+ const confirm = (opts = {}) => store.open({
28
+ kind: 'confirm',
29
+ props: {
30
+ severity: opts.severity ?? 'question',
31
+ title: opts.title ?? '',
32
+ message: opts.message ?? '',
33
+ confirmText: opts.confirmText ?? 'Confirmar',
34
+ cancelText: opts.cancelText ?? 'Cancelar',
35
+ },
36
+ })
37
+
38
+ /** Alert simple — solo botón OK. Resuelve undefined. */
39
+ const alert = (opts = {}) => store.open({
40
+ kind: 'alert',
41
+ props: {
42
+ severity: opts.severity ?? 'info',
43
+ title: opts.title ?? '',
44
+ message: opts.message ?? '',
45
+ confirmText: opts.confirmText ?? 'Entendido',
46
+ },
47
+ })
48
+
49
+ /** Prompt — pide un string. Resuelve string|null (null si cancela). */
50
+ const prompt = (opts = {}) => store.open({
51
+ kind: 'prompt',
52
+ props: {
53
+ title: opts.title ?? 'Ingresar valor',
54
+ message: opts.message ?? '',
55
+ placeholder: opts.placeholder ?? '',
56
+ defaultValue: opts.defaultValue ?? '',
57
+ confirmText: opts.confirmText ?? 'Aceptar',
58
+ cancelText: opts.cancelText ?? 'Cancelar',
59
+ required: opts.required ?? false,
60
+ },
61
+ })
62
+
63
+ /** Modal custom — montá tu propio componente. Resuelve con lo que pase a $close(value). */
64
+ const open = (component, props = {}) => store.open({
65
+ kind: 'component',
66
+ component,
67
+ props,
68
+ })
69
+
70
+ return { confirm, alert, prompt, open }
71
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Composable de organizaciones (sub-tenant scoping).
3
3
  *
4
- * Lee el contexto actual del usuario (via useApp) y expone su org activa + lista
4
+ * Lee el contexto actual del usuario (via useContext) y expone su org activa + lista
5
5
  * de accesibles para ese contexto. Cambiar de contexto rota la org automáticamente.
6
6
  *
7
7
  * Uso típico:
@@ -10,7 +10,7 @@
10
10
  * <OrganizationSwitcher v-if="available.length > 1" />
11
11
  */
12
12
  export function useOrganization() {
13
- const { current: currentApp } = useApp()
13
+ const { current: currentApp } = useContext()
14
14
  const organizationStore = useOrganizationStore()
15
15
  const appConfig = useAppConfig()
16
16
  const { fetchMe } = useAuth()
@@ -19,7 +19,7 @@ export function useOrganization() {
19
19
  const allowConsolidated = computed(() => !!appConfig.innertia?.organizations?.allowConsolidated)
20
20
  const required = computed(() => !!appConfig.innertia?.organizations?.required)
21
21
 
22
- /** Contexto actual (key) — desde useApp. */
22
+ /** Contexto actual (key) — desde useContext. */
23
23
  const currentContext = computed<string | null>(() => currentApp.value?.context ?? null)
24
24
 
25
25
  /** Lista de orgs accesibles para el contexto actual. */
@@ -1,69 +1,42 @@
1
- // app/composables/useToast.js
1
+ // composables/useToast.js
2
2
  import { useToastStore } from '../stores/toast'
3
3
 
4
+ /**
5
+ * useToast() — API limpia para mostrar toasts.
6
+ *
7
+ * ```js
8
+ * const toast = useToast()
9
+ *
10
+ * toast.success('Guardado')
11
+ * toast.error('Algo falló', { title: 'Error', duration: 8000 })
12
+ * toast.warning('Atención')
13
+ * toast.info('Tip')
14
+ *
15
+ * toast.notify({ title: 'Nueva mención', message: 'Te etiquetaron', action: { label: 'Ver', onClick: () => {} } })
16
+ *
17
+ * const id = toast.process('Subiendo archivo…', { position: 'bottom-right' })
18
+ * toast.updateProgress(id, 40, '40%')
19
+ * toast.completeProcess(id, 'Listo')
20
+ *
21
+ * toast.dismiss(id)
22
+ * toast.clear() // todos
23
+ * ```
24
+ */
4
25
  export function useToast() {
5
- const toast = useToastStore()
6
-
7
- return {
8
- // Métodos originales
9
- success: toast.success,
10
- info: toast.info,
11
- error: toast.error,
12
- show: toast.show,
13
- remove: toast.remove,
14
- update: toast.update,
15
- updateProgress: toast.updateProgress,
16
- completeProcess: toast.completeProcess,
17
-
18
- // Métodos rápidos para diferentes tipos de toast
19
- alert: {
20
- success: (message, config = {}) => toast.success({
21
- type: 'alert',
22
- message,
23
- duration: 3000,
24
- ...config
25
- }),
26
-
27
- error: (message, config = {}) => toast.error({
28
- type: 'alert',
29
- message,
30
- duration: 5000,
31
- ...config
32
- }),
33
-
34
- warning: (message, config = {}) => toast.show({
35
- type: 'alert',
36
- severity: 'warning',
37
- icon: 'ti ti-alert-triangle',
38
- title: 'Warning',
39
- message,
40
- duration: 4000,
41
- ...config
42
- }),
43
-
44
- info: (message, config = {}) => toast.info({
45
- type: 'alert',
46
- message,
47
- duration: 3000,
48
- ...config
49
- })
50
- },
51
-
52
- notification: (title, message, config = {}) => toast.show({
53
- type: 'notification',
54
- title,
55
- message,
56
- duration: 0, // Las notificaciones no se auto-dismiss por defecto
57
- ...config
58
- }),
59
-
60
- process: (title, config = {}) => toast.show({
61
- type: 'process',
62
- title,
63
- progress: 0,
64
- progressLabel: 'Iniciando...',
65
- duration: 0, // Los procesos no se auto-dismiss
66
- ...config
67
- })
68
- }
26
+ const store = useToastStore()
27
+ return {
28
+ show: store.show.bind(store),
29
+ success: store.success.bind(store),
30
+ error: store.error.bind(store),
31
+ warning: store.warning.bind(store),
32
+ info: store.info.bind(store),
33
+ notify: store.notify.bind(store),
34
+ process: store.process.bind(store),
35
+ update: store.update.bind(store),
36
+ updateProgress: store.updateProgress.bind(store),
37
+ completeProcess: store.completeProcess.bind(store),
38
+ failProcess: store.failProcess.bind(store),
39
+ dismiss: store.dismiss.bind(store),
40
+ clear: store.clear.bind(store),
41
+ }
69
42
  }