@startsimpli/ui 0.4.22 → 0.4.23

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.
@@ -0,0 +1,608 @@
1
+ 'use client';
2
+
3
+ import { useState, type ReactNode } from 'react';
4
+ import {
5
+ Zap,
6
+ Globe,
7
+ MousePointerClick,
8
+ Cpu,
9
+ Workflow,
10
+ Sparkles,
11
+ Loader2,
12
+ } from 'lucide-react';
13
+ import { Button } from '../ui/button';
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from '../ui/dialog';
21
+ import { Card, CardContent } from '../ui/card';
22
+ import { Badge } from '../ui/badge';
23
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
24
+
25
+ // ── Mini workflow diagram (ported inline — small + pure) ──
26
+
27
+ type DiagramColor =
28
+ | 'blue'
29
+ | 'orange'
30
+ | 'purple'
31
+ | 'green'
32
+ | 'gray'
33
+ | 'indigo'
34
+ | 'cyan'
35
+ | 'emerald'
36
+ | 'teal';
37
+
38
+ interface DiagramNode {
39
+ x: number;
40
+ y: number;
41
+ color: DiagramColor;
42
+ }
43
+
44
+ interface DiagramConnection {
45
+ from: number;
46
+ to: number;
47
+ }
48
+
49
+ interface MiniWorkflowDiagramProps {
50
+ nodes: DiagramNode[];
51
+ connections: DiagramConnection[];
52
+ isLoading?: boolean;
53
+ }
54
+
55
+ const colorMap: Record<DiagramColor, string> = {
56
+ blue: 'fill-blue-500/60',
57
+ orange: 'fill-orange-500/60',
58
+ purple: 'fill-purple-500/60',
59
+ green: 'fill-green-500/60',
60
+ gray: 'fill-gray-500/60',
61
+ indigo: 'fill-indigo-500/60',
62
+ cyan: 'fill-cyan-500/60',
63
+ emerald: 'fill-emerald-500/60',
64
+ teal: 'fill-teal-500/60',
65
+ };
66
+
67
+ function MiniWorkflowDiagram({
68
+ nodes,
69
+ connections,
70
+ isLoading = false,
71
+ }: MiniWorkflowDiagramProps) {
72
+ return (
73
+ <div className="relative w-full h-20 bg-muted/50 rounded-md overflow-hidden">
74
+ {isLoading ? (
75
+ <div className="absolute inset-0 flex items-center justify-center">
76
+ <Loader2 className="w-6 h-6 animate-spin text-primary" />
77
+ </div>
78
+ ) : (
79
+ <svg
80
+ width="188"
81
+ height="80"
82
+ viewBox="0 0 188 80"
83
+ className="w-full h-full"
84
+ role="img"
85
+ aria-label="Workflow diagram preview"
86
+ >
87
+ {/* Connection lines */}
88
+ <g className="text-muted-foreground/30">
89
+ {connections.map((conn, index) => {
90
+ const fromNode = nodes[conn.from];
91
+ const toNode = nodes[conn.to];
92
+ return (
93
+ <line
94
+ key={index}
95
+ x1={fromNode.x + 12}
96
+ y1={fromNode.y + 12}
97
+ x2={toNode.x + 12}
98
+ y2={toNode.y + 12}
99
+ stroke="currentColor"
100
+ strokeWidth="1.5"
101
+ />
102
+ );
103
+ })}
104
+ </g>
105
+
106
+ {/* Nodes */}
107
+ {nodes.map((node, index) => (
108
+ <rect
109
+ key={index}
110
+ x={node.x}
111
+ y={node.y}
112
+ width="24"
113
+ height="24"
114
+ rx="4"
115
+ className={colorMap[node.color]}
116
+ />
117
+ ))}
118
+ </svg>
119
+ )}
120
+ </div>
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Predefined diagram patterns for common workflow templates.
126
+ * Matches real backend workflow definitions.
127
+ */
128
+ const DiagramPatterns = {
129
+ // E2E Pipeline: trigger → build → deploy → run_suite (4 nodes linear)
130
+ e2ePipeline: {
131
+ nodes: [
132
+ { x: 20, y: 28, color: 'purple' as const },
133
+ { x: 67, y: 28, color: 'green' as const },
134
+ { x: 114, y: 28, color: 'green' as const },
135
+ { x: 161, y: 28, color: 'indigo' as const },
136
+ ],
137
+ connections: [
138
+ { from: 0, to: 1 },
139
+ { from: 1, to: 2 },
140
+ { from: 2, to: 3 },
141
+ ],
142
+ },
143
+
144
+ // Crawler: trigger → setup → crawl → kg_import → bootstrap → teardown (6 nodes)
145
+ crawler: {
146
+ nodes: [
147
+ { x: 5, y: 28, color: 'purple' as const },
148
+ { x: 38, y: 28, color: 'blue' as const },
149
+ { x: 71, y: 28, color: 'cyan' as const },
150
+ { x: 104, y: 28, color: 'emerald' as const },
151
+ { x: 137, y: 28, color: 'emerald' as const },
152
+ { x: 170, y: 28, color: 'blue' as const },
153
+ ],
154
+ connections: [
155
+ { from: 0, to: 1 },
156
+ { from: 1, to: 2 },
157
+ { from: 2, to: 3 },
158
+ { from: 3, to: 4 },
159
+ { from: 4, to: 5 },
160
+ ],
161
+ },
162
+
163
+ // E2E Test Run: trigger → setup → execute_task → teardown (4 nodes)
164
+ e2eTestRun: {
165
+ nodes: [
166
+ { x: 20, y: 28, color: 'purple' as const },
167
+ { x: 67, y: 28, color: 'blue' as const },
168
+ { x: 114, y: 28, color: 'cyan' as const },
169
+ { x: 161, y: 28, color: 'blue' as const },
170
+ ],
171
+ connections: [
172
+ { from: 0, to: 1 },
173
+ { from: 1, to: 2 },
174
+ { from: 2, to: 3 },
175
+ ],
176
+ },
177
+
178
+ // Agent Run: trigger → setup → execute_task → evaluate → teardown (5 nodes)
179
+ agentRun: {
180
+ nodes: [
181
+ { x: 10, y: 28, color: 'purple' as const },
182
+ { x: 47, y: 28, color: 'blue' as const },
183
+ { x: 84, y: 28, color: 'cyan' as const },
184
+ { x: 121, y: 28, color: 'teal' as const },
185
+ { x: 158, y: 28, color: 'blue' as const },
186
+ ],
187
+ connections: [
188
+ { from: 0, to: 1 },
189
+ { from: 1, to: 2 },
190
+ { from: 2, to: 3 },
191
+ { from: 3, to: 4 },
192
+ ],
193
+ },
194
+
195
+ // App Evaluation: trigger → setup → execute_task → teardown (4 nodes)
196
+ appEvaluation: {
197
+ nodes: [
198
+ { x: 20, y: 28, color: 'purple' as const },
199
+ { x: 67, y: 28, color: 'blue' as const },
200
+ { x: 114, y: 28, color: 'cyan' as const },
201
+ { x: 161, y: 28, color: 'blue' as const },
202
+ ],
203
+ connections: [
204
+ { from: 0, to: 1 },
205
+ { from: 1, to: 2 },
206
+ { from: 2, to: 3 },
207
+ ],
208
+ },
209
+
210
+ // Raw Crawl: trigger → setup → crawl → kg_import → teardown (5 nodes)
211
+ rawCrawl: {
212
+ nodes: [
213
+ { x: 10, y: 28, color: 'purple' as const },
214
+ { x: 50, y: 28, color: 'blue' as const },
215
+ { x: 90, y: 28, color: 'cyan' as const },
216
+ { x: 130, y: 28, color: 'emerald' as const },
217
+ { x: 170, y: 28, color: 'blue' as const },
218
+ ],
219
+ connections: [
220
+ { from: 0, to: 1 },
221
+ { from: 1, to: 2 },
222
+ { from: 2, to: 3 },
223
+ { from: 3, to: 4 },
224
+ ],
225
+ },
226
+ };
227
+
228
+ // ── Template model ──
229
+
230
+ /**
231
+ * Opaque workflow definition carried by a template. Kept loose so this
232
+ * gallery stays free of app workflow-detail type coupling; the consumer
233
+ * receives it verbatim via {@link WorkflowTemplateGalleryProps.onSelectTemplate}.
234
+ */
235
+ export type WorkflowTemplateDefinition = Record<string, unknown>;
236
+
237
+ export interface WorkflowTemplate {
238
+ id: string;
239
+ name: string;
240
+ description: string;
241
+ category: 'pipeline' | 'browser' | 'orchestration' | 'testing';
242
+ nodeCount: number;
243
+ complexity: 'beginner' | 'intermediate' | 'advanced';
244
+ icon: ReactNode;
245
+ useCase: string;
246
+ teaches: string[];
247
+ workflow: WorkflowTemplateDefinition;
248
+ diagramPattern: keyof typeof DiagramPatterns;
249
+ }
250
+
251
+ /**
252
+ * Default pre-built workflow templates. Consumers may override the set via
253
+ * {@link WorkflowTemplateGalleryProps.templates}.
254
+ */
255
+ export const WORKFLOW_TEMPLATES: WorkflowTemplate[] = [
256
+ {
257
+ id: 'e2e-pipeline',
258
+ name: 'E2E Pipeline',
259
+ description: 'Full CI/CD pipeline: build, deploy, then run E2E test suite',
260
+ category: 'pipeline',
261
+ nodeCount: 4,
262
+ complexity: 'advanced',
263
+ icon: <Workflow className="h-5 w-5" />,
264
+ useCase: 'End-to-end CI/CD testing pipeline',
265
+ teaches: [
266
+ 'Event triggers',
267
+ 'Container builds',
268
+ 'Deployments',
269
+ 'Test orchestration',
270
+ ],
271
+ diagramPattern: 'e2ePipeline',
272
+ workflow: {
273
+ name: 'E2E Pipeline Template',
274
+ description: 'Build, deploy, and run E2E tests triggered by pipeline events',
275
+ nodes: [
276
+ { id: 'node-1', type: 'trigger.event', name: 'Pipeline Trigger', parameters: { event: 'e2e_pipeline_requested' }, position: { x: 100, y: 200 } },
277
+ { id: 'node-2', type: 'pipeline.build', name: 'Build Container Image', parameters: {}, position: { x: 400, y: 200 } },
278
+ { id: 'node-3', type: 'pipeline.deploy', name: 'Deploy Container', parameters: {}, position: { x: 700, y: 200 } },
279
+ { id: 'node-4', type: 'e2e.run_suite', name: 'Run E2E Test Suite', parameters: {}, position: { x: 1000, y: 200 } },
280
+ ],
281
+ connections: {
282
+ 'node-1': { main: [[{ node: 'node-2', type: 'main', index: 0 }]] },
283
+ 'node-2': { main: [[{ node: 'node-3', type: 'main', index: 0 }]] },
284
+ 'node-3': { main: [[{ node: 'node-4', type: 'main', index: 0 }]] },
285
+ },
286
+ },
287
+ },
288
+ {
289
+ id: 'crawler',
290
+ name: 'Crawler Workflow',
291
+ description: 'Crawl a web app, build knowledge graph, and bootstrap E2E tests',
292
+ category: 'browser',
293
+ nodeCount: 6,
294
+ complexity: 'advanced',
295
+ icon: <Globe className="h-5 w-5" />,
296
+ useCase: 'Discover app pages and auto-generate tests',
297
+ teaches: [
298
+ 'Browser sessions',
299
+ 'Surfer crawling',
300
+ 'Knowledge graphs',
301
+ 'Test generation',
302
+ ],
303
+ diagramPattern: 'crawler',
304
+ workflow: {
305
+ name: 'Crawler Workflow Template',
306
+ description: 'Crawl application, import knowledge graph, and bootstrap E2E tests',
307
+ nodes: [
308
+ { id: 'node-1', type: 'trigger.event', name: 'Crawl Trigger', parameters: { event: 'crawl_requested' }, position: { x: 100, y: 200 } },
309
+ { id: 'node-2', type: 'browser.setup', name: 'Setup Browser', parameters: { headless: true, viewport_width: 1280, viewport_height: 720 }, position: { x: 350, y: 200 } },
310
+ { id: 'node-3', type: 'surfer.crawl', name: 'Crawl Application', parameters: { timeout: 600 }, position: { x: 600, y: 200 } },
311
+ { id: 'node-4', type: 'knowledge_graph.import', name: 'Import Knowledge Graph', parameters: {}, position: { x: 850, y: 200 } },
312
+ { id: 'node-5', type: 'e2e.bootstrap', name: 'Bootstrap E2E Tests', parameters: {}, position: { x: 1100, y: 200 } },
313
+ { id: 'node-6', type: 'browser.teardown', name: 'Teardown Browser', parameters: {}, position: { x: 1350, y: 200 } },
314
+ ],
315
+ connections: {
316
+ 'node-1': { main: [[{ node: 'node-2', type: 'main', index: 0 }]] },
317
+ 'node-2': { main: [[{ node: 'node-3', type: 'main', index: 0 }]] },
318
+ 'node-3': { main: [[{ node: 'node-4', type: 'main', index: 0 }]] },
319
+ 'node-4': { main: [[{ node: 'node-5', type: 'main', index: 0 }]] },
320
+ 'node-5': { main: [[{ node: 'node-6', type: 'main', index: 0 }]] },
321
+ },
322
+ },
323
+ },
324
+ {
325
+ id: 'raw-crawl',
326
+ name: 'Raw Crawl',
327
+ description: 'Crawl app and build knowledge graph only — no E2E bootstrap',
328
+ category: 'browser',
329
+ nodeCount: 5,
330
+ complexity: 'intermediate',
331
+ icon: <Globe className="h-5 w-5" />,
332
+ useCase: 'On-demand crawl + KG build, triggered via API',
333
+ teaches: ['Browser sessions', 'Surfer crawling', 'Knowledge graphs'],
334
+ diagramPattern: 'rawCrawl',
335
+ workflow: {
336
+ name: 'Raw Crawl Workflow Template',
337
+ description: 'Crawl application and import knowledge graph. No test generation.',
338
+ nodes: [
339
+ { id: 'node-1', type: 'trigger.event', name: 'Raw Crawl Trigger', parameters: { event: 'raw_crawl_requested' }, position: { x: 100, y: 200 } },
340
+ { id: 'node-2', type: 'browser.setup', name: 'Setup Browser', parameters: { headless: false, viewport_width: 1280, viewport_height: 720 }, position: { x: 350, y: 200 } },
341
+ { id: 'node-3', type: 'surfer.crawl', name: 'Crawl Application', parameters: { timeout: 600 }, position: { x: 600, y: 200 } },
342
+ { id: 'node-4', type: 'knowledge_graph.import', name: 'Import Knowledge Graph', parameters: {}, position: { x: 850, y: 200 } },
343
+ { id: 'node-5', type: 'browser.teardown', name: 'Teardown Browser', parameters: {}, position: { x: 1100, y: 200 } },
344
+ ],
345
+ connections: {
346
+ 'node-1': { main: [[{ node: 'node-2', type: 'main', index: 0 }]] },
347
+ 'node-2': { main: [[{ node: 'node-3', type: 'main', index: 0 }]] },
348
+ 'node-3': { main: [[{ node: 'node-4', type: 'main', index: 0 }]] },
349
+ 'node-4': { main: [[{ node: 'node-5', type: 'main', index: 0 }]] },
350
+ },
351
+ },
352
+ },
353
+ {
354
+ id: 'e2e-test-run',
355
+ name: 'E2E Test Execution',
356
+ description: 'Execute a single E2E test with browser automation via surfer',
357
+ category: 'testing',
358
+ nodeCount: 4,
359
+ complexity: 'beginner',
360
+ icon: <MousePointerClick className="h-5 w-5" />,
361
+ useCase: 'Run an individual E2E test',
362
+ teaches: ['Browser setup', 'Surfer task execution', 'Session teardown'],
363
+ diagramPattern: 'e2eTestRun',
364
+ workflow: {
365
+ name: 'E2E Test Execution Template',
366
+ description: 'Execute a single E2E test using surfer browser automation',
367
+ nodes: [
368
+ { id: 'node-1', type: 'trigger.event', name: 'Test Trigger', parameters: { event: 'e2e_run_requested' }, position: { x: 100, y: 200 } },
369
+ { id: 'node-2', type: 'browser.setup', name: 'Setup Browser', parameters: { headless: true, viewport_width: 1280, viewport_height: 720 }, position: { x: 400, y: 200 } },
370
+ { id: 'node-3', type: 'surfer.execute_task', name: 'Execute Test', parameters: { max_steps: 50, timeout: 300 }, position: { x: 700, y: 200 } },
371
+ { id: 'node-4', type: 'browser.teardown', name: 'Teardown Browser', parameters: {}, position: { x: 1000, y: 200 } },
372
+ ],
373
+ connections: {
374
+ 'node-1': { main: [[{ node: 'node-2', type: 'main', index: 0 }]] },
375
+ 'node-2': { main: [[{ node: 'node-3', type: 'main', index: 0 }]] },
376
+ 'node-3': { main: [[{ node: 'node-4', type: 'main', index: 0 }]] },
377
+ },
378
+ },
379
+ },
380
+ {
381
+ id: 'app-evaluation',
382
+ name: 'App Evaluation',
383
+ description: 'Evaluate an app deployment with surfer-driven browser tests',
384
+ category: 'testing',
385
+ nodeCount: 4,
386
+ complexity: 'beginner',
387
+ icon: <Zap className="h-5 w-5" />,
388
+ useCase: 'Verify a deployed app works correctly',
389
+ teaches: ['Event triggers', 'Browser automation', 'App evaluation'],
390
+ diagramPattern: 'appEvaluation',
391
+ workflow: {
392
+ name: 'App Evaluation Workflow Template',
393
+ description: 'Evaluate a deployed application with surfer browser automation',
394
+ nodes: [
395
+ { id: 'node-1', type: 'trigger.event', name: 'Evaluation Trigger', parameters: { event: 'app_evaluation_requested' }, position: { x: 100, y: 200 } },
396
+ { id: 'node-2', type: 'browser.setup', name: 'Setup Browser', parameters: { headless: true, viewport_width: 1280, viewport_height: 720 }, position: { x: 400, y: 200 } },
397
+ { id: 'node-3', type: 'surfer.execute_task', name: 'Evaluate App', parameters: { max_steps: 50, timeout: 300 }, position: { x: 700, y: 200 } },
398
+ { id: 'node-4', type: 'browser.teardown', name: 'Teardown Browser', parameters: {}, position: { x: 1000, y: 200 } },
399
+ ],
400
+ connections: {
401
+ 'node-1': { main: [[{ node: 'node-2', type: 'main', index: 0 }]] },
402
+ 'node-2': { main: [[{ node: 'node-3', type: 'main', index: 0 }]] },
403
+ 'node-3': { main: [[{ node: 'node-4', type: 'main', index: 0 }]] },
404
+ },
405
+ },
406
+ },
407
+ {
408
+ id: 'agent-run',
409
+ name: 'Agent Run',
410
+ description: 'Execute a browser agent with LLM brain evaluation',
411
+ category: 'browser',
412
+ nodeCount: 5,
413
+ complexity: 'intermediate',
414
+ icon: <Cpu className="h-5 w-5" />,
415
+ useCase: 'Run an AI agent with evaluation loop',
416
+ teaches: [
417
+ 'Agent triggers',
418
+ 'Browser sessions',
419
+ 'Brain evaluation',
420
+ 'Pass/fail routing',
421
+ ],
422
+ diagramPattern: 'agentRun',
423
+ workflow: {
424
+ name: 'Agent Run Template',
425
+ description: 'Execute a browser agent and evaluate results with LLM brain',
426
+ nodes: [
427
+ { id: 'node-1', type: 'trigger.event', name: 'Agent Trigger', parameters: { event: 'agent_run_requested' }, position: { x: 100, y: 200 } },
428
+ { id: 'node-2', type: 'browser.setup', name: 'Setup Browser', parameters: { headless: true, viewport_width: 1280, viewport_height: 720 }, position: { x: 350, y: 200 } },
429
+ { id: 'node-3', type: 'surfer.execute_task', name: 'Execute Agent Task', parameters: { max_steps: 50, timeout: 300 }, position: { x: 600, y: 200 } },
430
+ { id: 'node-4', type: 'brain.evaluate', name: 'Evaluate Results', parameters: {}, position: { x: 850, y: 200 } },
431
+ { id: 'node-5', type: 'browser.teardown', name: 'Teardown Browser', parameters: {}, position: { x: 1100, y: 200 } },
432
+ ],
433
+ connections: {
434
+ 'node-1': { main: [[{ node: 'node-2', type: 'main', index: 0 }]] },
435
+ 'node-2': { main: [[{ node: 'node-3', type: 'main', index: 0 }]] },
436
+ 'node-3': { main: [[{ node: 'node-4', type: 'main', index: 0 }]] },
437
+ 'node-4': { main: [[{ node: 'node-5', type: 'main', index: 0 }]] },
438
+ },
439
+ },
440
+ },
441
+ ];
442
+
443
+ export interface WorkflowTemplateGalleryProps {
444
+ open: boolean;
445
+ onOpenChange: (open: boolean) => void;
446
+ /** Invoked with the selected template's workflow definition. */
447
+ onSelectTemplate: (template: WorkflowTemplateDefinition) => void;
448
+ /** Override the default template set. Defaults to {@link WORKFLOW_TEMPLATES}. */
449
+ templates?: WorkflowTemplate[];
450
+ }
451
+
452
+ export function WorkflowTemplateGallery({
453
+ open,
454
+ onOpenChange,
455
+ onSelectTemplate,
456
+ templates = WORKFLOW_TEMPLATES,
457
+ }: WorkflowTemplateGalleryProps) {
458
+ const [selectedCategory, setSelectedCategory] = useState<string>('all');
459
+
460
+ const filteredTemplates =
461
+ selectedCategory === 'all'
462
+ ? templates
463
+ : templates.filter((t) => t.category === selectedCategory);
464
+
465
+ const handleTemplateClick = (template: WorkflowTemplate) => {
466
+ onSelectTemplate(template.workflow);
467
+ onOpenChange(false);
468
+ };
469
+
470
+ return (
471
+ <Dialog open={open} onOpenChange={onOpenChange}>
472
+ <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
473
+ <DialogHeader>
474
+ <DialogTitle className="text-2xl flex items-center gap-2">
475
+ <Sparkles className="h-6 w-6 text-primary" />
476
+ Workflow Templates
477
+ </DialogTitle>
478
+ <DialogDescription>
479
+ Choose a pre-built workflow to customize for your needs
480
+ </DialogDescription>
481
+ </DialogHeader>
482
+
483
+ <Tabs
484
+ value={selectedCategory}
485
+ onValueChange={setSelectedCategory}
486
+ className="w-full"
487
+ >
488
+ <TabsList className="grid w-full grid-cols-4">
489
+ <TabsTrigger value="all">All</TabsTrigger>
490
+ <TabsTrigger value="pipeline">Pipeline</TabsTrigger>
491
+ <TabsTrigger value="browser">Browser</TabsTrigger>
492
+ <TabsTrigger value="testing">Testing</TabsTrigger>
493
+ </TabsList>
494
+
495
+ <TabsContent value={selectedCategory} className="mt-6 space-y-4">
496
+ {filteredTemplates.map((template) => {
497
+ const diagramPattern = DiagramPatterns[template.diagramPattern];
498
+ return (
499
+ <Card
500
+ key={template.id}
501
+ className="cursor-pointer hover:shadow-md hover:border-primary/50 transition-all duration-150 group"
502
+ onClick={() => handleTemplateClick(template)}
503
+ role="button"
504
+ aria-label={`Use ${template.name} template`}
505
+ >
506
+ <CardContent className="pt-6">
507
+ <div className="flex items-start gap-4">
508
+ <div className="flex-shrink-0">
509
+ <div className="h-12 w-12 rounded-lg bg-primary/5 flex items-center justify-center group-hover:bg-primary/10 transition-colors mb-3">
510
+ {template.icon}
511
+ </div>
512
+ {/* Mini workflow diagram */}
513
+ <div className="w-[188px]">
514
+ <MiniWorkflowDiagram
515
+ nodes={diagramPattern.nodes}
516
+ connections={diagramPattern.connections}
517
+ />
518
+ </div>
519
+ </div>
520
+
521
+ <div className="flex-1 min-w-0">
522
+ <div className="flex items-start justify-between gap-4 mb-2">
523
+ <div>
524
+ <h3 className="font-semibold text-lg">
525
+ {template.name}
526
+ </h3>
527
+ <p className="text-sm text-muted-foreground mt-1">
528
+ {template.description}
529
+ </p>
530
+ </div>
531
+ <Badge
532
+ variant="secondary"
533
+ className="whitespace-nowrap"
534
+ >
535
+ {template.nodeCount} nodes
536
+ </Badge>
537
+ </div>
538
+
539
+ <div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
540
+ <span className="inline-flex items-center gap-1">
541
+ <strong>Use case:</strong> {template.useCase}
542
+ </span>
543
+ </div>
544
+
545
+ <div className="mt-2 flex flex-wrap gap-1">
546
+ {template.teaches.map((skill) => (
547
+ <Badge
548
+ key={skill}
549
+ variant="outline"
550
+ className="text-xs"
551
+ >
552
+ {skill}
553
+ </Badge>
554
+ ))}
555
+ </div>
556
+
557
+ <div className="mt-3 flex items-center gap-2">
558
+ <Badge
559
+ variant={
560
+ template.complexity === 'beginner'
561
+ ? 'default'
562
+ : template.complexity === 'intermediate'
563
+ ? 'secondary'
564
+ : 'destructive'
565
+ }
566
+ className="text-xs"
567
+ >
568
+ {template.complexity}
569
+ </Badge>
570
+ <Badge
571
+ variant="outline"
572
+ className="text-xs capitalize"
573
+ >
574
+ {template.category}
575
+ </Badge>
576
+ </div>
577
+ </div>
578
+ </div>
579
+ {/* Screen reader description */}
580
+ <span id={`${template.id}-desc`} className="sr-only">
581
+ {template.description}. Creates a workflow with{' '}
582
+ {template.nodeCount} nodes. Teaches:{' '}
583
+ {template.teaches.join(', ')}. Complexity:{' '}
584
+ {template.complexity}.
585
+ </span>
586
+ </CardContent>
587
+ </Card>
588
+ );
589
+ })}
590
+ </TabsContent>
591
+ </Tabs>
592
+
593
+ {filteredTemplates.length === 0 && (
594
+ <div className="text-center py-12 text-muted-foreground">
595
+ <p>No templates found in this category.</p>
596
+ <Button
597
+ variant="link"
598
+ onClick={() => setSelectedCategory('all')}
599
+ className="mt-2"
600
+ >
601
+ View all templates
602
+ </Button>
603
+ </div>
604
+ )}
605
+ </DialogContent>
606
+ </Dialog>
607
+ );
608
+ }
@@ -85,3 +85,27 @@ export type {
85
85
  WorkflowVersion,
86
86
  WorkflowVersionHistoryProps,
87
87
  } from './WorkflowVersionHistory';
88
+
89
+ // Page composers — workflow settings, stats, list, and template gallery
90
+ export { WorkflowSettingsCard } from './WorkflowSettingsCard';
91
+ export type { WorkflowSettingsCardProps } from './WorkflowSettingsCard';
92
+ export { WorkflowStatsOverview } from './WorkflowStatsOverview';
93
+ export type {
94
+ WorkflowStats,
95
+ WorkflowStatsOverviewProps,
96
+ } from './WorkflowStatsOverview';
97
+ export { WorkflowListComposer } from './WorkflowListComposer';
98
+ export type {
99
+ WorkflowListItem,
100
+ WorkflowCreateInput,
101
+ WorkflowListComposerProps,
102
+ } from './WorkflowListComposer';
103
+ export {
104
+ WorkflowTemplateGallery,
105
+ WORKFLOW_TEMPLATES,
106
+ } from './WorkflowTemplateGallery';
107
+ export type {
108
+ WorkflowTemplate,
109
+ WorkflowTemplateDefinition,
110
+ WorkflowTemplateGalleryProps,
111
+ } from './WorkflowTemplateGallery';