@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,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
+ }
@@ -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,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
  }
@@ -0,0 +1,162 @@
1
+ import { ref, computed, watch } from 'vue'
2
+
3
+ /**
4
+ * useWorkflowForm — gestiona el estado de edición de una WorkflowDefinition.
5
+ *
6
+ * Responsabilidad: CRUD de steps y transitions sobre el JSON de configuración.
7
+ * No sabe nada de instancias, vistas ni API calls de runtime.
8
+ *
9
+ * @param {Object|null} initial — definición inicial { steps:[], transitions:[] }
10
+ */
11
+ export function useWorkflowForm(initial = null) {
12
+ const definition = ref(
13
+ initial
14
+ ? JSON.parse(JSON.stringify(initial))
15
+ : { steps: [], transitions: [] }
16
+ )
17
+
18
+ const isDirty = ref(false)
19
+ const saving = ref(false)
20
+
21
+ // Track changes — se activa después del primer tick para no marcar dirty en la carga
22
+ let _trackChanges = false
23
+ watch(definition, () => { if (_trackChanges) isDirty.value = true }, { deep: true })
24
+ setTimeout(() => { _trackChanges = true }, 0)
25
+
26
+ // ── Steps ──────────────────────────────────────────────────────────────────
27
+
28
+ const addStep = (step) => {
29
+ if (!step.key || !step.label || !step.type) throw new Error('Step requiere key, label y type')
30
+ if (definition.value.steps.some(s => s.key === step.key)) throw new Error(`Step '${step.key}' ya existe`)
31
+ definition.value.steps.push({ ...step })
32
+ }
33
+
34
+ const updateStep = (key, patch) => {
35
+ const idx = definition.value.steps.findIndex(s => s.key === key)
36
+ if (idx === -1) return
37
+ const newKey = patch.key ?? key
38
+ definition.value.steps[idx] = { ...definition.value.steps[idx], ...patch }
39
+ // Si cambió el key, actualizar todas las transiciones que lo referencian
40
+ if (newKey !== key) {
41
+ definition.value.transitions = definition.value.transitions.map(t => ({
42
+ ...t,
43
+ from: t.from === key ? newKey : t.from,
44
+ to: t.to === key ? newKey : t.to,
45
+ }))
46
+ }
47
+ }
48
+
49
+ const deleteStep = (key) => {
50
+ definition.value.steps = definition.value.steps.filter(s => s.key !== key)
51
+ definition.value.transitions = definition.value.transitions.filter(t => t.from !== key && t.to !== key)
52
+ }
53
+
54
+ const reorderSteps = (fromIdx, toIdx) => {
55
+ const steps = [...definition.value.steps]
56
+ const [moved] = steps.splice(fromIdx, 1)
57
+ steps.splice(toIdx, 0, moved)
58
+ definition.value.steps = steps
59
+ }
60
+
61
+ // ── Transitions ────────────────────────────────────────────────────────────
62
+
63
+ const addTransition = (transition) => {
64
+ if (!transition.from || !transition.to) throw new Error('Transition requiere from y to')
65
+ const exists = definition.value.transitions.some(
66
+ t => t.from === transition.from && t.to === transition.to
67
+ )
68
+ if (exists) throw new Error(`Transición ${transition.from} → ${transition.to} ya existe`)
69
+ definition.value.transitions.push({
70
+ from: transition.from,
71
+ to: transition.to,
72
+ label: transition.label ?? '',
73
+ description: transition.description ?? '',
74
+ restrictions: transition.restrictions ?? [],
75
+ })
76
+ }
77
+
78
+ const updateTransition = (from, to, patch) => {
79
+ const idx = definition.value.transitions.findIndex(t => t.from === from && t.to === to)
80
+ if (idx === -1) return
81
+ definition.value.transitions[idx] = { ...definition.value.transitions[idx], ...patch }
82
+ }
83
+
84
+ const deleteTransition = (from, to) => {
85
+ definition.value.transitions = definition.value.transitions.filter(
86
+ t => !(t.from === from && t.to === to)
87
+ )
88
+ }
89
+
90
+ // ── Restrictions helpers ───────────────────────────────────────────────────
91
+
92
+ const addRestriction = (from, to, restriction) => {
93
+ const t = definition.value.transitions.find(t => t.from === from && t.to === to)
94
+ if (!t) return
95
+ t.restrictions = [...(t.restrictions ?? []), { type: 'role', message: '', ...restriction }]
96
+ }
97
+
98
+ const removeRestriction = (from, to, idx) => {
99
+ const t = definition.value.transitions.find(t => t.from === from && t.to === to)
100
+ if (!t) return
101
+ t.restrictions = t.restrictions.filter((_, i) => i !== idx)
102
+ }
103
+
104
+ // ── Computed helpers ───────────────────────────────────────────────────────
105
+
106
+ const stepByKey = computed(() =>
107
+ Object.fromEntries((definition.value.steps ?? []).map(s => [s.key, s]))
108
+ )
109
+
110
+ const transitionsFrom = (key) =>
111
+ (definition.value.transitions ?? []).filter(t => t.from === key)
112
+
113
+ const transitionsTo = (key) =>
114
+ (definition.value.transitions ?? []).filter(t => t.to === key)
115
+
116
+ const isValid = computed(() => {
117
+ const steps = definition.value.steps ?? []
118
+ return (
119
+ steps.length >= 2 &&
120
+ steps.some(s => s.type === 'start') &&
121
+ steps.some(s => ['finished', 'cancelled'].includes(s.type))
122
+ )
123
+ })
124
+
125
+ // ── Reset / load ───────────────────────────────────────────────────────────
126
+
127
+ const reset = (newDefinition = null) => {
128
+ definition.value = newDefinition
129
+ ? JSON.parse(JSON.stringify(newDefinition))
130
+ : { steps: [], transitions: [] }
131
+ isDirty.value = false
132
+ }
133
+
134
+ const markSaved = () => { isDirty.value = false }
135
+
136
+ return {
137
+ // State
138
+ definition,
139
+ isDirty,
140
+ saving,
141
+ isValid,
142
+ // Step CRUD
143
+ addStep,
144
+ updateStep,
145
+ deleteStep,
146
+ reorderSteps,
147
+ // Transition CRUD
148
+ addTransition,
149
+ updateTransition,
150
+ deleteTransition,
151
+ // Restriction helpers
152
+ addRestriction,
153
+ removeRestriction,
154
+ // Computed
155
+ stepByKey,
156
+ transitionsFrom,
157
+ transitionsTo,
158
+ // Utils
159
+ reset,
160
+ markSaved,
161
+ }
162
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * useWorkflow — punto de entrada único del sistema de workflows.
3
+ *
4
+ * Estructura:
5
+ * useWorkflowForm → edición de definiciones (steps, transitions, restrictions)
6
+ * useWorkflowView → estado operacional (items, columnas, onMove, viewMode)
7
+ *
8
+ * Uso independiente:
9
+ * import { useWorkflowForm } from '~/composables/useWorkflow'
10
+ * import { useWorkflowView } from '~/composables/useWorkflow'
11
+ *
12
+ * Con auto-import de Nuxt también funcionan directamente:
13
+ * const form = useWorkflowForm(initialDefinition)
14
+ * const view = useWorkflowView(form.definition, { endpoint: 'tasks' })
15
+ */
16
+
17
+ export { useWorkflowForm } from './form.js'
18
+ export { useWorkflowView } from './view.js'