@innertia-solutions/innertia-nuxt 0.1.12 → 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 (51) hide show
  1. package/components/Attention.vue +269 -0
  2. package/components/Chart/index.vue +193 -0
  3. package/components/Container.vue +116 -0
  4. package/components/CopyButton.vue +98 -0
  5. package/components/Files/Card.vue +215 -0
  6. package/components/Files/Preview.vue +675 -0
  7. package/components/Files/Upload.vue +408 -0
  8. package/components/Files/UploadModal.vue +83 -0
  9. package/components/Forms/InputPin.vue +162 -0
  10. package/components/InfoToolbar.vue +54 -0
  11. package/components/Kanban/Standard.vue +588 -0
  12. package/components/Loader.vue +358 -0
  13. package/components/Modal/Base.vue +6 -0
  14. package/components/Modal/Confirm.vue +89 -0
  15. package/components/Modal/DeleteConfirm.vue +14 -31
  16. package/components/Modal/Form.vue +76 -0
  17. package/components/Modal/Host.vue +58 -0
  18. package/components/Modal/Prompt.vue +74 -0
  19. package/components/Modal.vue +119 -34
  20. package/components/Table/Standard.vue +198 -25
  21. package/components/Table/index.vue +169 -369
  22. package/components/Table/views/ViewCards.vue +74 -0
  23. package/components/Table/views/ViewDatabase.vue +236 -0
  24. package/components/Table/views/ViewTable.vue +299 -0
  25. package/components/Toast/Alert.vue +16 -111
  26. package/components/Toast/Container.vue +4 -29
  27. package/components/Toast/Notification.vue +8 -43
  28. package/components/Toast/Process.vue +8 -86
  29. package/components/Toast/Stack.vue +59 -0
  30. package/components/Toast/Standard.vue +115 -0
  31. package/components/Tree/Standard.vue +49 -4
  32. package/components/Workflow/Board.vue +356 -0
  33. package/components/Workflow/Builder.vue +454 -0
  34. package/components/Workflow/Form.vue +526 -0
  35. package/components/Workflow/Viewer.vue +225 -0
  36. package/components/Workflow/edges/TransitionEdge.vue +84 -0
  37. package/components/Workflow/nodes/StepNode.vue +88 -0
  38. package/composables/useClipboard.js +79 -0
  39. package/composables/useConfetti.js +194 -0
  40. package/composables/useFile.js +189 -0
  41. package/composables/useModal.js +71 -0
  42. package/composables/useToast.js +38 -65
  43. package/composables/useWorkflow/form.js +162 -0
  44. package/composables/useWorkflow/index.js +18 -0
  45. package/composables/useWorkflow/view.js +220 -0
  46. package/composables/useWorkflowLayout.js +162 -0
  47. package/css/tokens.css +10 -10
  48. package/nuxt.config.ts +1 -1
  49. package/package.json +13 -8
  50. package/stores/modal.js +48 -0
  51. package/stores/toast.js +156 -117
