flowengine-rails 0.1.0

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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/branch-name.sh +49 -0
  3. data/.claude/commands/create-pr.md +93 -0
  4. data/.claude/commands/stash-unstaged.md +21 -0
  5. data/.claude/commands/unstash-unstaged.md +15 -0
  6. data/.claude/settings.json +72 -0
  7. data/.rubocop_todo.yml +17 -0
  8. data/.ruby-version +1 -0
  9. data/CHANGELOG.md +5 -0
  10. data/CLAUDE.md +153 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +294 -0
  13. data/Rakefile +43 -0
  14. data/app/assets/javascripts/flow_engine/embed.js +44 -0
  15. data/app/assets/javascripts/flow_engine/progress_controller.js +17 -0
  16. data/app/assets/javascripts/flow_engine/step_controller.js +22 -0
  17. data/app/assets/stylesheets/flow_engine/application.css +569 -0
  18. data/app/controllers/flow_engine/admin/definitions_controller.rb +95 -0
  19. data/app/controllers/flow_engine/application_controller.rb +41 -0
  20. data/app/controllers/flow_engine/sessions_controller.rb +91 -0
  21. data/app/helpers/flow_engine/sessions_helper.rb +20 -0
  22. data/app/models/flow_engine/application_record.rb +8 -0
  23. data/app/models/flow_engine/flow_definition.rb +75 -0
  24. data/app/models/flow_engine/flow_session.rb +98 -0
  25. data/app/views/flow_engine/admin/definitions/_form.html.erb +27 -0
  26. data/app/views/flow_engine/admin/definitions/edit.html.erb +5 -0
  27. data/app/views/flow_engine/admin/definitions/index.html.erb +41 -0
  28. data/app/views/flow_engine/admin/definitions/mermaid.html.erb +9 -0
  29. data/app/views/flow_engine/admin/definitions/new.html.erb +5 -0
  30. data/app/views/flow_engine/admin/definitions/show.html.erb +25 -0
  31. data/app/views/flow_engine/sessions/completed.html.erb +34 -0
  32. data/app/views/flow_engine/sessions/new.html.erb +17 -0
  33. data/app/views/flow_engine/sessions/show.html.erb +26 -0
  34. data/app/views/flow_engine/sessions/steps/_boolean.html.erb +10 -0
  35. data/app/views/flow_engine/sessions/steps/_display.html.erb +4 -0
  36. data/app/views/flow_engine/sessions/steps/_multi_select.html.erb +8 -0
  37. data/app/views/flow_engine/sessions/steps/_number.html.erb +3 -0
  38. data/app/views/flow_engine/sessions/steps/_number_matrix.html.erb +13 -0
  39. data/app/views/flow_engine/sessions/steps/_single_select.html.erb +8 -0
  40. data/app/views/flow_engine/sessions/steps/_text.html.erb +11 -0
  41. data/app/views/flow_engine/sessions/steps/_unknown.html.erb +4 -0
  42. data/app/views/layouts/flow_engine/application.html.erb +26 -0
  43. data/app/views/layouts/flow_engine/embed.html.erb +30 -0
  44. data/config/routes.rb +22 -0
  45. data/db/migrate/01_create_flow_engine_definitions.rb +18 -0
  46. data/db/migrate/02_create_flow_engine_sessions.rb +18 -0
  47. data/exe/flowengine-rails +4 -0
  48. data/justfile +49 -0
  49. data/lefthook.yml +16 -0
  50. data/lib/flowengine/rails/configuration.rb +23 -0
  51. data/lib/flowengine/rails/dsl_loader.rb +35 -0
  52. data/lib/flowengine/rails/engine.rb +26 -0
  53. data/lib/flowengine/rails/version.rb +7 -0
  54. data/lib/flowengine/rails.rb +27 -0
  55. data/lib/generators/flow_engine/flow/flow_generator.rb +29 -0
  56. data/lib/generators/flow_engine/flow/templates/flow_definition.rb.tt +27 -0
  57. data/lib/generators/flow_engine/flow/templates/seed_task.rake.tt +22 -0
  58. data/lib/generators/flow_engine/install/install_generator.rb +34 -0
  59. data/lib/generators/flow_engine/install/templates/initializer.rb +25 -0
  60. data/log/.gitkeep +0 -0
  61. data/sig/flowengine/rails.rbs +6 -0
  62. metadata +164 -0
