@intranefr/superbackend 1.6.6 → 1.7.7

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/.env.example +4 -0
  2. package/README.md +18 -0
  3. package/package.json +8 -2
  4. package/public/js/admin-superdemos.js +396 -0
  5. package/public/sdk/superdemos.iife.js +614 -0
  6. package/public/superdemos-qa.html +324 -0
  7. package/sdk/superdemos/browser/src/index.js +719 -0
  8. package/src/cli/agent-chat.js +369 -0
  9. package/src/cli/agent-list.js +42 -0
  10. package/src/controllers/adminAgentsChat.controller.js +172 -0
  11. package/src/controllers/adminSuperDemos.controller.js +382 -0
  12. package/src/controllers/superDemosPublic.controller.js +126 -0
  13. package/src/middleware.js +102 -19
  14. package/src/models/BlogAutomationLock.js +4 -4
  15. package/src/models/BlogPost.js +16 -16
  16. package/src/models/CacheEntry.js +17 -6
  17. package/src/models/JsonConfig.js +2 -4
  18. package/src/models/RateLimitMetricBucket.js +10 -5
  19. package/src/models/SuperDemo.js +38 -0
  20. package/src/models/SuperDemoProject.js +32 -0
  21. package/src/models/SuperDemoStep.js +27 -0
  22. package/src/routes/adminAgents.routes.js +10 -0
  23. package/src/routes/adminMarkdowns.routes.js +3 -0
  24. package/src/routes/adminSuperDemos.routes.js +31 -0
  25. package/src/routes/superDemos.routes.js +9 -0
  26. package/src/services/auditLogger.js +75 -37
  27. package/src/services/email.service.js +18 -3
  28. package/src/services/superDemosAuthoringSessions.service.js +132 -0
  29. package/src/services/superDemosWs.service.js +164 -0
  30. package/src/services/terminalsWs.service.js +35 -3
  31. package/src/utils/rbac/rightsRegistry.js +2 -0
  32. package/views/admin-agents.ejs +261 -11
  33. package/views/admin-dashboard.ejs +78 -8
  34. package/views/admin-superdemos.ejs +335 -0
  35. package/views/admin-terminals.ejs +462 -34
  36. package/views/partials/admin/agents-chat.ejs +80 -0
  37. package/views/partials/dashboard/nav-items.ejs +1 -0
  38. package/views/partials/dashboard/tab-bar.ejs +6 -0
  39. package/cookies.txt +0 -6
  40. package/cookies1.txt +0 -6
  41. package/cookies2.txt +0 -6
  42. package/cookies3.txt +0 -6
  43. package/cookies4.txt +0 -5
  44. package/cookies_old.txt +0 -5
  45. package/cookies_old_test.txt +0 -6
  46. package/cookies_super.txt +0 -5
  47. package/cookies_super_test.txt +0 -6
  48. package/cookies_test.txt +0 -6
  49. package/test-access.js +0 -63
  50. package/test-iframe-fix.html +0 -63
  51. package/test-iframe.html +0 -14