@@ -0,0 +1,220 @@
1
+ import { ref, computed, watch } from 'vue'
2
+
3
+ /**
4
+ * useWorkflowView — estado operacional de un workflow en runtime.
5
+ *
6
+ * Responsabilidad: cargar items, derivar columnas desde la definición,
7
+ * validar y ejecutar transiciones, manejar el view mode del board.
8
+ *
9
+ * NO edita la definición — eso es trabajo de useWorkflowForm.
10
+ *
11
+ * @param {Ref<Object>} definitionRef — ref reactivo a { steps, transitions }
12
+ * @param {Object} options
13
+ * @param {string} options.endpoint — endpoint para cargar items via useApi()
14
+ * @param {Object} options.params — params extra para el fetch
15
+ * @param {string} options.stateKey — campo del item que indica el step actual (default: 'current_step')
16
+ * @param {Function} options.onTransition — async (item, toStep, fromStep) => void — consumer llama su API
17
+ * @param {string} options.defaultView — 'kanban' | 'table' | 'flow' | 'funnel' (default: 'kanban')
18
+ * @param {boolean} options.validateTransitions — si false, permite cualquier movimiento (default: true)
19
+ */
20
+ export function useWorkflowView(definitionRef, options = {}) {
21
+ const {
22
+ endpoint = null,
23
+ params = {},
24
+ stateKey = 'current_step',
25
+ onTransition = null,
26
+ defaultView = 'kanban',
27
+ validateTransitions = true,
28
+ } = options
29
+
30
+ // ── Items ──────────────────────────────────────────────────────────────────
31
+
32
+ const items = ref([])
33
+ const loading = ref(false)
34
+ const error = ref(null)
35
+ const total = computed(() => items.value.length)
36
+
37
+ const api = useApi?.() ?? null // auto-import de Nuxt
38
+
39
+ const load = async () => {
40
+ if (!endpoint) return
41
+ loading.value = true
42
+ error.value = null
43
+ try {
44
+ const res = await api.get(endpoint, params)
45
+ items.value = Array.isArray(res) ? res : (res.data ?? [])
46
+ } catch (e) {
47
+ error.value = e?.message ?? 'Error al cargar'
48
+ } finally {
49
+ loading.value = false
50
+ }
51
+ }
52
+
53
+ const reload = () => load()
54
+
55
+ // Cargar al montar si hay endpoint
56
+ if (endpoint) {
57
+ // onMounted no está disponible aquí — el consumer debe llamar load() o
58
+ // lo hacemos con watch en el primer tick
59
+ watch(() => endpoint, load, { immediate: true })
60
+ }
61
+
62
+ // ── Columnas derivadas de la definición ────────────────────────────────────
63
+
64
+ const STEP_TYPE_COLOR = {
65
+ start: 'slate',
66
+ in_progress: 'blue',
67
+ pause_internal: 'amber',
68
+ pause_external: 'orange',
69
+ finished: 'green',
70
+ cancelled: 'red',
71
+ }
72
+
73
+ const columns = computed(() =>
74
+ (definitionRef.value?.steps ?? []).map(step => ({
75
+ key: step.key,
76
+ label: step.label,
77
+ color: STEP_TYPE_COLOR[step.type] ?? 'slate',
78
+ type: step.type,
79
+ }))
80
+ )
81
+
82
+ // ── Validación de transiciones ─────────────────────────────────────────────
83
+
84
+ /**
85
+ * Verifica si existe una transición definida entre dos steps.
86
+ */
87
+ const canTransition = (fromStep, toStep) => {
88
+ if (!validateTransitions) return true
89
+ return (definitionRef.value?.transitions ?? []).some(
90
+ t => t.from === fromStep && t.to === toStep
91
+ )
92
+ }
93
+
94
+ /**
95
+ * Devuelve los steps destino disponibles desde un step dado.
96
+ */
97
+ const availableTransitions = (fromStep) =>
98
+ (definitionRef.value?.transitions ?? [])
99
+ .filter(t => t.from === fromStep)
100
+ .map(t => ({
101
+ key: t.to,
102
+ label: definitionRef.value?.steps?.find(s => s.key === t.to)?.label ?? t.to,
103
+ transitionLabel: t.label ?? '',
104
+ restrictions: t.restrictions ?? [],
105
+ }))
106
+
107
+ /**
108
+ * Devuelve la config de una transición específica.
109
+ */
110
+ const getTransition = (fromStep, toStep) =>
111
+ (definitionRef.value?.transitions ?? []).find(
112
+ t => t.from === fromStep && t.to === toStep
113
+ ) ?? null
114
+
115
+ // ── onMove — para KanbanStandard ──────────────────────────────────────────
116
+
117
+ /**
118
+ * Función compatible con KanbanStandard :move-mutation.
119
+ * Valida la transición y llama al onTransition del consumer.
120
+ *
121
+ * @param {string|Object} itemOrId — id del item o el item completo
122
+ * @param {string} toStep — step destino
123
+ * @param {string} fromStep — step origen (opcional, se busca en items)
124
+ */
125
+ const onMove = async (itemOrId, toStep, fromStep) => {
126
+ const id = typeof itemOrId === 'object' ? itemOrId.id : itemOrId
127
+ const item = items.value.find(i => i.id === id) ?? itemOrId
128
+
129
+ const from = fromStep ?? item?.[stateKey]
130
+
131
+ if (validateTransitions && !canTransition(from, toStep)) {
132
+ console.warn(`[useWorkflowView] Transición bloqueada: ${from} → ${toStep}`)
133
+ return false
134
+ }
135
+
136
+ // Optimistic update
137
+ const idx = items.value.findIndex(i => i.id === id)
138
+ if (idx >= 0) items.value[idx] = { ...items.value[idx], [stateKey]: toStep }
139
+
140
+ try {
141
+ if (onTransition) await onTransition(item, toStep, from)
142
+ return true
143
+ } catch (e) {
144
+ // Rollback
145
+ if (idx >= 0) items.value[idx] = { ...items.value[idx], [stateKey]: from }
146
+ error.value = e?.message ?? 'Error en transición'
147
+ return false
148
+ }
149
+ }
150
+
151
+ // ── Funnel data — para WorkflowFunnel / Chart ──────────────────────────────
152
+
153
+ /**
154
+ * Cuenta de items por step — útil para la vista funnel.
155
+ */
156
+ const funnelData = computed(() => {
157
+ const counts = Object.fromEntries(
158
+ (definitionRef.value?.steps ?? []).map(s => [s.key, 0])
159
+ )
160
+ for (const item of items.value) {
161
+ const step = item[stateKey]
162
+ if (step in counts) counts[step]++
163
+ }
164
+ return (definitionRef.value?.steps ?? []).map(step => ({
165
+ key: step.key,
166
+ label: step.label,
167
+ type: step.type,
168
+ count: counts[step.key] ?? 0,
169
+ }))
170
+ })
171
+
172
+ // ── View mode ──────────────────────────────────────────────────────────────
173
+
174
+ const VALID_VIEWS = ['kanban', 'table', 'flow', 'funnel']
175
+
176
+ const viewMode = ref(VALID_VIEWS.includes(defaultView) ? defaultView : 'kanban')
177
+ const setViewMode = (mode) => {
178
+ if (VALID_VIEWS.includes(mode)) viewMode.value = mode
179
+ }
180
+
181
+ // ── Item helpers ───────────────────────────────────────────────────────────
182
+
183
+ const itemsByStep = computed(() => {
184
+ const map = Object.fromEntries(
185
+ (definitionRef.value?.steps ?? []).map(s => [s.key, []])
186
+ )
187
+ for (const item of items.value) {
188
+ const step = item[stateKey]
189
+ if (step in map) map[step].push(item)
190
+ }
191
+ return map
192
+ })
193
+
194
+ const setItems = (newItems) => { items.value = newItems }
195
+
196
+ return {
197
+ // Items
198
+ items,
199
+ loading,
200
+ error,
201
+ total,
202
+ load,
203
+ reload,
204
+ setItems,
205
+ itemsByStep,
206
+ // Columnas derivadas
207
+ columns,
208
+ stateKey,
209
+ // Transición
210
+ canTransition,
211
+ availableTransitions,
212
+ getTransition,
213
+ onMove,
214
+ // Funnel
215
+ funnelData,
216
+ // View mode
217
+ viewMode,
218
+ setViewMode,
219
+ }
220
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Auto-layout para workflow definitions.
3
+ *
4
+ * Usa BFS desde el nodo start (en vez de Kahn) para manejar ciclos
5
+ * correctamente. Back-edges (transiciones hacia nodos ya visitados con
6
+ * capa ≤ origen) se marcan como isBackEdge:true y usan handles top para
7
+ * que su path se rutee por encima del row de nodos.
8
+ */
9
+
10
+ const NODE_W = 208 // w-52 en Tailwind
11
+ const NODE_H = 96 // alto aproximado de StepNode con descripción
12
+ const H_GAP = 100 // espacio horizontal entre columnas (para labels de edge)
13
+ const V_GAP = 40 // espacio vertical entre nodos en misma columna
14
+
15
+ // ── Helpers privados ──────────────────────────────────────────────────────────
16
+
17
+ /**
18
+ * BFS desde el nodo start para asignar capa (layer) a cada step.
19
+ * Nodos no alcanzables se colocan en max_layer + 1.
20
+ */
21
+ function buildLayerMap(steps, transitions) {
22
+ if (!steps.length) return {}
23
+ const keys = steps.map(s => s.key)
24
+ const startKey = steps.find(s => s.type === 'start')?.key ?? keys[0]
25
+
26
+ // Adjacency list (solo forward)
27
+ const adj = Object.fromEntries(keys.map(k => [k, []]))
28
+ for (const t of transitions) {
29
+ if (adj[t.from] !== undefined) adj[t.from].push(t.to)
30
+ }
31
+
32
+ // BFS
33
+ const layer = {}
34
+ const queue = [startKey]
35
+ layer[startKey] = 0
36
+ let head = 0
37
+
38
+ while (head < queue.length) {
39
+ const node = queue[head++]
40
+ for (const nb of adj[node] ?? []) {
41
+ if (!(nb in layer)) {
42
+ layer[nb] = layer[node] + 1
43
+ queue.push(nb)
44
+ }
45
+ }
46
+ }
47
+
48
+ // Nodos desconectados → capa extra al final
49
+ const maxL = Math.max(-1, ...Object.values(layer))
50
+ for (const k of keys) {
51
+ if (!(k in layer)) layer[k] = maxL + 1
52
+ }
53
+
54
+ return layer
55
+ }
56
+
57
+ /**
58
+ * Convierte un layerMap en posiciones x/y.
59
+ * Cada capa es una columna; los nodos de una misma capa se centran verticalmente.
60
+ */
61
+ function computePositions(steps, layerMap) {
62
+ const groups = {}
63
+ for (const s of steps) {
64
+ const l = layerMap[s.key] ?? 0
65
+ if (!groups[l]) groups[l] = []
66
+ groups[l].push(s.key)
67
+ }
68
+
69
+ const positions = {}
70
+ for (const [l, group] of Object.entries(groups)) {
71
+ const col = parseInt(l)
72
+ const totalH = group.length * NODE_H + (group.length - 1) * V_GAP
73
+ const startY = -totalH / 2
74
+ for (let row = 0; row < group.length; row++) {
75
+ positions[group[row]] = {
76
+ x: col * (NODE_W + H_GAP),
77
+ y: startY + row * (NODE_H + V_GAP),
78
+ }
79
+ }
80
+ }
81
+
82
+ return positions
83
+ }
84
+
85
+ // ── API pública ───────────────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Calcula posiciones { x, y } para cada step.
89
+ * Exportado para uso directo en Builder (relayout).
90
+ *
91
+ * @param {Array} steps
92
+ * @param {Array} transitions
93
+ * @returns {Record<string, { x: number, y: number }>}
94
+ */
95
+ export function autoLayoutWorkflow(steps, transitions) {
96
+ const layer = buildLayerMap(steps, transitions)
97
+ return computePositions(steps, layer)
98
+ }
99
+
100
+ /**
101
+ * Convierte una workflow definition al formato nodes/edges de Vue Flow.
102
+ *
103
+ * Back-edges (transiciones que van hacia una capa ≤ origen) reciben:
104
+ * - data.isBackEdge = true
105
+ * - sourceHandle: 'top-source' / targetHandle: 'top-target'
106
+ * (StepNode expone esos handles invisibles en Position.Top)
107
+ */
108
+ export function definitionToFlow(config) {
109
+ const layers = buildLayerMap(config.steps ?? [], config.transitions ?? [])
110
+ const positions = computePositions(config.steps ?? [], layers)
111
+
112
+ const nodes = (config.steps ?? []).map(step => ({
113
+ id: step.key,
114
+ type: 'step',
115
+ position: step.position ?? positions[step.key] ?? { x: 0, y: 0 },
116
+ data: { ...step },
117
+ }))
118
+
119
+ const edges = (config.transitions ?? []).map(t => {
120
+ const fromL = layers[t.from] ?? 0
121
+ const toL = layers[t.to] ?? 0
122
+ const isBack = fromL >= toL
123
+
124
+ return {
125
+ id: `${t.from}__${t.to}`,
126
+ source: t.from,
127
+ target: t.to,
128
+ type: 'transition',
129
+ label: t.label ?? '',
130
+ data: { ...t, isBackEdge: isBack },
131
+ markerEnd: { type: 'arrowclosed' },
132
+ // Back-edges: rutear por los handles top para que el path arque por encima
133
+ ...(isBack ? { sourceHandle: 'top-source', targetHandle: 'top-target' } : {}),
134
+ }
135
+ })
136
+
137
+ return { nodes, edges }
138
+ }
139
+
140
+ /**
141
+ * Convierte nodes/edges de Vue Flow de vuelta al config JSON de Laravel.
142
+ * Filtra isBackEdge del data (es metadata de layout, no del dominio).
143
+ */
144
+ export function flowToDefinition(nodes, edges) {
145
+ const steps = nodes.map(n => ({
146
+ ...n.data,
147
+ position: { x: Math.round(n.position.x), y: Math.round(n.position.y) },
148
+ }))
149
+
150
+ const transitions = edges.map(e => {
151
+ // eslint-disable-next-line no-unused-vars
152
+ const { isBackEdge, ...rest } = e.data ?? {}
153
+ return {
154
+ ...rest,
155
+ from: e.source,
156
+ to: e.target,
157
+ label: e.label ?? rest.label ?? '',
158
+ }
159
+ })
160
+
161
+ return { steps, transitions }
162
+ }
package/css/tokens.css CHANGED
@@ -14,7 +14,10 @@
14
14
  * y solo redefinen estos tokens semánticos. NO redefinen primitivos.
15
15
  * ============================================================================ */
16
16
 
17
- @theme inline {
17
+ /* @theme (sin `inline`) → Tailwind genera `rounded-card`, `rounded-control`, etc.
18
+ * referenciando `var(--radius-card)` etc. en lugar de inline-ar el valor en
19
+ * build-time. Esto permite OVERRIDES en runtime (templates, playground toggles). */
20
+ @theme {
18
21
  /* ─── Radius ───────────────────────────────────────────────────────────
19
22
  * Categorización por *función*, no por tamaño. */
20
23
  --radius-control: var(--radius-md); /* botones, inputs, selects */
@@ -25,16 +28,13 @@
25
28
  --radius-badge: var(--radius-full); /* chips, tags, status pills */
26
29
  --radius-avatar: var(--radius-full); /* avatares circulares */
27
30
 
28
- /* ─── Fields (controles de formulario) ─────────────────────────────────
29
- * TODOS los inputs/selects/datepicker comparten estos tokens. Un template
30
- * puede redefinirlos una vez y todos los controles del producto cambian
31
- * coherentemente. */
32
- --field-height: 2.5rem; /* 40px */
33
- --field-height-sm: 2rem; /* 32px */
34
- --field-padding-x: 0.875rem; /* 14px */
31
+ /* ─── Fields (controles de formulario) ───────────────────────────────── */
32
+ --field-height: 2.5rem;
33
+ --field-height-sm: 2rem;
34
+ --field-padding-x: 0.875rem;
35
35
  --field-border-width: 1px;
36
- --field-font-size: 0.875rem; /* text-sm */
37
- --field-font-size-sm: 0.8125rem; /* text-[13px] */
36
+ --field-font-size: 0.875rem;
37
+ --field-font-size-sm: 0.8125rem;
38
38
  --field-bg: var(--color-card);
39
39
  --field-dropdown-bg: var(--field-bg);
40
40
  --field-border: var(--color-card-line);
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.12",
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
+ })