@@ -0,0 +1,569 @@
1
+ /* FlowEngine — Adaptive Theme System
2
+ *
3
+ * Every visual token is a CSS custom property. Override at the .fe-container
4
+ * level or higher to reshape the wizard for any host theme.
5
+ *
6
+ * .my-dark-theme .fe-container {
7
+ * --fe-bg: #1c1c1e;
8
+ * --fe-fg: #f5f5f7;
9
+ * --fe-accent: #6ee7b7;
10
+ * }
11
+ */
12
+
13
+ /* ------------------------------------------------------------------ */
14
+ /* Design Tokens */
15
+ /* ------------------------------------------------------------------ */
16
+
17
+ .fe-container {
18
+ /* Surface & text */
19
+ --fe-bg: #ffffff;
20
+ --fe-fg: #1d1d1f;
21
+ --fe-fg-secondary: #6e6e73;
22
+ --fe-fg-tertiary: #aeaeb2;
23
+
24
+ /* Accent — a single hue drives the whole palette */
25
+ --fe-accent: #0071e3;
26
+ --fe-accent-hover: #0077ed;
27
+ --fe-accent-subtle: color-mix(in srgb, var(--fe-accent) 8%, transparent);
28
+ --fe-accent-ring: color-mix(in srgb, var(--fe-accent) 20%, transparent);
29
+
30
+ /* Surfaces */
31
+ --fe-surface: #f5f5f7;
32
+ --fe-surface-hover: #e8e8ed;
33
+ --fe-surface-raised: #ffffff;
34
+ --fe-border: #d2d2d7;
35
+ --fe-border-light: #e5e5ea;
36
+
37
+ /* Feedback */
38
+ --fe-success: #248a3d;
39
+ --fe-success-bg: #dafbe8;
40
+ --fe-error: #d70015;
41
+ --fe-error-bg: #ffe5e7;
42
+
43
+ /* Typography */
44
+ --fe-font-body: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
45
+ --fe-font-display: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
46
+ --fe-font-mono: "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", monospace;
47
+ --fe-line-height: 1.55;
48
+
49
+ /* Spacing scale (4px base) */
50
+ --fe-space-xs: 0.25rem;
51
+ --fe-space-sm: 0.5rem;
52
+ --fe-space-md: 1rem;
53
+ --fe-space-lg: 1.5rem;
54
+ --fe-space-xl: 2rem;
55
+ --fe-space-2xl: 3rem;
56
+
57
+ /* Shape */
58
+ --fe-radius-sm: 6px;
59
+ --fe-radius-md: 10px;
60
+ --fe-radius-lg: 14px;
61
+ --fe-radius-full: 9999px;
62
+
63
+ /* Shadows */
64
+ --fe-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
65
+ --fe-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
66
+ --fe-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04);
67
+ --fe-shadow-focus: 0 0 0 3px var(--fe-accent-ring);
68
+
69
+ /* Transitions */
70
+ --fe-ease: cubic-bezier(0.25, 0.46, 0.45, 0.94);
71
+ --fe-duration-fast: 120ms;
72
+ --fe-duration: 200ms;
73
+ --fe-duration-slow: 400ms;
74
+
75
+ /* Layout */
76
+ --fe-max-width: 680px;
77
+ }
78
+
79
+ /* ------------------------------------------------------------------ */
80
+ /* Container */
81
+ /* ------------------------------------------------------------------ */
82
+
83
+ .fe-container {
84
+ max-width: var(--fe-max-width);
85
+ margin: 0 auto;
86
+ padding: var(--fe-space-xl) var(--fe-space-lg);
87
+ font-family: var(--fe-font-body);
88
+ font-size: 1rem;
89
+ color: var(--fe-fg);
90
+ line-height: var(--fe-line-height);
91
+ background: var(--fe-bg);
92
+ -webkit-font-smoothing: antialiased;
93
+ -moz-osx-font-smoothing: grayscale;
94
+ }
95
+
96
+ /* ------------------------------------------------------------------ */
97
+ /* Flash Messages */
98
+ /* ------------------------------------------------------------------ */
99
+
100
+ .fe-flash {
101
+ padding: var(--fe-space-sm) var(--fe-space-md);
102
+ margin-bottom: var(--fe-space-md);
103
+ border-radius: var(--fe-radius-sm);
104
+ font-size: 0.875rem;
105
+ font-weight: 500;
106
+ animation: fe-slide-in var(--fe-duration) var(--fe-ease) both;
107
+ }
108
+ .fe-flash--notice {
109
+ background: var(--fe-success-bg);
110
+ color: var(--fe-success);
111
+ }
112
+ .fe-flash--alert {
113
+ background: var(--fe-error-bg);
114
+ color: var(--fe-error);
115
+ }
116
+
117
+ /* ------------------------------------------------------------------ */
118
+ /* Progress Bar */
119
+ /* ------------------------------------------------------------------ */
120
+
121
+ .fe-progress {
122
+ background: var(--fe-surface);
123
+ border-radius: var(--fe-radius-full);
124
+ height: 6px;
125
+ position: relative;
126
+ margin-bottom: var(--fe-space-2xl);
127
+ overflow: hidden;
128
+ }
129
+ .fe-progress__bar {
130
+ background: var(--fe-accent);
131
+ height: 100%;
132
+ border-radius: var(--fe-radius-full);
133
+ transition: width var(--fe-duration-slow) var(--fe-ease);
134
+ min-width: 0;
135
+ position: relative;
136
+ }
137
+ .fe-progress__bar::after {
138
+ content: "";
139
+ position: absolute;
140
+ inset: 0;
141
+ background: linear-gradient(
142
+ 90deg,
143
+ transparent 0%,
144
+ rgba(255, 255, 255, 0.3) 50%,
145
+ transparent 100%
146
+ );
147
+ animation: fe-shimmer 2s ease-in-out infinite;
148
+ }
149
+ .fe-progress__text {
150
+ display: block;
151
+ margin-top: var(--fe-space-xs);
152
+ font-size: 0.75rem;
153
+ font-weight: 500;
154
+ color: var(--fe-fg-tertiary);
155
+ text-align: right;
156
+ letter-spacing: 0.02em;
157
+ }
158
+
159
+ /* ------------------------------------------------------------------ */
160
+ /* Step */
161
+ /* ------------------------------------------------------------------ */
162
+
163
+ .fe-step {
164
+ margin-bottom: var(--fe-space-xl);
165
+ animation: fe-fade-up var(--fe-duration-slow) var(--fe-ease) both;
166
+ }
167
+
168
+ .fe-step__question {
169
+ font-family: var(--fe-font-display);
170
+ font-size: 1.375rem;
171
+ font-weight: 600;
172
+ line-height: 1.3;
173
+ margin: 0 0 var(--fe-space-lg);
174
+ color: var(--fe-fg);
175
+ letter-spacing: -0.01em;
176
+ }
177
+
178
+ .fe-step__actions {
179
+ margin-top: var(--fe-space-xl);
180
+ display: flex;
181
+ align-items: center;
182
+ gap: var(--fe-space-sm);
183
+ }
184
+
185
+ /* ------------------------------------------------------------------ */
186
+ /* Form Fields */
187
+ /* ------------------------------------------------------------------ */
188
+
189
+ .fe-field {
190
+ margin-bottom: var(--fe-space-md);
191
+ }
192
+
193
+ /* Radio & Checkbox — card-style selectors */
194
+ .fe-checkbox,
195
+ .fe-radio {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: var(--fe-space-sm);
199
+ padding: var(--fe-space-sm) var(--fe-space-md);
200
+ margin-bottom: var(--fe-space-xs);
201
+ cursor: pointer;
202
+ font-size: 0.9375rem;
203
+ border-radius: var(--fe-radius-sm);
204
+ border: 1px solid var(--fe-border-light);
205
+ background: var(--fe-surface-raised);
206
+ transition:
207
+ border-color var(--fe-duration-fast) var(--fe-ease),
208
+ background var(--fe-duration-fast) var(--fe-ease),
209
+ box-shadow var(--fe-duration-fast) var(--fe-ease);
210
+ }
211
+ .fe-checkbox:hover,
212
+ .fe-radio:hover {
213
+ border-color: var(--fe-border);
214
+ background: var(--fe-surface);
215
+ }
216
+ .fe-checkbox:has(input:checked),
217
+ .fe-radio:has(input:checked) {
218
+ border-color: var(--fe-accent);
219
+ background: var(--fe-accent-subtle);
220
+ }
221
+ .fe-checkbox input,
222
+ .fe-radio input {
223
+ width: 18px;
224
+ height: 18px;
225
+ cursor: pointer;
226
+ accent-color: var(--fe-accent);
227
+ flex-shrink: 0;
228
+ }
229
+
230
+ /* Matrix rows */
231
+ .fe-matrix-row {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: var(--fe-space-md);
235
+ padding: var(--fe-space-sm) 0;
236
+ }
237
+ .fe-matrix-row__label {
238
+ min-width: 160px;
239
+ font-weight: 500;
240
+ font-size: 0.9375rem;
241
+ color: var(--fe-fg);
242
+ }
243
+
244
+ /* Text inputs */
245
+ .fe-input {
246
+ display: block;
247
+ width: 100%;
248
+ padding: var(--fe-space-sm) 0.75rem;
249
+ font-size: 0.9375rem;
250
+ font-family: var(--fe-font-body);
251
+ line-height: 1.5;
252
+ color: var(--fe-fg);
253
+ border: 1px solid var(--fe-border);
254
+ border-radius: var(--fe-radius-sm);
255
+ background: var(--fe-surface-raised);
256
+ box-sizing: border-box;
257
+ transition:
258
+ border-color var(--fe-duration-fast) var(--fe-ease),
259
+ box-shadow var(--fe-duration-fast) var(--fe-ease);
260
+ }
261
+ .fe-input::placeholder {
262
+ color: var(--fe-fg-tertiary);
263
+ }
264
+ .fe-input:focus {
265
+ outline: none;
266
+ border-color: var(--fe-accent);
267
+ box-shadow: var(--fe-shadow-focus);
268
+ }
269
+ .fe-input--number {
270
+ width: 120px;
271
+ text-align: center;
272
+ font-variant-numeric: tabular-nums;
273
+ }
274
+
275
+ /* Textarea — full-width, generous height */
276
+ .fe-input--textarea {
277
+ width: 100%;
278
+ min-height: 160px;
279
+ resize: vertical;
280
+ font-family: var(--fe-font-body);
281
+ line-height: 1.6;
282
+ padding: 0.75rem;
283
+ box-sizing: border-box;
284
+ }
285
+ .fe-input--code {
286
+ font-family: var(--fe-font-mono);
287
+ font-size: 0.8125rem;
288
+ line-height: 1.5;
289
+ tab-size: 2;
290
+ }
291
+
292
+ .fe-label {
293
+ display: block;
294
+ font-weight: 600;
295
+ font-size: 0.875rem;
296
+ margin-bottom: var(--fe-space-xs);
297
+ color: var(--fe-fg);
298
+ letter-spacing: 0.01em;
299
+ }
300
+
301
+ .fe-form-group {
302
+ margin-bottom: var(--fe-space-md);
303
+ }
304
+
305
+ /* ------------------------------------------------------------------ */
306
+ /* Buttons */
307
+ /* ------------------------------------------------------------------ */
308
+
309
+ .fe-btn {
310
+ display: inline-flex;
311
+ align-items: center;
312
+ justify-content: center;
313
+ gap: var(--fe-space-xs);
314
+ padding: 0.625rem 1.375rem;
315
+ font-family: var(--fe-font-body);
316
+ font-size: 0.9375rem;
317
+ font-weight: 600;
318
+ line-height: 1;
319
+ border: 1px solid var(--fe-border);
320
+ border-radius: var(--fe-radius-sm);
321
+ background: var(--fe-surface-raised);
322
+ color: var(--fe-fg);
323
+ cursor: pointer;
324
+ text-decoration: none;
325
+ white-space: nowrap;
326
+ transition:
327
+ background var(--fe-duration-fast) var(--fe-ease),
328
+ border-color var(--fe-duration-fast) var(--fe-ease),
329
+ box-shadow var(--fe-duration-fast) var(--fe-ease),
330
+ transform var(--fe-duration-fast) var(--fe-ease);
331
+ }
332
+ .fe-btn:hover {
333
+ background: var(--fe-surface);
334
+ border-color: var(--fe-border);
335
+ }
336
+ .fe-btn:active {
337
+ transform: scale(0.98);
338
+ }
339
+
340
+ .fe-btn--primary {
341
+ background: var(--fe-accent);
342
+ color: #fff;
343
+ border-color: var(--fe-accent);
344
+ }
345
+ .fe-btn--primary:hover {
346
+ background: var(--fe-accent-hover);
347
+ border-color: var(--fe-accent-hover);
348
+ box-shadow: var(--fe-shadow-md);
349
+ }
350
+ .fe-btn:disabled {
351
+ opacity: 0.5;
352
+ cursor: not-allowed;
353
+ pointer-events: none;
354
+ }
355
+
356
+ /* ------------------------------------------------------------------ */
357
+ /* Tables */
358
+ /* ------------------------------------------------------------------ */
359
+
360
+ .fe-table {
361
+ width: 100%;
362
+ border-collapse: collapse;
363
+ margin: var(--fe-space-md) 0;
364
+ font-size: 0.9375rem;
365
+ }
366
+ .fe-table th,
367
+ .fe-table td {
368
+ text-align: left;
369
+ padding: var(--fe-space-sm) 0.75rem;
370
+ border-bottom: 1px solid var(--fe-border-light);
371
+ }
372
+ .fe-table th {
373
+ font-weight: 600;
374
+ font-size: 0.75rem;
375
+ text-transform: uppercase;
376
+ letter-spacing: 0.05em;
377
+ color: var(--fe-fg-secondary);
378
+ background: transparent;
379
+ }
380
+ .fe-table tr:hover td {
381
+ background: var(--fe-accent-subtle);
382
+ }
383
+
384
+ /* ------------------------------------------------------------------ */
385
+ /* Code Blocks */
386
+ /* ------------------------------------------------------------------ */
387
+
388
+ .fe-code {
389
+ background: var(--fe-surface);
390
+ border: 1px solid var(--fe-border-light);
391
+ border-radius: var(--fe-radius-md);
392
+ padding: var(--fe-space-md);
393
+ overflow-x: auto;
394
+ font-family: var(--fe-font-mono);
395
+ font-size: 0.8125rem;
396
+ line-height: 1.6;
397
+ }
398
+
399
+ /* ------------------------------------------------------------------ */
400
+ /* Errors */
401
+ /* ------------------------------------------------------------------ */
402
+
403
+ .fe-errors {
404
+ background: var(--fe-error-bg);
405
+ border: 1px solid color-mix(in srgb, var(--fe-error) 20%, transparent);
406
+ border-radius: var(--fe-radius-md);
407
+ padding: var(--fe-space-md);
408
+ margin-bottom: var(--fe-space-md);
409
+ color: var(--fe-error);
410
+ animation: fe-shake var(--fe-duration-slow) var(--fe-ease);
411
+ }
412
+ .fe-errors h3 {
413
+ margin: 0 0 var(--fe-space-sm);
414
+ font-size: 0.875rem;
415
+ font-weight: 600;
416
+ }
417
+ .fe-errors ul {
418
+ margin: 0;
419
+ padding-left: 1.25rem;
420
+ font-size: 0.875rem;
421
+ }
422
+
423
+ /* ------------------------------------------------------------------ */
424
+ /* Definition Cards */
425
+ /* ------------------------------------------------------------------ */
426
+
427
+ .fe-definition-card {
428
+ border: 1px solid var(--fe-border-light);
429
+ border-radius: var(--fe-radius-md);
430
+ padding: var(--fe-space-md) var(--fe-space-lg);
431
+ margin-bottom: var(--fe-space-sm);
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: space-between;
435
+ background: var(--fe-surface-raised);
436
+ box-shadow: var(--fe-shadow-sm);
437
+ transition:
438
+ box-shadow var(--fe-duration) var(--fe-ease),
439
+ border-color var(--fe-duration) var(--fe-ease);
440
+ }
441
+ .fe-definition-card:hover {
442
+ box-shadow: var(--fe-shadow-md);
443
+ border-color: var(--fe-border);
444
+ }
445
+ .fe-definition-card h2 {
446
+ margin: 0;
447
+ font-family: var(--fe-font-display);
448
+ font-size: 1.0625rem;
449
+ font-weight: 600;
450
+ }
451
+ .fe-definition-card small {
452
+ color: var(--fe-fg-tertiary);
453
+ font-weight: 400;
454
+ font-size: 0.8125rem;
455
+ }
456
+
457
+ .fe-definition-group {
458
+ margin-bottom: var(--fe-space-xl);
459
+ }
460
+ .fe-definition-group h2 {
461
+ font-family: var(--fe-font-display);
462
+ font-size: 0.8125rem;
463
+ font-weight: 600;
464
+ text-transform: uppercase;
465
+ letter-spacing: 0.06em;
466
+ color: var(--fe-fg-secondary);
467
+ border-bottom: 1px solid var(--fe-border-light);
468
+ padding-bottom: var(--fe-space-sm);
469
+ margin-bottom: var(--fe-space-md);
470
+ }
471
+
472
+ /* ------------------------------------------------------------------ */
473
+ /* Completed View */
474
+ /* ------------------------------------------------------------------ */
475
+
476
+ .fe-completed {
477
+ text-align: center;
478
+ animation: fe-fade-up var(--fe-duration-slow) var(--fe-ease) both;
479
+ }
480
+ .fe-completed h1 {
481
+ font-family: var(--fe-font-display);
482
+ font-size: 1.75rem;
483
+ font-weight: 700;
484
+ color: var(--fe-success);
485
+ letter-spacing: -0.02em;
486
+ }
487
+ .fe-completed__summary {
488
+ text-align: left;
489
+ margin-top: var(--fe-space-xl);
490
+ }
491
+
492
+ /* ------------------------------------------------------------------ */
493
+ /* Display & Hints */
494
+ /* ------------------------------------------------------------------ */
495
+
496
+ .fe-display-text {
497
+ font-size: 0.9375rem;
498
+ color: var(--fe-fg-secondary);
499
+ line-height: 1.6;
500
+ }
501
+ .fe-field__hint {
502
+ font-size: 0.8125rem;
503
+ color: var(--fe-fg-tertiary);
504
+ font-style: normal;
505
+ margin-top: var(--fe-space-xs);
506
+ }
507
+
508
+ /* ------------------------------------------------------------------ */
509
+ /* Mermaid */
510
+ /* ------------------------------------------------------------------ */
511
+
512
+ .fe-mermaid {
513
+ margin: var(--fe-space-md) 0;
514
+ }
515
+
516
+ /* ------------------------------------------------------------------ */
517
+ /* Animations */
518
+ /* ------------------------------------------------------------------ */
519
+
520
+ @keyframes fe-fade-up {
521
+ from {
522
+ opacity: 0;
523
+ transform: translateY(8px);
524
+ }
525
+ to {
526
+ opacity: 1;
527
+ transform: translateY(0);
528
+ }
529
+ }
530
+
531
+ @keyframes fe-slide-in {
532
+ from {
533
+ opacity: 0;
534
+ transform: translateY(-4px);
535
+ }
536
+ to {
537
+ opacity: 1;
538
+ transform: translateY(0);
539
+ }
540
+ }
541
+
542
+ @keyframes fe-shimmer {
543
+ 0% { transform: translateX(-100%); }
544
+ 100% { transform: translateX(200%); }
545
+ }
546
+
547
+ @keyframes fe-shake {
548
+ 0%, 100% { transform: translateX(0); }
549
+ 15% { transform: translateX(-4px); }
550
+ 30% { transform: translateX(3px); }
551
+ 45% { transform: translateX(-2px); }
552
+ 60% { transform: translateX(1px); }
553
+ }
554
+
555
+ /* Respect reduced motion */
556
+ @media (prefers-reduced-motion: reduce) {
557
+ .fe-step,
558
+ .fe-completed,
559
+ .fe-flash,
560
+ .fe-errors {
561
+ animation: none;
562
+ }
563
+ .fe-progress__bar::after {
564
+ animation: none;
565
+ }
566
+ .fe-progress__bar {
567
+ transition: none;
568
+ }
569
+ }
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ module Admin
5
+ class DefinitionsController < ApplicationController
6
+ before_action :authenticate_admin!
7
+ before_action :set_definition, only: %i[show edit update destroy activate deactivate mermaid]
8
+
9
+ def index
10
+ @definitions = FlowEngine::FlowDefinition.order(:name, version: :desc)
11
+ @grouped = @definitions.group_by(&:name)
12
+ end
13
+
14
+ def show; end
15
+
16
+ def new
17
+ @definition = FlowEngine::FlowDefinition.new
18
+ end
19
+
20
+ def create
21
+ @definition = FlowEngine::FlowDefinition.new(definition_params)
22
+
23
+ if @definition.save
24
+ redirect_to admin_definition_path(@definition), notice: "Definition created."
25
+ else
26
+ render :new, status: :unprocessable_entity
27
+ end
28
+ end
29
+
30
+ def edit
31
+ return unless @definition.readonly?
32
+
33
+ redirect_to admin_definition_path(@definition),
34
+ alert: "Cannot edit definition with existing sessions. Create a new version instead."
35
+ nil
36
+ end
37
+
38
+ def update
39
+ if @definition.readonly?
40
+ redirect_to admin_definition_path(@definition),
41
+ alert: "Cannot edit definition with existing sessions."
42
+ return
43
+ end
44
+
45
+ if @definition.update(definition_params)
46
+ FlowEngine::Rails::DslLoader.clear_cache!
47
+ redirect_to admin_definition_path(@definition), notice: "Definition updated."
48
+ else
49
+ render :edit, status: :unprocessable_entity
50
+ end
51
+ end
52
+
53
+ def destroy
54
+ if @definition.flow_sessions.exists?
55
+ redirect_to admin_definitions_path, alert: "Cannot delete definition with existing sessions."
56
+ return
57
+ end
58
+
59
+ @definition.destroy!
60
+ redirect_to admin_definitions_path, notice: "Definition deleted."
61
+ end
62
+
63
+ def activate
64
+ @definition.activate!
65
+ redirect_to admin_definition_path(@definition), notice: "Definition activated."
66
+ end
67
+
68
+ def deactivate
69
+ @definition.deactivate!
70
+ redirect_to admin_definition_path(@definition), notice: "Definition deactivated."
71
+ end
72
+
73
+ def mermaid
74
+ @diagram = @definition.mermaid_diagram
75
+ end
76
+
77
+ private
78
+
79
+ def set_definition
80
+ @definition = FlowEngine::FlowDefinition.find(params[:id])
81
+ end
82
+
83
+ def definition_params
84
+ params.expect(definition: %i[name dsl])
85
+ end
86
+
87
+ def authenticate_admin!
88
+ method_name = FlowEngine::Rails.configuration.admin_authentication_method
89
+ return unless method_name
90
+
91
+ send(method_name)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+
7
+ before_action :detect_embed_mode
8
+ before_action :set_cors_headers, if: :embed_mode?
9
+
10
+ helper_method :embed_mode?
11
+
12
+ private
13
+
14
+ def detect_embed_mode
15
+ @embed_mode = params[:embed] == "true"
16
+ end
17
+
18
+ def embed_mode?
19
+ @embed_mode
20
+ end
21
+
22
+ def set_cors_headers
23
+ allowed_origins = FlowEngine::Rails.configuration.embed_allowed_origins
24
+ origin = request.headers["Origin"]
25
+
26
+ return unless origin && (allowed_origins.include?("*") || allowed_origins.include?(origin))
27
+
28
+ response.headers["Access-Control-Allow-Origin"] = origin
29
+ response.headers["Access-Control-Allow-Methods"] = "GET, POST, PATCH"
30
+ response.headers["Access-Control-Allow-Headers"] = "Content-Type"
31
+ end
32
+
33
+ def current_layout
34
+ if embed_mode?
35
+ FlowEngine::Rails.configuration.embed_layout
36
+ else
37
+ FlowEngine::Rails.configuration.default_layout
38
+ end
39
+ end
40
+ end
41
+ end