@@ -0,0 +1,335 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>SuperDemos — Admin</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
9
+ <style>[v-cloak]{display:none}</style>
10
+ </head>
11
+ <body class="bg-gray-50 text-gray-900">
12
+ <div id="app" v-cloak class="min-h-screen">
13
+
14
+ <!-- Toast -->
15
+ <div v-if="toast.show" class="fixed top-4 right-4 z-50 pointer-events-none">
16
+ <div :class="['px-4 py-3 rounded-lg shadow-lg text-sm font-medium flex items-center gap-2', toast.type === 'error' ? 'bg-red-600 text-white' : 'bg-green-600 text-white']">
17
+ <span>{{ toast.type === 'error' ? '✕' : '✓' }}</span>
18
+ {{ toast.message }}
19
+ </div>
20
+ </div>
21
+
22
+ <!-- Navbar -->
23
+ <header class="bg-white border-b border-gray-200 sticky top-0 z-40">
24
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between gap-4">
25
+ <div class="flex items-center gap-3">
26
+ <div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white text-xs font-bold select-none">SD</div>
27
+ <div>
28
+ <h1 class="text-base font-bold text-gray-900 leading-tight">SuperDemos</h1>
29
+ <p class="text-[11px] text-gray-500 leading-tight">Interactive demo authoring</p>
30
+ </div>
31
+ </div>
32
+ <div class="flex items-center gap-2">
33
+ <a :href="qaPageUrl" target="_blank" rel="noreferrer" class="text-xs px-3 py-1.5 rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium">QA Playground ↗</a>
34
+ <button @click="refreshAll" class="text-xs px-3 py-1.5 rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium">↻ Refresh</button>
35
+ </div>
36
+ </div>
37
+ </header>
38
+
39
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-5 space-y-4">
40
+
41
+ <!-- SDK snippet (collapsible) -->
42
+ <details class="bg-white rounded-xl border border-gray-200 shadow-sm">
43
+ <summary class="px-4 py-3 cursor-pointer text-sm font-semibold text-gray-700 flex items-center gap-2 select-none list-none">
44
+ <span class="text-gray-400">&#123;&#125;</span> SDK embed snippet
45
+ <span class="ml-auto text-xs font-normal text-gray-400">click to expand</span>
46
+ </summary>
47
+ <div class="px-4 pb-4 pt-1">
48
+ <p class="text-xs text-gray-500 mb-2">Paste in your external app to enable live + author modes.</p>
49
+ <pre class="text-[11px] bg-gray-50 border border-gray-200 rounded-lg p-3 whitespace-pre-wrap break-words"><code>&lt;script src="{{ origin }}<%= baseUrl %>/public/sdk/superdemos.iife.js"&gt;&lt;/script&gt;
50
+ &lt;script&gt;
51
+ SuperDemos.init({
52
+ projectId: 'sdp_yourproject',
53
+ apiUrl: '{{ origin }}<%= baseUrl %>',
54
+ apiKey: null, // required if private project
55
+ mode: 'live',
56
+ });
57
+ &lt;/script&gt;</code></pre>
58
+ </div>
59
+ </details>
60
+
61
+ <!-- Context breadcrumb (visible once a project is selected) -->
62
+ <div v-if="selectedProject" class="bg-blue-50 border border-blue-200 rounded-xl px-4 py-2.5 flex items-center gap-2 text-sm flex-wrap">
63
+ <span class="text-blue-500 font-medium">Project</span>
64
+ <span class="font-semibold text-blue-900">{{ selectedProject.name }}</span>
65
+ <button @click="copyText(selectedProject.projectId, 'Project ID')" class="text-[11px] px-2 py-0.5 rounded-md bg-blue-100 hover:bg-blue-200 text-blue-700 font-mono">{{ selectedProject.projectId }} ⧉</button>
66
+ <span :class="['text-[10px] px-2 py-0.5 rounded-full font-medium', selectedProject.isPublic ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700']">{{ selectedProject.isPublic ? 'public' : 'private' }}</span>
67
+ <template v-if="selectedDemo">
68
+ <span class="text-blue-300 mx-0.5">›</span>
69
+ <span class="text-blue-500 font-medium">Demo</span>
70
+ <span class="font-semibold text-blue-900">{{ selectedDemo.name }}</span>
71
+ <button @click="copyText(selectedDemo.demoId, 'Demo ID')" class="text-[11px] px-2 py-0.5 rounded-md bg-blue-100 hover:bg-blue-200 text-blue-700 font-mono">{{ selectedDemo.demoId }} ⧉</button>
72
+ <span :class="['text-[10px] px-2 py-0.5 rounded-full font-medium', selectedDemo.status === 'published' ? 'bg-green-100 text-green-700' : 'bg-gray-200 text-gray-600']">{{ selectedDemo.status }}</span>
73
+ </template>
74
+ <template v-else>
75
+ <span class="text-blue-300 mx-0.5">›</span>
76
+ <span class="text-xs text-blue-400">select a demo</span>
77
+ </template>
78
+ </div>
79
+
80
+ <!-- API key banner (shown once after project creation) -->
81
+ <div v-if="lastGeneratedKey" class="bg-amber-50 border border-amber-300 rounded-xl px-4 py-3 flex items-start gap-3">
82
+ <span class="text-amber-500 text-xl leading-none mt-0.5">&#128273;</span>
83
+ <div class="flex-1 min-w-0">
84
+ <div class="font-semibold text-amber-900 text-sm">New project API key — copy now, it won't be shown again</div>
85
+ <div class="font-mono text-xs mt-1 break-all text-amber-800">{{ lastGeneratedKey }}</div>
86
+ </div>
87
+ <button @click="copyText(lastGeneratedKey, 'API key')" class="flex-shrink-0 text-xs px-3 py-1.5 rounded-md bg-amber-200 hover:bg-amber-300 text-amber-900 font-medium">Copy</button>
88
+ </div>
89
+
90
+ <!-- Three-column main workspace -->
91
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
92
+
93
+ <!-- ===== PROJECTS PANEL ===== -->
94
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm flex flex-col">
95
+ <div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
96
+ <h2 class="font-semibold text-gray-800 flex items-center gap-2">
97
+ Projects
98
+ <span class="text-[11px] bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded-full">{{ projects.length }}</span>
99
+ </h2>
100
+ </div>
101
+
102
+ <!-- New project form (collapsible) -->
103
+ <div class="px-4 py-3 border-b border-gray-100">
104
+ <details>
105
+ <summary class="cursor-pointer text-xs font-medium text-blue-600 hover:text-blue-700 select-none list-none">+ New project</summary>
106
+ <div class="mt-3 space-y-2">
107
+ <input v-model="newProject.name" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" placeholder="Project name" />
108
+ <input v-model="newProject.projectId" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" placeholder="projectId (optional, sdp_...)" />
109
+ <label class="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
110
+ <input type="checkbox" v-model="newProject.isPublic" class="rounded" />
111
+ Public (no API key required)
112
+ </label>
113
+ <button @click="createProject" class="w-full bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-2 rounded-lg font-medium">Create project</button>
114
+ </div>
115
+ </details>
116
+ </div>
117
+
118
+ <!-- Project list (divs, not nested buttons) -->
119
+ <div class="flex-1 overflow-hidden">
120
+ <div class="p-3 space-y-1 max-h-[360px] overflow-y-auto">
121
+ <div v-if="!projects.length" class="text-center py-8 text-sm text-gray-400">No projects yet</div>
122
+ <div
123
+ v-for="(p, pIdx) in (projects || [])"
124
+ :key="(p && p.projectId) || ('p-' + pIdx)"
125
+ @click="selectProject(p)"
126
+ :class="['group rounded-lg border px-3 py-2.5 cursor-pointer transition-all', selectedProject && p && selectedProject.projectId === p.projectId ? 'bg-blue-50 border-blue-300' : 'bg-white border-gray-200 hover:border-gray-300 hover:bg-gray-50']"
127
+ >
128
+ <div class="flex items-start justify-between gap-2">
129
+ <div class="font-medium text-gray-900 text-sm truncate">{{ (p && p.name) || 'Unnamed' }}</div>
130
+ <span :class="['text-[10px] px-1.5 py-0.5 rounded-full font-medium flex-shrink-0', (p && p.isPublic) ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700']">{{ (p && p.isPublic) ? 'public' : 'private' }}</span>
131
+ </div>
132
+ <div class="flex items-center gap-2 mt-1">
133
+ <span class="text-[11px] text-gray-400 font-mono truncate flex-1">{{ (p && p.projectId) || '—' }}</span>
134
+ <button @click.stop="copyText(p && p.projectId, 'Project ID')" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 hover:bg-gray-200 text-gray-600 opacity-0 group-hover:opacity-100 flex-shrink-0 transition-opacity">⧉ Copy ID</button>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Style settings (shown when a project is selected) -->
141
+ <div v-if="selectedProject" class="border-t border-gray-100 p-4 space-y-2 bg-gray-50 rounded-b-xl">
142
+ <div class="text-[11px] font-semibold text-gray-500 uppercase tracking-wide">Style settings</div>
143
+ <select v-model="projectStylePreset" class="w-full border border-gray-200 rounded-lg px-2 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-blue-300">
144
+ <option value="default">default</option>
145
+ <option value="glass-dark">glass-dark</option>
146
+ <option value="high-contrast">high-contrast</option>
147
+ <option value="soft-purple">soft-purple</option>
148
+ </select>
149
+ <textarea v-model="projectStyleOverrides" rows="3" class="w-full border border-gray-200 rounded-lg px-2 py-2 text-xs font-mono bg-white focus:outline-none focus:ring-2 focus:ring-blue-300 resize-none" placeholder="Custom CSS override (optional)"></textarea>
150
+ <button @click="saveProjectStyleSettings" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-3 py-2 rounded-lg font-medium">Save style</button>
151
+ </div>
152
+ </div>
153
+
154
+ <!-- ===== DEMOS PANEL ===== -->
155
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm flex flex-col">
156
+ <div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
157
+ <h2 class="font-semibold text-gray-800 flex items-center gap-2">
158
+ Demos
159
+ <span v-if="selectedProject" class="text-[11px] bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded-full">{{ demos.length }}</span>
160
+ </h2>
161
+ </div>
162
+
163
+ <div v-if="!selectedProject" class="flex-1 flex items-center justify-center p-8">
164
+ <div class="text-center text-sm text-gray-400 space-y-2">
165
+ <div class="text-3xl text-gray-200">&#8592;</div>
166
+ <div>Select a project first</div>
167
+ </div>
168
+ </div>
169
+
170
+ <template v-if="selectedProject">
171
+ <!-- New demo form (collapsible) -->
172
+ <div class="px-4 py-3 border-b border-gray-100">
173
+ <details>
174
+ <summary class="cursor-pointer text-xs font-medium text-blue-600 hover:text-blue-700 select-none list-none">+ New demo</summary>
175
+ <div class="mt-3 space-y-2">
176
+ <input v-model="newDemo.name" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" placeholder="Demo name" />
177
+ <input v-model="newDemo.startUrlPattern" class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" placeholder="startUrlPattern (optional)" />
178
+ <button @click="createDemo" class="w-full bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-2 rounded-lg font-medium">Create demo</button>
179
+ </div>
180
+ </details>
181
+ </div>
182
+
183
+ <!-- Demo list (divs, not nested buttons) -->
184
+ <div class="flex-1 overflow-hidden">
185
+ <div class="p-3 space-y-1 max-h-[420px] overflow-y-auto">
186
+ <div v-if="!demos.length" class="text-center py-8 text-sm text-gray-400">No demos yet</div>
187
+ <div
188
+ v-for="(d, dIdx) in (demos || [])"
189
+ :key="(d && d.demoId) || ('d-' + dIdx)"
190
+ @click="selectDemo(d)"
191
+ :class="['group rounded-lg border px-3 py-2.5 cursor-pointer transition-all', selectedDemo && d && selectedDemo.demoId === d.demoId ? 'bg-blue-50 border-blue-300' : 'bg-white border-gray-200 hover:border-gray-300 hover:bg-gray-50']"
192
+ >
193
+ <div class="flex items-start justify-between gap-2">
194
+ <div class="font-medium text-gray-900 text-sm truncate">{{ (d && d.name) || 'Unnamed' }}</div>
195
+ <div class="flex items-center gap-1 flex-shrink-0">
196
+ <span v-if="d && d.publishedVersion" class="text-[10px] text-gray-400 font-mono">v{{ d.publishedVersion }}</span>
197
+ <span :class="['text-[10px] px-1.5 py-0.5 rounded-full font-medium', (d && d.status) === 'published' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500']">{{ (d && d.status) || 'draft' }}</span>
198
+ </div>
199
+ </div>
200
+ <div class="flex items-center gap-2 mt-1">
201
+ <span class="text-[11px] text-gray-400 font-mono truncate flex-1">{{ (d && d.demoId) || '—' }}</span>
202
+ <button @click.stop="copyText(d && d.demoId, 'Demo ID')" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 hover:bg-gray-200 text-gray-600 opacity-0 group-hover:opacity-100 flex-shrink-0 transition-opacity">⧉ Copy ID</button>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Publish action -->
209
+ <div v-if="selectedDemo" class="border-t border-gray-100 p-4 bg-gray-50 rounded-b-xl">
210
+ <button @click="publishDemo" class="w-full bg-green-600 hover:bg-green-700 text-white text-sm px-3 py-2 rounded-lg font-medium flex items-center justify-center gap-2">
211
+ &#9650; Publish demo
212
+ </button>
213
+ </div>
214
+ </template>
215
+ </div>
216
+
217
+ <!-- ===== STEPS & AUTHORING PANEL ===== -->
218
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm flex flex-col">
219
+ <div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
220
+ <h2 class="font-semibold text-gray-800 flex items-center gap-2">
221
+ Steps & Authoring
222
+ <span v-if="selectedDemo" class="text-[11px] bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded-full">{{ steps.length }}</span>
223
+ </h2>
224
+ <button v-if="selectedDemo" @click="saveSteps" class="text-xs px-3 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium">Save steps</button>
225
+ </div>
226
+
227
+ <div v-if="!selectedDemo" class="flex-1 flex items-center justify-center p-8">
228
+ <div class="text-center text-sm text-gray-400 space-y-2">
229
+ <div class="text-3xl text-gray-200">&#8592;</div>
230
+ <div>Select a demo first</div>
231
+ </div>
232
+ </div>
233
+
234
+ <div v-if="selectedDemo" class="flex-1 overflow-y-auto p-4 space-y-4">
235
+
236
+ <!-- Authoring session box -->
237
+ <div class="border border-gray-200 rounded-xl overflow-hidden">
238
+ <div class="px-3 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
239
+ <div class="text-xs font-semibold text-gray-700">Authoring session</div>
240
+ <div class="flex items-center gap-1.5">
241
+ <span :class="['w-2 h-2 rounded-full', authoring.wsStatus === 'connected' ? 'bg-green-500' : authoring.wsStatus === 'connecting' ? 'bg-amber-400' : authoring.wsStatus === 'error' ? 'bg-red-500' : 'bg-gray-300']"></span>
242
+ <span class="text-[10px] text-gray-500 font-mono">{{ authoring.wsStatus }}</span>
243
+ </div>
244
+ </div>
245
+ <div class="p-3 space-y-2">
246
+ <div class="flex gap-2">
247
+ <input v-model="authoring.targetUrl" class="flex-1 min-w-0 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-300" placeholder="Target URL" />
248
+ <button @click="setQaTargetUrl" class="flex-shrink-0 text-xs px-2.5 py-2 rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-600 font-medium">QA</button>
249
+ </div>
250
+ <button @click="startAuthoring" class="w-full bg-purple-600 hover:bg-purple-700 text-white text-sm px-3 py-2 rounded-lg font-medium">Start authoring session</button>
251
+ <div v-if="authoring.connectUrl" class="text-xs bg-purple-50 border border-purple-200 rounded-lg p-2.5 space-y-1">
252
+ <div class="font-semibold text-purple-900">Open this URL in your target app:</div>
253
+ <a class="text-purple-700 underline break-all block" :href="authoring.connectUrl" target="_blank" rel="noreferrer">{{ authoring.connectUrl }}</a>
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ <!-- Selected element box -->
259
+ <div class="border border-gray-200 rounded-xl overflow-hidden">
260
+ <div class="px-3 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
261
+ <div class="text-xs font-semibold text-gray-700">Selected element</div>
262
+ <button
263
+ @click="addStepFromSelection"
264
+ :disabled="!lastSelection"
265
+ :class="['text-xs px-2.5 py-1 rounded-lg font-medium transition-colors', lastSelection ? 'bg-gray-900 hover:bg-gray-700 text-white' : 'bg-gray-100 text-gray-400 cursor-not-allowed']"
266
+ >+ Add step</button>
267
+ </div>
268
+ <div class="p-3">
269
+ <div v-if="lastSelection" class="text-xs font-mono bg-gray-50 border border-gray-200 rounded-lg p-2 break-all text-gray-700">{{ lastSelection.selector }}</div>
270
+ <div v-else class="text-xs text-gray-400 text-center py-2">Click an element in the target app to select it.</div>
271
+ </div>
272
+ </div>
273
+
274
+ <!-- Steps list -->
275
+ <div v-if="steps.length" class="space-y-2">
276
+ <div v-for="(s, idx) in steps" :key="idx" class="border border-gray-200 rounded-xl overflow-hidden">
277
+ <div class="px-3 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
278
+ <span class="text-xs font-semibold text-gray-600">Step {{ idx + 1 }}</span>
279
+ <div class="flex items-center gap-1">
280
+ <button @click="moveStep(idx, -1)" :disabled="idx === 0" :class="['text-xs w-6 h-6 rounded flex items-center justify-center', idx === 0 ? 'text-gray-300' : 'text-gray-500 hover:bg-gray-200']">&#8593;</button>
281
+ <button @click="moveStep(idx, 1)" :disabled="idx === steps.length - 1" :class="['text-xs w-6 h-6 rounded flex items-center justify-center', idx === steps.length - 1 ? 'text-gray-300' : 'text-gray-500 hover:bg-gray-200']">&#8595;</button>
282
+ <button @click="previewStep(s)" class="text-[10px] px-2 py-1 rounded-md bg-purple-100 hover:bg-purple-200 text-purple-700 font-medium ml-1">Preview</button>
283
+ <button @click="removeStep(idx)" class="text-[10px] px-2 py-1 rounded-md bg-red-100 hover:bg-red-200 text-red-700 font-medium">Remove</button>
284
+ </div>
285
+ </div>
286
+ <div class="p-3 space-y-2">
287
+ <input v-model="s.selector" class="w-full border border-gray-200 rounded-lg px-2 py-1.5 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-blue-300" placeholder="CSS selector" />
288
+ <input v-model="s.message" class="w-full border border-gray-200 rounded-lg px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" placeholder="Step message" />
289
+ <div class="grid grid-cols-2 gap-2">
290
+ <div>
291
+ <label class="text-[10px] text-gray-400 mb-0.5 block">Placement</label>
292
+ <select v-model="s.placement" class="w-full border border-gray-200 rounded-lg px-2 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-300">
293
+ <option value="auto">auto</option>
294
+ <option value="top">top</option>
295
+ <option value="bottom">bottom</option>
296
+ <option value="left">left</option>
297
+ <option value="right">right</option>
298
+ </select>
299
+ </div>
300
+ <div>
301
+ <label class="text-[10px] text-gray-400 mb-0.5 block">Advance</label>
302
+ <select v-model="s.advance.type" class="w-full border border-gray-200 rounded-lg px-2 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-300">
303
+ <option value="manualNext">Click Next</option>
304
+ <option value="clickTarget">Click target</option>
305
+ <option value="delayMs">Auto (delay)</option>
306
+ </select>
307
+ </div>
308
+ </div>
309
+ <div v-if="s.advance.type === 'delayMs'">
310
+ <input v-model.number="s.advance.ms" type="number" class="w-full border border-gray-200 rounded-lg px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300" placeholder="Delay in ms" />
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </div>
315
+
316
+ <div v-if="!steps.length" class="text-center py-5 text-xs text-gray-400 border border-dashed border-gray-200 rounded-xl">
317
+ No steps yet — start an authoring session and click elements to add steps.
318
+ </div>
319
+
320
+ </div>
321
+ </div>
322
+
323
+ </div>
324
+ </div>
325
+ </div>
326
+
327
+ <script>
328
+ window.__adminSuperDemosConfig = {
329
+ baseUrl: '<%= baseUrl %>',
330
+ adminPath: '<%= adminPath %>',
331
+ };
332
+ </script>
333
+ <script src="<%= baseUrl %>/public/js/admin-superdemos.js"></script>
334
+ </body>
335
+ </html>