@innertia-solutions/innertia-nuxt 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Attention.vue +269 -0
- package/components/Auth/LoginForm.vue +1 -1
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Organization/Picker.vue +3 -3
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useAuth.js +1 -1
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useContext.js +53 -5
- package/composables/useFile.js +189 -0
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useModal.js +71 -0
- package/composables/useOrganization.ts +3 -3
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
- package/composables/useApp.ts +0 -46
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, markRaw, watch } from 'vue'
|
|
3
|
+
import { VueFlow, Panel } from '@vue-flow/core'
|
|
4
|
+
import { Background } from '@vue-flow/background'
|
|
5
|
+
import { Controls } from '@vue-flow/controls'
|
|
6
|
+
import StepNode from './nodes/StepNode.vue'
|
|
7
|
+
import TransitionEdge from './edges/TransitionEdge.vue'
|
|
8
|
+
import { definitionToFlow } from '~/composables/useWorkflowLayout.js'
|
|
9
|
+
|
|
10
|
+
// ── Props & emits ─────────────────────────────────────────────────────────────
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
/** WorkflowDefinition.config — { steps:[], transitions:[] } */
|
|
13
|
+
definition: { type: Object, required: true },
|
|
14
|
+
/** current_step key */
|
|
15
|
+
currentStep: { type: String, default: null },
|
|
16
|
+
/** array de step keys disponibles para transicionar */
|
|
17
|
+
availableTransitions: { type: Array, default: () => [] },
|
|
18
|
+
/** array de { from_step, to_step, performed_by, performed_at, notes } */
|
|
19
|
+
history: { type: Array, default: () => [] },
|
|
20
|
+
/** 'flow' | 'list' | 'history' */
|
|
21
|
+
defaultView: { type: String, default: 'flow' },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits(['transition'])
|
|
25
|
+
|
|
26
|
+
// ── Vue Flow types ────────────────────────────────────────────────────────────
|
|
27
|
+
const nodeTypes = markRaw({ step: StepNode })
|
|
28
|
+
const edgeTypes = markRaw({ transition: TransitionEdge })
|
|
29
|
+
|
|
30
|
+
// ── Reactive nodes/edges — re-calculados cuando cambia currentStep o definition
|
|
31
|
+
const flowNodes = ref([])
|
|
32
|
+
const flowEdges = ref([])
|
|
33
|
+
|
|
34
|
+
const rebuildFlow = () => {
|
|
35
|
+
const { nodes: n, edges: e } = definitionToFlow(props.definition)
|
|
36
|
+
|
|
37
|
+
flowNodes.value = n.map(node => ({
|
|
38
|
+
...node,
|
|
39
|
+
selectable: false,
|
|
40
|
+
draggable: false,
|
|
41
|
+
class: [
|
|
42
|
+
node.id === props.currentStep ? 'vf-current' : '',
|
|
43
|
+
props.availableTransitions.includes(node.id) ? 'vf-available' : '',
|
|
44
|
+
].filter(Boolean).join(' '),
|
|
45
|
+
data: {
|
|
46
|
+
...node.data,
|
|
47
|
+
isCurrent: node.id === props.currentStep,
|
|
48
|
+
isAvailable: props.availableTransitions.includes(node.id),
|
|
49
|
+
},
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
flowEdges.value = e
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
watch(
|
|
56
|
+
[() => props.definition, () => props.currentStep, () => props.availableTransitions],
|
|
57
|
+
rebuildFlow,
|
|
58
|
+
{ immediate: true, deep: true },
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// ── View mode ─────────────────────────────────────────────────────────────────
|
|
62
|
+
const activeView = ref(props.defaultView)
|
|
63
|
+
|
|
64
|
+
// ── List helpers ──────────────────────────────────────────────────────────────
|
|
65
|
+
const STEP_TYPE_LABELS = {
|
|
66
|
+
start: 'Inicio',
|
|
67
|
+
in_progress: 'En progreso',
|
|
68
|
+
pause_internal: 'Pausa interna',
|
|
69
|
+
pause_external: 'Pausa externa',
|
|
70
|
+
finished: 'Finalizado',
|
|
71
|
+
cancelled: 'Cancelado',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const stepsWithState = computed(() =>
|
|
75
|
+
(props.definition.steps ?? []).map(step => ({
|
|
76
|
+
...step,
|
|
77
|
+
isCurrent: step.key === props.currentStep,
|
|
78
|
+
isAvailable: props.availableTransitions.includes(step.key),
|
|
79
|
+
isPast: (props.history ?? []).some(h => h.to_step === step.key),
|
|
80
|
+
}))
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const formatDate = (iso) =>
|
|
84
|
+
iso
|
|
85
|
+
? new Date(iso).toLocaleString('es-CL', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })
|
|
86
|
+
: '—'
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<div class="flex flex-col h-full min-h-0">
|
|
91
|
+
|
|
92
|
+
<!-- Toolbar -->
|
|
93
|
+
<div class="flex items-center justify-between gap-3 px-3 py-2 border-b border-card-line bg-surface shrink-0">
|
|
94
|
+
<div class="inline-flex items-center rounded-control border border-card-line bg-card overflow-hidden">
|
|
95
|
+
<button
|
|
96
|
+
v-for="mode in [{ id: 'flow', label: 'Flujo' }, { id: 'list', label: 'Lista' }, { id: 'history', label: 'Historial' }]"
|
|
97
|
+
:key="mode.id"
|
|
98
|
+
type="button"
|
|
99
|
+
@click="activeView = mode.id"
|
|
100
|
+
:class="[
|
|
101
|
+
'px-3 py-1.5 text-xs font-medium transition-colors',
|
|
102
|
+
activeView === mode.id ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted-hover',
|
|
103
|
+
]"
|
|
104
|
+
>
|
|
105
|
+
{{ mode.label }}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
|
110
|
+
<span class="flex items-center gap-1.5">
|
|
111
|
+
<span class="w-2.5 h-2.5 rounded-full border-2 border-primary bg-primary/20" />
|
|
112
|
+
Actual
|
|
113
|
+
</span>
|
|
114
|
+
<span class="flex items-center gap-1.5">
|
|
115
|
+
<span class="w-2.5 h-2.5 rounded-full border-2 border-emerald-400 bg-emerald-50 dark:bg-emerald-900/30" />
|
|
116
|
+
Disponible
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Content -->
|
|
122
|
+
<div class="flex-1 min-h-0 relative overflow-hidden">
|
|
123
|
+
|
|
124
|
+
<!-- ── FLOW VIEW ──────────────────────────────────────────────────────── -->
|
|
125
|
+
<div v-if="activeView === 'flow'" class="h-full">
|
|
126
|
+
<VueFlow
|
|
127
|
+
:nodes="flowNodes"
|
|
128
|
+
:edges="flowEdges"
|
|
129
|
+
:node-types="nodeTypes"
|
|
130
|
+
:edge-types="edgeTypes"
|
|
131
|
+
:nodes-draggable="false"
|
|
132
|
+
:nodes-connectable="false"
|
|
133
|
+
:elements-selectable="false"
|
|
134
|
+
fit-view-on-init
|
|
135
|
+
class="h-full bg-background"
|
|
136
|
+
>
|
|
137
|
+
<Background pattern-color="var(--color-card-line)" :gap="20" />
|
|
138
|
+
<Controls :show-interactive="false" />
|
|
139
|
+
|
|
140
|
+
<Panel position="bottom-center">
|
|
141
|
+
<div v-if="availableTransitions.length" class="flex items-center gap-2 flex-wrap justify-center pb-2">
|
|
142
|
+
<button
|
|
143
|
+
v-for="stepKey in availableTransitions"
|
|
144
|
+
:key="stepKey"
|
|
145
|
+
type="button"
|
|
146
|
+
@click="emit('transition', stepKey)"
|
|
147
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary-hover rounded-control shadow transition-colors"
|
|
148
|
+
>
|
|
149
|
+
Ir a {{ definition.steps?.find(s => s.key === stepKey)?.label ?? stepKey }}
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</Panel>
|
|
153
|
+
</VueFlow>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<!-- ── LIST VIEW ──────────────────────────────────────────────────────── -->
|
|
157
|
+
<div v-if="activeView === 'list'" class="h-full overflow-y-auto p-4">
|
|
158
|
+
<div class="relative pl-6 space-y-1 before:absolute before:left-[7px] before:top-3 before:bottom-3 before:w-px before:bg-card-line">
|
|
159
|
+
<div
|
|
160
|
+
v-for="step in stepsWithState"
|
|
161
|
+
:key="step.key"
|
|
162
|
+
:class="[
|
|
163
|
+
'relative flex items-start gap-3 pl-4 py-3 rounded-card transition-colors',
|
|
164
|
+
step.isCurrent ? 'bg-primary/5 border border-primary/20' : '',
|
|
165
|
+
step.isAvailable && !step.isCurrent ? 'bg-emerald-50/50 dark:bg-emerald-900/10 border border-emerald-400/30' : '',
|
|
166
|
+
!step.isCurrent && !step.isAvailable ? 'opacity-60' : '',
|
|
167
|
+
]"
|
|
168
|
+
>
|
|
169
|
+
<span
|
|
170
|
+
:class="[
|
|
171
|
+
'absolute -left-[3px] top-4 w-3 h-3 rounded-full border-2 shrink-0',
|
|
172
|
+
step.isCurrent ? 'border-primary bg-primary' : '',
|
|
173
|
+
step.isAvailable && !step.isCurrent ? 'border-emerald-400 bg-emerald-50 dark:bg-emerald-900/50' : '',
|
|
174
|
+
!step.isCurrent && !step.isAvailable ? 'border-card-line bg-background' : '',
|
|
175
|
+
]"
|
|
176
|
+
/>
|
|
177
|
+
<div class="flex-1 min-w-0">
|
|
178
|
+
<p class="text-sm font-medium text-foreground">{{ step.label }}</p>
|
|
179
|
+
<p class="text-xs text-muted-foreground font-mono">{{ step.key }} · {{ STEP_TYPE_LABELS[step.type] }}</p>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
182
|
+
<span v-if="step.isCurrent" class="text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded-badge bg-primary/10 text-primary">
|
|
183
|
+
Actual
|
|
184
|
+
</span>
|
|
185
|
+
<button
|
|
186
|
+
v-if="step.isAvailable && !step.isCurrent"
|
|
187
|
+
type="button"
|
|
188
|
+
@click="emit('transition', step.key)"
|
|
189
|
+
class="text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-badge bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/20 transition-colors"
|
|
190
|
+
>
|
|
191
|
+
Transicionar
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- ── HISTORY ────────────────────────────────────────────────────────── -->
|
|
199
|
+
<div v-if="activeView === 'history'" class="h-full overflow-y-auto p-4">
|
|
200
|
+
<div v-if="history.length" class="space-y-2">
|
|
201
|
+
<div
|
|
202
|
+
v-for="(entry, idx) in [...history].reverse()"
|
|
203
|
+
:key="idx"
|
|
204
|
+
class="flex items-start gap-3 bg-card rounded-card border border-card-line px-4 py-3"
|
|
205
|
+
>
|
|
206
|
+
<div class="w-2 h-2 rounded-full bg-primary/60 shrink-0 mt-1.5" />
|
|
207
|
+
<div class="flex-1 min-w-0">
|
|
208
|
+
<p class="text-xs text-foreground">
|
|
209
|
+
<span class="font-mono text-muted-foreground">{{ entry.from_step }}</span>
|
|
210
|
+
<span class="mx-1.5 text-muted-foreground-2">→</span>
|
|
211
|
+
<span class="font-mono font-medium">{{ entry.to_step }}</span>
|
|
212
|
+
</p>
|
|
213
|
+
<p v-if="entry.notes" class="text-xs text-muted-foreground mt-0.5 italic">{{ entry.notes }}</p>
|
|
214
|
+
</div>
|
|
215
|
+
<p class="text-[11px] text-muted-foreground shrink-0">{{ formatDate(entry.performed_at) }}</p>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
<div v-else class="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
|
219
|
+
Sin historial de transiciones
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@vue-flow/core'
|
|
4
|
+
|
|
5
|
+
defineOptions({ inheritAttrs: false })
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
id: String,
|
|
9
|
+
sourceX: Number,
|
|
10
|
+
sourceY: Number,
|
|
11
|
+
targetX: Number,
|
|
12
|
+
targetY: Number,
|
|
13
|
+
sourcePosition: String,
|
|
14
|
+
targetPosition: String,
|
|
15
|
+
data: { type: Object, default: () => ({}) },
|
|
16
|
+
label: { type: String, default: '' },
|
|
17
|
+
selected: { type: Boolean, default: false },
|
|
18
|
+
markerEnd: String,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// ── Path reactivo — recalculado cuando cambian las posiciones ─────────────────
|
|
22
|
+
const pathData = computed(() =>
|
|
23
|
+
getBezierPath({
|
|
24
|
+
sourceX: props.sourceX,
|
|
25
|
+
sourceY: props.sourceY,
|
|
26
|
+
sourcePosition: props.sourcePosition,
|
|
27
|
+
targetX: props.targetX,
|
|
28
|
+
targetY: props.targetY,
|
|
29
|
+
targetPosition: props.targetPosition,
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const edgePath = computed(() => pathData.value[0])
|
|
34
|
+
const labelX = computed(() => pathData.value[1])
|
|
35
|
+
const labelY = computed(() => pathData.value[2])
|
|
36
|
+
|
|
37
|
+
const hasRestrictions = computed(() => (props.data?.restrictions?.length ?? 0) > 0)
|
|
38
|
+
const displayLabel = computed(() => props.label || props.data?.label || '')
|
|
39
|
+
const isBackEdge = computed(() => props.data?.isBackEdge === true)
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<BaseEdge
|
|
44
|
+
:id="id"
|
|
45
|
+
:path="edgePath"
|
|
46
|
+
:marker-end="markerEnd"
|
|
47
|
+
:style="{
|
|
48
|
+
stroke: selected ? 'var(--color-primary)' : isBackEdge ? 'var(--color-muted-foreground)' : 'var(--color-card-line)',
|
|
49
|
+
strokeWidth: selected ? 2 : 1.5,
|
|
50
|
+
strokeDasharray: isBackEdge ? '5 3' : 'none',
|
|
51
|
+
opacity: isBackEdge && !selected ? 0.65 : 1,
|
|
52
|
+
}"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<EdgeLabelRenderer v-if="displayLabel || hasRestrictions">
|
|
56
|
+
<div
|
|
57
|
+
:style="{
|
|
58
|
+
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
|
59
|
+
pointerEvents: 'all',
|
|
60
|
+
}"
|
|
61
|
+
class="absolute nodrag nopan"
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
:class="[
|
|
65
|
+
'flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium border transition-colors',
|
|
66
|
+
selected
|
|
67
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
68
|
+
: 'bg-card text-foreground border-card-line hover:border-primary/50',
|
|
69
|
+
]"
|
|
70
|
+
>
|
|
71
|
+
<span v-if="displayLabel">{{ displayLabel }}</span>
|
|
72
|
+
<span
|
|
73
|
+
v-if="hasRestrictions"
|
|
74
|
+
:class="[
|
|
75
|
+
'inline-flex items-center justify-center w-4 h-4 rounded-full text-[9px] font-bold',
|
|
76
|
+
selected ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-muted text-muted-foreground',
|
|
77
|
+
]"
|
|
78
|
+
>
|
|
79
|
+
{{ data.restrictions.length }}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</EdgeLabelRenderer>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { Handle, Position } from '@vue-flow/core'
|
|
4
|
+
|
|
5
|
+
defineOptions({ inheritAttrs: false })
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
data: { type: Object, required: true },
|
|
9
|
+
selected: { type: Boolean, default: false },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const STEP_CONFIG = {
|
|
13
|
+
start: { label: 'Inicio', color: 'emerald', dot: 'bg-emerald-500', border: 'border-emerald-400 dark:border-emerald-600', bg: 'bg-emerald-50 dark:bg-emerald-900/30' },
|
|
14
|
+
in_progress: { label: 'En progreso', color: 'blue', dot: 'bg-blue-500', border: 'border-blue-400 dark:border-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/30' },
|
|
15
|
+
pause_internal: { label: 'Pausa int.', color: 'amber', dot: 'bg-amber-500', border: 'border-amber-400 dark:border-amber-600', bg: 'bg-amber-50 dark:bg-amber-900/30' },
|
|
16
|
+
pause_external: { label: 'Pausa ext.', color: 'orange', dot: 'bg-orange-500', border: 'border-orange-400 dark:border-orange-600', bg: 'bg-orange-50 dark:bg-orange-900/30' },
|
|
17
|
+
finished: { label: 'Finalizado', color: 'violet', dot: 'bg-violet-500', border: 'border-violet-400 dark:border-violet-600', bg: 'bg-violet-50 dark:bg-violet-900/30' },
|
|
18
|
+
cancelled: { label: 'Cancelado', color: 'red', dot: 'bg-red-500', border: 'border-red-400 dark:border-red-600', bg: 'bg-red-50 dark:bg-red-900/30' },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cfg = computed(() => STEP_CONFIG[props.data.type] ?? STEP_CONFIG.in_progress)
|
|
22
|
+
const isStart = computed(() => props.data.type === 'start')
|
|
23
|
+
const isTerminal = computed(() => ['finished', 'cancelled'].includes(props.data.type))
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div
|
|
28
|
+
:class="[
|
|
29
|
+
'relative flex flex-col gap-1 rounded-lg border-2 px-3 py-2.5 w-52 bg-card shadow-sm transition-shadow',
|
|
30
|
+
cfg.border,
|
|
31
|
+
selected ? 'shadow-md ring-2 ring-primary/50 ring-offset-1' : 'hover:shadow-md',
|
|
32
|
+
]"
|
|
33
|
+
>
|
|
34
|
+
<!-- Handles invisibles en Top — para rutear back-edges por encima del nodo -->
|
|
35
|
+
<Handle
|
|
36
|
+
id="top-target"
|
|
37
|
+
type="target"
|
|
38
|
+
:position="Position.Top"
|
|
39
|
+
style="opacity:0; pointer-events:none; width:6px; height:6px;"
|
|
40
|
+
/>
|
|
41
|
+
<Handle
|
|
42
|
+
id="top-source"
|
|
43
|
+
type="source"
|
|
44
|
+
:position="Position.Top"
|
|
45
|
+
style="opacity:0; pointer-events:none; width:6px; height:6px;"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<!-- Target handle (no mostrar para nodo start) -->
|
|
49
|
+
<Handle
|
|
50
|
+
v-if="!isStart"
|
|
51
|
+
type="target"
|
|
52
|
+
:position="Position.Left"
|
|
53
|
+
class="!bg-card-line !border-card-line !w-3 !h-3 hover:!bg-primary transition-colors"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<!-- Header: dot + type badge -->
|
|
57
|
+
<div class="flex items-center gap-2">
|
|
58
|
+
<span :class="['w-2 h-2 rounded-full shrink-0', cfg.dot]" />
|
|
59
|
+
<span
|
|
60
|
+
:class="[
|
|
61
|
+
'text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded-full',
|
|
62
|
+
cfg.bg, 'text-foreground',
|
|
63
|
+
]"
|
|
64
|
+
>
|
|
65
|
+
{{ cfg.label }}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- Label -->
|
|
70
|
+
<p class="text-sm font-medium text-foreground leading-tight truncate">{{ data.label }}</p>
|
|
71
|
+
|
|
72
|
+
<!-- Key -->
|
|
73
|
+
<p class="text-[11px] text-muted-foreground font-mono truncate">{{ data.key }}</p>
|
|
74
|
+
|
|
75
|
+
<!-- Description (if present) -->
|
|
76
|
+
<p v-if="data.description" class="text-[11px] text-muted-foreground leading-tight line-clamp-2 mt-0.5">
|
|
77
|
+
{{ data.description }}
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
<!-- Source handle (no mostrar para terminales) -->
|
|
81
|
+
<Handle
|
|
82
|
+
v-if="!isTerminal"
|
|
83
|
+
type="source"
|
|
84
|
+
:position="Position.Right"
|
|
85
|
+
class="!bg-card-line !border-card-line !w-3 !h-3 hover:!bg-primary transition-colors"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
package/composables/useAuth.js
CHANGED
|
@@ -14,7 +14,7 @@ export function useAuth() {
|
|
|
14
14
|
*/
|
|
15
15
|
async function performLogin(context, email, password, remember = false) {
|
|
16
16
|
authStore.rememberUser = remember
|
|
17
|
-
const data = await api.post(`${context}/auth/login`, { email, password,
|
|
17
|
+
const data = await api.post(`${context}/auth/login`, { email, password, context })
|
|
18
18
|
authStore.saveToken(data.token ?? data.access_token)
|
|
19
19
|
authStore.setCurrentContext(context)
|
|
20
20
|
queryClient.clear()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useClipboard — Wrapper sobre navigator.clipboard con estado reactivo
|
|
3
|
+
* `copied` que vuelve a false después de `timeout` ms.
|
|
4
|
+
*
|
|
5
|
+
* Hace fallback a un truco con `document.execCommand('copy')` cuando la
|
|
6
|
+
* Clipboard API no está disponible (HTTP, iframes restringidos, etc.).
|
|
7
|
+
*
|
|
8
|
+
* ```js
|
|
9
|
+
* const { copy, copied, supported } = useClipboard({ timeout: 1500 })
|
|
10
|
+
*
|
|
11
|
+
* <button @click="copy('hola mundo')">
|
|
12
|
+
* {{ copied ? '¡Copiado!' : 'Copiar' }}
|
|
13
|
+
* </button>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* También expone `text` para usar como modelo si querés bindear automáticamente
|
|
17
|
+
* un valor que cambia (ej. un input controlado).
|
|
18
|
+
*/
|
|
19
|
+
export function useClipboard(options = {}) {
|
|
20
|
+
const timeout = options.timeout ?? 1500
|
|
21
|
+
|
|
22
|
+
const text = ref('')
|
|
23
|
+
const copied = ref(false)
|
|
24
|
+
const error = ref(null)
|
|
25
|
+
const supported = computed(() => {
|
|
26
|
+
if (typeof navigator === 'undefined') return false
|
|
27
|
+
return !!(navigator.clipboard && navigator.clipboard.writeText)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
let timeoutId = null
|
|
31
|
+
|
|
32
|
+
const _resetSoon = () => {
|
|
33
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
34
|
+
timeoutId = setTimeout(() => {
|
|
35
|
+
copied.value = false
|
|
36
|
+
error.value = null
|
|
37
|
+
}, timeout)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const _legacy = (value) => {
|
|
41
|
+
if (typeof document === 'undefined') return false
|
|
42
|
+
const el = document.createElement('textarea')
|
|
43
|
+
el.value = value
|
|
44
|
+
el.setAttribute('readonly', '')
|
|
45
|
+
el.style.position = 'fixed'
|
|
46
|
+
el.style.opacity = '0'
|
|
47
|
+
document.body.appendChild(el)
|
|
48
|
+
el.select()
|
|
49
|
+
let ok = false
|
|
50
|
+
try { ok = document.execCommand('copy') } catch (_) { ok = false }
|
|
51
|
+
document.body.removeChild(el)
|
|
52
|
+
return ok
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Copia el string al clipboard. Retorna Promise<boolean>. */
|
|
56
|
+
const copy = async (value) => {
|
|
57
|
+
const str = String(value ?? '')
|
|
58
|
+
text.value = str
|
|
59
|
+
error.value = null
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (supported.value) {
|
|
63
|
+
await navigator.clipboard.writeText(str)
|
|
64
|
+
} else {
|
|
65
|
+
const ok = _legacy(str)
|
|
66
|
+
if (!ok) throw new Error('No se pudo copiar')
|
|
67
|
+
}
|
|
68
|
+
copied.value = true
|
|
69
|
+
_resetSoon()
|
|
70
|
+
return true
|
|
71
|
+
} catch (e) {
|
|
72
|
+
error.value = e?.message ?? 'Error al copiar'
|
|
73
|
+
copied.value = false
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { copy, copied, error, text, supported }
|
|
79
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useConfetti — Animación de confetti zero-deps en canvas overlay.
|
|
3
|
+
*
|
|
4
|
+
* ```js
|
|
5
|
+
* const { burst, fireworks, cannon, schoolPride, stop } = useConfetti()
|
|
6
|
+
*
|
|
7
|
+
* burst() // explosión simple en el centro
|
|
8
|
+
* burst({ origin: { x: 0.5, y: 0.3 } }) // explosión desde un punto (0-1)
|
|
9
|
+
* burst({ colors: ['#ff0', '#0f0'] }) // colores custom
|
|
10
|
+
* fireworks({ duration: 3000 }) // fuegos artificiales 3s
|
|
11
|
+
* cannon({ angle: 60, spread: 55 }) // un solo "cañon"
|
|
12
|
+
* schoolPride(['#ff0066', '#33ccff']) // dos cañones laterales
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* El canvas se monta una sola vez en <body>, full-screen, pointer-events: none,
|
|
16
|
+
* z-index altísimo. Se limpia automáticamente cuando no hay partículas.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const DEFAULT_COLORS = ['#FF3E7F', '#FFC23C', '#3CDBFF', '#7B61FF', '#27D67B', '#FF7A45']
|
|
20
|
+
const GRAVITY = 0.18
|
|
21
|
+
const DRAG = 0.012
|
|
22
|
+
|
|
23
|
+
let canvas = null
|
|
24
|
+
let ctx = null
|
|
25
|
+
let particles = []
|
|
26
|
+
let rafId = null
|
|
27
|
+
let resizeHandler = null
|
|
28
|
+
|
|
29
|
+
function ensureCanvas() {
|
|
30
|
+
if (typeof window === 'undefined') return null
|
|
31
|
+
if (canvas) return canvas
|
|
32
|
+
canvas = document.createElement('canvas')
|
|
33
|
+
canvas.setAttribute('data-innertia-confetti', '')
|
|
34
|
+
Object.assign(canvas.style, {
|
|
35
|
+
position: 'fixed',
|
|
36
|
+
inset: '0',
|
|
37
|
+
width: '100%',
|
|
38
|
+
height: '100%',
|
|
39
|
+
pointerEvents: 'none',
|
|
40
|
+
zIndex: '99999',
|
|
41
|
+
})
|
|
42
|
+
document.body.appendChild(canvas)
|
|
43
|
+
ctx = canvas.getContext('2d')
|
|
44
|
+
resize()
|
|
45
|
+
resizeHandler = () => resize()
|
|
46
|
+
window.addEventListener('resize', resizeHandler)
|
|
47
|
+
return canvas
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resize() {
|
|
51
|
+
if (!canvas) return
|
|
52
|
+
const dpr = window.devicePixelRatio || 1
|
|
53
|
+
canvas.width = window.innerWidth * dpr
|
|
54
|
+
canvas.height = window.innerHeight * dpr
|
|
55
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function destroyCanvas() {
|
|
59
|
+
if (!canvas) return
|
|
60
|
+
canvas.remove()
|
|
61
|
+
canvas = null
|
|
62
|
+
ctx = null
|
|
63
|
+
if (resizeHandler) window.removeEventListener('resize', resizeHandler)
|
|
64
|
+
resizeHandler = null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function spawnParticles({
|
|
68
|
+
origin = { x: 0.5, y: 0.5 },
|
|
69
|
+
count = 80,
|
|
70
|
+
spread = 70,
|
|
71
|
+
angle = 90, // 90 = arriba, 0 = derecha, 180 = izquierda
|
|
72
|
+
velocity = 18,
|
|
73
|
+
colors = DEFAULT_COLORS,
|
|
74
|
+
shapes = ['square', 'circle', 'rect'],
|
|
75
|
+
ticks = 200, // vida en frames
|
|
76
|
+
scalar = 1,
|
|
77
|
+
} = {}) {
|
|
78
|
+
ensureCanvas()
|
|
79
|
+
if (!ctx) return
|
|
80
|
+
|
|
81
|
+
const radSpread = (spread * Math.PI) / 180
|
|
82
|
+
const radAngle = (angle * Math.PI) / 180
|
|
83
|
+
const ox = origin.x * window.innerWidth
|
|
84
|
+
const oy = origin.y * window.innerHeight
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < count; i++) {
|
|
87
|
+
const a = radAngle - radSpread / 2 + Math.random() * radSpread
|
|
88
|
+
const v = velocity * (0.5 + Math.random() * 0.7)
|
|
89
|
+
particles.push({
|
|
90
|
+
x: ox,
|
|
91
|
+
y: oy,
|
|
92
|
+
vx: Math.cos(a) * v,
|
|
93
|
+
vy: -Math.sin(a) * v,
|
|
94
|
+
rot: Math.random() * Math.PI * 2,
|
|
95
|
+
vrot: (Math.random() - 0.5) * 0.3,
|
|
96
|
+
size: (5 + Math.random() * 6) * scalar,
|
|
97
|
+
color: colors[(Math.random() * colors.length) | 0],
|
|
98
|
+
shape: shapes[(Math.random() * shapes.length) | 0],
|
|
99
|
+
life: 0,
|
|
100
|
+
maxLife: ticks * (0.7 + Math.random() * 0.6),
|
|
101
|
+
wobble: Math.random() * 2 * Math.PI,
|
|
102
|
+
wobbleSpeed: 0.05 + Math.random() * 0.05,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!rafId) tick()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function tick() {
|
|
110
|
+
if (!ctx) { rafId = null; return }
|
|
111
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
112
|
+
|
|
113
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
114
|
+
const p = particles[i]
|
|
115
|
+
p.vx *= 1 - DRAG
|
|
116
|
+
p.vy = p.vy * (1 - DRAG) + GRAVITY
|
|
117
|
+
p.x += p.vx
|
|
118
|
+
p.y += p.vy
|
|
119
|
+
p.rot += p.vrot
|
|
120
|
+
p.wobble += p.wobbleSpeed
|
|
121
|
+
p.life++
|
|
122
|
+
|
|
123
|
+
if (p.life >= p.maxLife || p.y > window.innerHeight + 40) {
|
|
124
|
+
particles.splice(i, 1)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const alpha = Math.max(0, 1 - p.life / p.maxLife)
|
|
129
|
+
ctx.save()
|
|
130
|
+
ctx.globalAlpha = alpha
|
|
131
|
+
ctx.translate(p.x + Math.cos(p.wobble) * 2, p.y)
|
|
132
|
+
ctx.rotate(p.rot)
|
|
133
|
+
ctx.fillStyle = p.color
|
|
134
|
+
|
|
135
|
+
if (p.shape === 'circle') {
|
|
136
|
+
ctx.beginPath()
|
|
137
|
+
ctx.arc(0, 0, p.size / 2, 0, Math.PI * 2)
|
|
138
|
+
ctx.fill()
|
|
139
|
+
} else if (p.shape === 'rect') {
|
|
140
|
+
ctx.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2)
|
|
141
|
+
} else {
|
|
142
|
+
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size)
|
|
143
|
+
}
|
|
144
|
+
ctx.restore()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (particles.length) {
|
|
148
|
+
rafId = requestAnimationFrame(tick)
|
|
149
|
+
} else {
|
|
150
|
+
rafId = null
|
|
151
|
+
// Mantener el canvas montado — limpiarlo es opcional. Lo dejamos para no
|
|
152
|
+
// recrear elementos en bursts repetidos.
|
|
153
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function useConfetti() {
|
|
158
|
+
/** Explosión simple. */
|
|
159
|
+
const burst = (opts = {}) => spawnParticles({ count: 90, spread: 70, velocity: 22, ...opts })
|
|
160
|
+
|
|
161
|
+
/** Un "cañón" direccional (ej. desde una esquina). */
|
|
162
|
+
const cannon = (opts = {}) => spawnParticles({ count: 60, spread: 30, velocity: 25, ...opts })
|
|
163
|
+
|
|
164
|
+
/** Dos cañones laterales con colores propios. */
|
|
165
|
+
const schoolPride = (colors = DEFAULT_COLORS, opts = {}) => {
|
|
166
|
+
spawnParticles({ origin: { x: 0, y: 0.7 }, angle: 60, spread: 55, count: 50, colors, velocity: 25, ...opts })
|
|
167
|
+
spawnParticles({ origin: { x: 1, y: 0.7 }, angle: 120, spread: 55, count: 50, colors, velocity: 25, ...opts })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Fuegos artificiales repetidos durante `duration` ms. */
|
|
171
|
+
const fireworks = ({ duration = 3000, interval = 250, ...opts } = {}) => {
|
|
172
|
+
const end = Date.now() + duration
|
|
173
|
+
const iv = setInterval(() => {
|
|
174
|
+
if (Date.now() >= end) { clearInterval(iv); return }
|
|
175
|
+
spawnParticles({
|
|
176
|
+
origin: { x: 0.2 + Math.random() * 0.6, y: 0.3 + Math.random() * 0.3 },
|
|
177
|
+
count: 40,
|
|
178
|
+
spread: 360,
|
|
179
|
+
velocity: 18,
|
|
180
|
+
...opts,
|
|
181
|
+
})
|
|
182
|
+
}, interval)
|
|
183
|
+
return () => clearInterval(iv)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Limpia todas las partículas activas y destruye el canvas. */
|
|
187
|
+
const stop = () => {
|
|
188
|
+
particles = []
|
|
189
|
+
if (rafId) { cancelAnimationFrame(rafId); rafId = null }
|
|
190
|
+
destroyCanvas()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { burst, cannon, schoolPride, fireworks, stop }
|
|
194
|
+
}
|