@d34dman/flowdrop 0.0.45 → 0.0.46
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/README.md +2 -2
- package/dist/components/ConfigForm.svelte +4 -20
- package/dist/components/Navbar.svelte +6 -7
- package/dist/components/SchemaForm.svelte +2 -10
- package/dist/components/WorkflowEditor.svelte +132 -4
- package/dist/components/form/FormAutocomplete.svelte +5 -9
- package/dist/components/form/FormCheckboxGroup.svelte +11 -1
- package/dist/components/form/FormCheckboxGroup.svelte.d.ts +2 -0
- package/dist/components/form/FormCodeEditor.svelte +16 -7
- package/dist/components/form/FormCodeEditor.svelte.d.ts +2 -0
- package/dist/components/form/FormField.svelte +20 -1
- package/dist/components/form/FormMarkdownEditor.svelte +29 -19
- package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
- package/dist/components/form/FormNumberField.svelte +4 -0
- package/dist/components/form/FormNumberField.svelte.d.ts +2 -0
- package/dist/components/form/FormRangeField.svelte +4 -0
- package/dist/components/form/FormRangeField.svelte.d.ts +2 -0
- package/dist/components/form/FormSelect.svelte +4 -0
- package/dist/components/form/FormSelect.svelte.d.ts +2 -0
- package/dist/components/form/FormTemplateEditor.svelte +16 -7
- package/dist/components/form/FormTemplateEditor.svelte.d.ts +2 -0
- package/dist/components/form/FormTextField.svelte +4 -0
- package/dist/components/form/FormTextField.svelte.d.ts +2 -0
- package/dist/components/form/FormTextarea.svelte +4 -0
- package/dist/components/form/FormTextarea.svelte.d.ts +2 -0
- package/dist/components/form/FormToggle.svelte +4 -0
- package/dist/components/form/FormToggle.svelte.d.ts +2 -0
- package/dist/components/form/types.d.ts +5 -0
- package/dist/components/form/types.js +1 -1
- package/dist/components/layouts/MainLayout.svelte +5 -2
- package/dist/components/nodes/GatewayNode.svelte +0 -8
- package/dist/components/nodes/SimpleNode.svelte +2 -3
- package/dist/components/nodes/WorkflowNode.svelte +0 -8
- package/dist/components/playground/Playground.svelte +43 -38
- package/dist/editor/index.d.ts +3 -1
- package/dist/editor/index.js +5 -1
- package/dist/helpers/workflowEditorHelper.js +1 -2
- package/dist/services/autoSaveService.js +5 -5
- package/dist/services/historyService.d.ts +207 -0
- package/dist/services/historyService.js +317 -0
- package/dist/services/settingsService.d.ts +2 -2
- package/dist/services/settingsService.js +15 -21
- package/dist/services/toastService.d.ts +1 -1
- package/dist/services/toastService.js +10 -10
- package/dist/stores/historyStore.d.ts +133 -0
- package/dist/stores/historyStore.js +188 -0
- package/dist/stores/settingsStore.d.ts +1 -1
- package/dist/stores/settingsStore.js +40 -42
- package/dist/stores/themeStore.d.ts +2 -2
- package/dist/stores/themeStore.js +30 -32
- package/dist/stores/workflowStore.d.ts +52 -2
- package/dist/stores/workflowStore.js +102 -2
- package/dist/styles/base.css +28 -8
- package/dist/styles/toast.css +3 -1
- package/dist/styles/tokens.css +2 -2
- package/dist/types/settings.d.ts +3 -3
- package/dist/types/settings.js +13 -19
- package/dist/utils/colors.js +17 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -66,8 +66,8 @@ You get a production-ready workflow UI. You keep full control of everything else
|
|
|
66
66
|
|
|
67
67
|
## Features
|
|
68
68
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
69
|
+
| | |
|
|
70
|
+
| ---------------------------- | ------------------------------------------------------------------------- |
|
|
71
71
|
| 🎨 **Visual Editor Only** | Pure UI component. No hidden backend, no external dependencies |
|
|
72
72
|
| 🔐 **You Own Everything** | Your data, your servers, your orchestration logic, your security policies |
|
|
73
73
|
| 🔌 **Backend Agnostic** | Connect to any API: Drupal, Laravel, Express, FastAPI, or your own |
|
|
@@ -670,11 +670,7 @@
|
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
.config-form__button--primary {
|
|
673
|
-
background: linear-gradient(
|
|
674
|
-
135deg,
|
|
675
|
-
var(--fd-primary) 0%,
|
|
676
|
-
var(--fd-primary-hover) 100%
|
|
677
|
-
);
|
|
673
|
+
background: linear-gradient(135deg, var(--fd-primary) 0%, var(--fd-primary-hover) 100%);
|
|
678
674
|
color: var(--fd-primary-foreground);
|
|
679
675
|
box-shadow:
|
|
680
676
|
0 1px 3px rgba(59, 130, 246, 0.3),
|
|
@@ -682,11 +678,7 @@
|
|
|
682
678
|
}
|
|
683
679
|
|
|
684
680
|
.config-form__button--primary:hover {
|
|
685
|
-
background: linear-gradient(
|
|
686
|
-
135deg,
|
|
687
|
-
var(--fd-primary-hover) 0%,
|
|
688
|
-
var(--fd-primary-hover) 100%
|
|
689
|
-
);
|
|
681
|
+
background: linear-gradient(135deg, var(--fd-primary-hover) 0%, var(--fd-primary-hover) 100%);
|
|
690
682
|
box-shadow:
|
|
691
683
|
0 4px 12px rgba(59, 130, 246, 0.35),
|
|
692
684
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
@@ -1006,11 +998,7 @@
|
|
|
1006
998
|
============================================ */
|
|
1007
999
|
|
|
1008
1000
|
.config-form__button--external {
|
|
1009
|
-
background: linear-gradient(
|
|
1010
|
-
135deg,
|
|
1011
|
-
var(--fd-accent) 0%,
|
|
1012
|
-
var(--fd-primary) 100%
|
|
1013
|
-
);
|
|
1001
|
+
background: linear-gradient(135deg, var(--fd-accent) 0%, var(--fd-primary) 100%);
|
|
1014
1002
|
color: var(--fd-accent-foreground);
|
|
1015
1003
|
box-shadow:
|
|
1016
1004
|
0 1px 3px rgba(99, 102, 241, 0.3),
|
|
@@ -1018,11 +1006,7 @@
|
|
|
1018
1006
|
}
|
|
1019
1007
|
|
|
1020
1008
|
.config-form__button--external:hover {
|
|
1021
|
-
background: linear-gradient(
|
|
1022
|
-
135deg,
|
|
1023
|
-
var(--fd-accent-hover) 0%,
|
|
1024
|
-
var(--fd-primary-hover) 100%
|
|
1025
|
-
);
|
|
1009
|
+
background: linear-gradient(135deg, var(--fd-accent-hover) 0%, var(--fd-primary-hover) 100%);
|
|
1026
1010
|
box-shadow:
|
|
1027
1011
|
0 4px 12px rgba(99, 102, 241, 0.35),
|
|
1028
1012
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
@@ -377,12 +377,11 @@
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
.flowdrop-navbar__status {
|
|
380
|
-
display: flex;
|
|
380
|
+
display: inline-flex;
|
|
381
381
|
align-items: center;
|
|
382
382
|
gap: 0.375rem;
|
|
383
|
-
padding:
|
|
383
|
+
padding: var(--fd-space-1) var(--fd-space-2);
|
|
384
384
|
background-color: var(--fd-success-muted);
|
|
385
|
-
border: 1px solid var(--fd-success);
|
|
386
385
|
border-radius: var(--fd-radius-md);
|
|
387
386
|
font-size: var(--fd-text-xs);
|
|
388
387
|
font-weight: 500;
|
|
@@ -391,13 +390,13 @@
|
|
|
391
390
|
.flowdrop-navbar__status-indicator {
|
|
392
391
|
width: 0.375rem;
|
|
393
392
|
height: 0.375rem;
|
|
394
|
-
background-color: var(--fd-success);
|
|
393
|
+
background-color: var(--fd-success-hover);
|
|
395
394
|
border-radius: 50%;
|
|
396
395
|
animation: pulse 2s infinite;
|
|
397
396
|
}
|
|
398
397
|
|
|
399
398
|
.flowdrop-navbar__status-text {
|
|
400
|
-
color: var(--fd-success);
|
|
399
|
+
color: var(--fd-success-hover);
|
|
401
400
|
font-size: var(--fd-text-xs);
|
|
402
401
|
font-weight: 500;
|
|
403
402
|
}
|
|
@@ -705,8 +704,8 @@
|
|
|
705
704
|
}
|
|
706
705
|
|
|
707
706
|
.flowdrop-navbar__status {
|
|
708
|
-
font-size:
|
|
709
|
-
padding:
|
|
707
|
+
font-size: var(--fd-text-xs);
|
|
708
|
+
padding: var(--fd-space-1) var(--fd-space-2);
|
|
710
709
|
}
|
|
711
710
|
}
|
|
712
711
|
|
|
@@ -427,11 +427,7 @@
|
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
.schema-form__button--primary {
|
|
430
|
-
background: linear-gradient(
|
|
431
|
-
135deg,
|
|
432
|
-
var(--fd-primary) 0%,
|
|
433
|
-
var(--fd-primary-hover) 100%
|
|
434
|
-
);
|
|
430
|
+
background: linear-gradient(135deg, var(--fd-primary) 0%, var(--fd-primary-hover) 100%);
|
|
435
431
|
color: var(--fd-primary-foreground);
|
|
436
432
|
box-shadow:
|
|
437
433
|
0 1px 3px rgba(59, 130, 246, 0.3),
|
|
@@ -439,11 +435,7 @@
|
|
|
439
435
|
}
|
|
440
436
|
|
|
441
437
|
.schema-form__button--primary:hover:not(:disabled) {
|
|
442
|
-
background: linear-gradient(
|
|
443
|
-
135deg,
|
|
444
|
-
var(--fd-primary-hover) 0%,
|
|
445
|
-
var(--fd-primary-hover) 100%
|
|
446
|
-
);
|
|
438
|
+
background: linear-gradient(135deg, var(--fd-primary-hover) 0%, var(--fd-primary-hover) 100%);
|
|
447
439
|
box-shadow:
|
|
448
440
|
0 4px 12px rgba(59, 130, 246, 0.35),
|
|
449
441
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import type { EndpointConfig } from '../config/endpoints.js';
|
|
31
31
|
import ConnectionLine from './ConnectionLine.svelte';
|
|
32
32
|
import { workflowStore, workflowActions } from '../stores/workflowStore.js';
|
|
33
|
+
import { historyActions, setOnRestoreCallback } from '../stores/historyStore.js';
|
|
33
34
|
import UniversalNode from './UniversalNode.svelte';
|
|
34
35
|
import {
|
|
35
36
|
EdgeStylingHelper,
|
|
@@ -65,13 +66,45 @@
|
|
|
65
66
|
// Create a local currentWorkflow variable that we can control directly
|
|
66
67
|
let currentWorkflow = $state<Workflow | null>(null);
|
|
67
68
|
|
|
69
|
+
// Track if we're currently dragging a node (for history debouncing)
|
|
70
|
+
let isDraggingNode = $state(false);
|
|
71
|
+
|
|
72
|
+
// Track the workflow ID we're currently editing to detect workflow switches
|
|
73
|
+
let currentWorkflowId: string | null = null;
|
|
74
|
+
|
|
68
75
|
// Initialize currentWorkflow from global store
|
|
76
|
+
// Only sync when workflow ID changes (new workflow loaded) or on initial load
|
|
69
77
|
$effect(() => {
|
|
70
78
|
if ($workflowStore) {
|
|
71
|
-
|
|
79
|
+
const storeWorkflowId = $workflowStore.id;
|
|
80
|
+
|
|
81
|
+
// Sync on initial load or when a different workflow is loaded
|
|
82
|
+
if (currentWorkflowId !== storeWorkflowId) {
|
|
83
|
+
currentWorkflow = $workflowStore;
|
|
84
|
+
currentWorkflowId = storeWorkflowId;
|
|
85
|
+
}
|
|
86
|
+
} else if (currentWorkflow !== null) {
|
|
87
|
+
// Store was cleared
|
|
88
|
+
currentWorkflow = null;
|
|
89
|
+
currentWorkflowId = null;
|
|
72
90
|
}
|
|
73
91
|
});
|
|
74
92
|
|
|
93
|
+
// Set up the history restore callback to update workflow when undo/redo is triggered
|
|
94
|
+
$effect(() => {
|
|
95
|
+
setOnRestoreCallback((restoredWorkflow: Workflow) => {
|
|
96
|
+
// Directly update local state (bypass store sync effect)
|
|
97
|
+
currentWorkflow = restoredWorkflow;
|
|
98
|
+
// Also update the store without triggering history
|
|
99
|
+
workflowActions.restoreFromHistory(restoredWorkflow);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Cleanup on unmount
|
|
103
|
+
return () => {
|
|
104
|
+
setOnRestoreCallback(null);
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
75
108
|
// Create local reactive variables that sync with currentWorkflow
|
|
76
109
|
let flowNodes = $state<WorkflowNodeType[]>([]);
|
|
77
110
|
let flowEdges = $state<WorkflowEdge[]>([]);
|
|
@@ -282,6 +315,29 @@
|
|
|
282
315
|
// Handle arrows in our custom connection handler
|
|
283
316
|
const defaultEdgeOptions = {};
|
|
284
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Handle node drag start
|
|
320
|
+
*
|
|
321
|
+
* Marks the beginning of a drag operation.
|
|
322
|
+
*/
|
|
323
|
+
function handleNodeDragStart(): void {
|
|
324
|
+
isDraggingNode = true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Handle node drag stop
|
|
329
|
+
*
|
|
330
|
+
* Push the NEW state (after drag) to history.
|
|
331
|
+
* Undo will then restore to the previous state (before drag).
|
|
332
|
+
*/
|
|
333
|
+
function handleNodeDragStop(): void {
|
|
334
|
+
isDraggingNode = false;
|
|
335
|
+
// Push the current state AFTER the drag completed
|
|
336
|
+
if (currentWorkflow) {
|
|
337
|
+
workflowActions.pushHistory('Move node', currentWorkflow);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
285
341
|
/**
|
|
286
342
|
* Handle new connections between nodes
|
|
287
343
|
* Let SvelteFlow handle edge creation, styling will be applied via reactive effects
|
|
@@ -303,6 +359,12 @@
|
|
|
303
359
|
if (currentWorkflow) {
|
|
304
360
|
updateCurrentWorkflowFromSvelteFlow();
|
|
305
361
|
}
|
|
362
|
+
|
|
363
|
+
// Push to history AFTER the connection is made
|
|
364
|
+
// This way undo will restore to the state before the connection
|
|
365
|
+
if (currentWorkflow) {
|
|
366
|
+
workflowActions.pushHistory('Add connection', currentWorkflow);
|
|
367
|
+
}
|
|
306
368
|
}
|
|
307
369
|
|
|
308
370
|
/**
|
|
@@ -338,15 +400,18 @@
|
|
|
338
400
|
|
|
339
401
|
// Show native confirmation dialog
|
|
340
402
|
const confirmed = window.confirm(message);
|
|
341
|
-
|
|
403
|
+
if (!confirmed) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
342
406
|
}
|
|
343
407
|
|
|
344
|
-
//
|
|
408
|
+
// Don't push to history here - we'll push AFTER deletion in handleNodesDelete
|
|
409
|
+
// This ensures undo will restore the state before deletion
|
|
345
410
|
return true;
|
|
346
411
|
}
|
|
347
412
|
|
|
348
413
|
/**
|
|
349
|
-
* Handle node deletion - automatically remove connected edges
|
|
414
|
+
* Handle node deletion - automatically remove connected edges and push to history
|
|
350
415
|
*/
|
|
351
416
|
function handleNodesDelete(params: { nodes: WorkflowNodeType[]; edges: WorkflowEdge[] }): void {
|
|
352
417
|
const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
|
|
@@ -360,6 +425,21 @@
|
|
|
360
425
|
if (currentWorkflow) {
|
|
361
426
|
updateCurrentWorkflowFromSvelteFlow();
|
|
362
427
|
}
|
|
428
|
+
|
|
429
|
+
// Push to history AFTER the deletion so undo restores the previous state
|
|
430
|
+
const nodeCount = params.nodes.length;
|
|
431
|
+
const edgeCount = params.edges.length;
|
|
432
|
+
let description = 'Delete';
|
|
433
|
+
if (nodeCount > 0 && edgeCount > 0) {
|
|
434
|
+
description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''} and ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
|
|
435
|
+
} else if (nodeCount > 0) {
|
|
436
|
+
description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''}`;
|
|
437
|
+
} else if (edgeCount > 0) {
|
|
438
|
+
description = `Delete ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
|
|
439
|
+
}
|
|
440
|
+
if (currentWorkflow) {
|
|
441
|
+
workflowActions.pushHistory(description, currentWorkflow);
|
|
442
|
+
}
|
|
363
443
|
}
|
|
364
444
|
|
|
365
445
|
/**
|
|
@@ -413,6 +493,7 @@
|
|
|
413
493
|
const newNode = NodeOperationsHelper.createNodeFromDrop(nodeTypeData, position, flowNodes);
|
|
414
494
|
|
|
415
495
|
if (newNode && currentWorkflow) {
|
|
496
|
+
// Add the node first
|
|
416
497
|
currentWorkflow = WorkflowOperationsHelper.addNode(currentWorkflow, newNode);
|
|
417
498
|
|
|
418
499
|
// Update the global store
|
|
@@ -420,6 +501,10 @@
|
|
|
420
501
|
|
|
421
502
|
// Wait for DOM update to ensure SvelteFlow updates
|
|
422
503
|
await tick();
|
|
504
|
+
|
|
505
|
+
// Push to history AFTER adding the node
|
|
506
|
+
// This way undo will restore to the state before the add
|
|
507
|
+
workflowActions.pushHistory('Add node', currentWorkflow);
|
|
423
508
|
} else if (!currentWorkflow) {
|
|
424
509
|
console.warn('No currentWorkflow available for new node');
|
|
425
510
|
}
|
|
@@ -450,8 +535,49 @@
|
|
|
450
535
|
function handleEdgeRefreshComplete(): void {
|
|
451
536
|
nodeIdToRefresh = null;
|
|
452
537
|
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Handle keyboard shortcuts for undo/redo
|
|
541
|
+
*
|
|
542
|
+
* - Ctrl+Z (or Cmd+Z on Mac): Undo
|
|
543
|
+
* - Ctrl+Shift+Z (or Cmd+Shift+Z): Redo
|
|
544
|
+
* - Ctrl+Y (or Cmd+Y): Redo (Windows convention)
|
|
545
|
+
*/
|
|
546
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
547
|
+
// Check for Ctrl (Windows/Linux) or Cmd (Mac)
|
|
548
|
+
const isModifierPressed = event.ctrlKey || event.metaKey;
|
|
549
|
+
|
|
550
|
+
if (!isModifierPressed) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Don't handle shortcuts if user is typing in an input, textarea, or contenteditable
|
|
555
|
+
const target = event.target as HTMLElement;
|
|
556
|
+
const isInputElement =
|
|
557
|
+
target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
558
|
+
|
|
559
|
+
if (isInputElement) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Undo: Ctrl+Z (without Shift)
|
|
564
|
+
if (event.key === 'z' && !event.shiftKey) {
|
|
565
|
+
event.preventDefault();
|
|
566
|
+
historyActions.undo();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Redo: Ctrl+Shift+Z or Ctrl+Y
|
|
571
|
+
if ((event.key === 'z' && event.shiftKey) || event.key === 'y') {
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
historyActions.redo();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
453
577
|
</script>
|
|
454
578
|
|
|
579
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
580
|
+
|
|
455
581
|
<SvelteFlowProvider>
|
|
456
582
|
<!-- EdgeRefresher component - handles updateNodeInternals calls -->
|
|
457
583
|
<EdgeRefresher {nodeIdToRefresh} onRefreshComplete={handleEdgeRefreshComplete} />
|
|
@@ -471,6 +597,8 @@
|
|
|
471
597
|
onconnect={handleConnect}
|
|
472
598
|
onbeforedelete={handleBeforeDelete}
|
|
473
599
|
ondelete={handleNodesDelete}
|
|
600
|
+
onnodedragstart={handleNodeDragStart}
|
|
601
|
+
onnodedragstop={handleNodeDragStop}
|
|
474
602
|
minZoom={0.2}
|
|
475
603
|
maxZoom={3}
|
|
476
604
|
clickConnect={true}
|
|
@@ -262,7 +262,7 @@
|
|
|
262
262
|
function handleInput(event: Event): void {
|
|
263
263
|
const target = event.currentTarget as HTMLInputElement;
|
|
264
264
|
inputValue = target.value;
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
// Open dropdown
|
|
267
267
|
showDropdown();
|
|
268
268
|
|
|
@@ -493,9 +493,9 @@
|
|
|
493
493
|
*/
|
|
494
494
|
function showDropdown(): void {
|
|
495
495
|
if (!popoverElement || disabled) return;
|
|
496
|
-
|
|
496
|
+
|
|
497
497
|
updatePopoverPosition();
|
|
498
|
-
|
|
498
|
+
|
|
499
499
|
try {
|
|
500
500
|
popoverElement.showPopover();
|
|
501
501
|
isOpen = true;
|
|
@@ -510,7 +510,7 @@
|
|
|
510
510
|
*/
|
|
511
511
|
function hideDropdown(): void {
|
|
512
512
|
if (!popoverElement) return;
|
|
513
|
-
|
|
513
|
+
|
|
514
514
|
try {
|
|
515
515
|
popoverElement.hidePopover();
|
|
516
516
|
} catch {
|
|
@@ -658,11 +658,7 @@
|
|
|
658
658
|
style={popoverStyle}
|
|
659
659
|
onmousedown={(e) => e.preventDefault()}
|
|
660
660
|
>
|
|
661
|
-
<ul
|
|
662
|
-
class="form-autocomplete__listbox"
|
|
663
|
-
role="listbox"
|
|
664
|
-
aria-label="Suggestions"
|
|
665
|
-
>
|
|
661
|
+
<ul class="form-autocomplete__listbox" role="listbox" aria-label="Suggestions">
|
|
666
662
|
{#if isLoading}
|
|
667
663
|
<li class="form-autocomplete__status form-autocomplete__status--loading">
|
|
668
664
|
<Icon icon="heroicons:arrow-path" class="form-autocomplete__status-icon" />
|
|
@@ -18,13 +18,22 @@
|
|
|
18
18
|
value: string[];
|
|
19
19
|
/** Available options */
|
|
20
20
|
options: string[];
|
|
21
|
+
/** Whether the field is disabled (read-only) */
|
|
22
|
+
disabled?: boolean;
|
|
21
23
|
/** ARIA description ID */
|
|
22
24
|
ariaDescribedBy?: string;
|
|
23
25
|
/** Callback when value changes */
|
|
24
26
|
onChange: (value: string[]) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
let {
|
|
29
|
+
let {
|
|
30
|
+
id,
|
|
31
|
+
value = [],
|
|
32
|
+
options = [],
|
|
33
|
+
disabled = false,
|
|
34
|
+
ariaDescribedBy,
|
|
35
|
+
onChange
|
|
36
|
+
}: Props = $props();
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
39
|
* Handle checkbox toggle
|
|
@@ -56,6 +65,7 @@
|
|
|
56
65
|
class="form-checkbox__input"
|
|
57
66
|
value={option}
|
|
58
67
|
checked={isChecked}
|
|
68
|
+
{disabled}
|
|
59
69
|
onchange={(e) => handleCheckboxChange(option, e.currentTarget.checked)}
|
|
60
70
|
/>
|
|
61
71
|
<span class="form-checkbox__custom" aria-hidden="true">
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
height?: string;
|
|
48
48
|
/** Whether to auto-format JSON on blur */
|
|
49
49
|
autoFormat?: boolean;
|
|
50
|
+
/** Whether the field is disabled (read-only) */
|
|
51
|
+
disabled?: boolean;
|
|
50
52
|
/** ARIA description ID */
|
|
51
53
|
ariaDescribedBy?: string;
|
|
52
54
|
/** Callback when value changes */
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
darkTheme = false,
|
|
62
64
|
height = '200px',
|
|
63
65
|
autoFormat = true,
|
|
66
|
+
disabled = false,
|
|
64
67
|
ariaDescribedBy,
|
|
65
68
|
onChange
|
|
66
69
|
}: Props = $props();
|
|
@@ -167,6 +170,7 @@
|
|
|
167
170
|
/**
|
|
168
171
|
* Create editor extensions array
|
|
169
172
|
* Uses minimal setup for better performance (no auto-closing brackets, no autocompletion)
|
|
173
|
+
* When disabled is true, adds readOnly/editable so the editor cannot be modified
|
|
170
174
|
*/
|
|
171
175
|
function createExtensions() {
|
|
172
176
|
const extensions = [
|
|
@@ -177,22 +181,27 @@
|
|
|
177
181
|
highlightActiveLine(),
|
|
178
182
|
drawSelection(),
|
|
179
183
|
|
|
180
|
-
// Editing features
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
// Editing features (skip when read-only)
|
|
185
|
+
...(disabled
|
|
186
|
+
? []
|
|
187
|
+
: [
|
|
188
|
+
history(),
|
|
189
|
+
indentOnInput(),
|
|
190
|
+
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])
|
|
191
|
+
]),
|
|
192
|
+
|
|
193
|
+
// Read-only: prevent document changes and mark content as non-editable
|
|
194
|
+
...(disabled ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
|
|
183
195
|
|
|
184
196
|
// Syntax highlighting
|
|
185
197
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
186
198
|
|
|
187
|
-
// Keymaps for basic editing
|
|
188
|
-
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
|
189
|
-
|
|
190
199
|
// JSON-specific features
|
|
191
200
|
json(),
|
|
192
201
|
linter(jsonParseLinter()),
|
|
193
202
|
lintGutter(),
|
|
194
203
|
|
|
195
|
-
// Update listener
|
|
204
|
+
// Update listener (only fires on user edit when not disabled)
|
|
196
205
|
EditorView.updateListener.of(handleUpdate),
|
|
197
206
|
|
|
198
207
|
// Custom theme
|
|
@@ -13,6 +13,8 @@ interface Props {
|
|
|
13
13
|
height?: string;
|
|
14
14
|
/** Whether to auto-format JSON on blur */
|
|
15
15
|
autoFormat?: boolean;
|
|
16
|
+
/** Whether the field is disabled (read-only) */
|
|
17
|
+
disabled?: boolean;
|
|
16
18
|
/** ARIA description ID */
|
|
17
19
|
ariaDescribedBy?: string;
|
|
18
20
|
/** Callback when value changes */
|
|
@@ -61,6 +61,11 @@
|
|
|
61
61
|
|
|
62
62
|
let { fieldKey, schema, value, required = false, animationIndex = 0, onChange }: Props = $props();
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* When schema.readOnly is true, disable all inputs (no editing).
|
|
66
|
+
*/
|
|
67
|
+
const isReadOnly = $derived(schema.readOnly === true);
|
|
68
|
+
|
|
64
69
|
/**
|
|
65
70
|
* Computed description ID for ARIA association
|
|
66
71
|
*/
|
|
@@ -204,7 +209,7 @@
|
|
|
204
209
|
}
|
|
205
210
|
return value ? [String(value)] : [];
|
|
206
211
|
}
|
|
207
|
-
return String(value ??
|
|
212
|
+
return String(value ?? '');
|
|
208
213
|
});
|
|
209
214
|
</script>
|
|
210
215
|
|
|
@@ -222,6 +227,7 @@
|
|
|
222
227
|
value={arrayValue}
|
|
223
228
|
options={enumOptions}
|
|
224
229
|
ariaDescribedBy={descriptionId}
|
|
230
|
+
disabled={isReadOnly}
|
|
225
231
|
onChange={(val) => onChange(val)}
|
|
226
232
|
/>
|
|
227
233
|
{:else if fieldType === 'select-enum'}
|
|
@@ -231,6 +237,7 @@
|
|
|
231
237
|
options={enumOptions}
|
|
232
238
|
{required}
|
|
233
239
|
ariaDescribedBy={descriptionId}
|
|
240
|
+
disabled={isReadOnly}
|
|
234
241
|
onChange={(val) => onChange(val)}
|
|
235
242
|
/>
|
|
236
243
|
{:else if fieldType === 'textarea'}
|
|
@@ -240,6 +247,7 @@
|
|
|
240
247
|
placeholder={schema.placeholder ?? ''}
|
|
241
248
|
{required}
|
|
242
249
|
ariaDescribedBy={descriptionId}
|
|
250
|
+
disabled={isReadOnly}
|
|
243
251
|
onChange={(val) => onChange(val)}
|
|
244
252
|
/>
|
|
245
253
|
{:else if fieldType === 'text'}
|
|
@@ -249,6 +257,7 @@
|
|
|
249
257
|
placeholder={schema.placeholder ?? ''}
|
|
250
258
|
{required}
|
|
251
259
|
ariaDescribedBy={descriptionId}
|
|
260
|
+
disabled={isReadOnly}
|
|
252
261
|
onChange={(val) => onChange(val)}
|
|
253
262
|
/>
|
|
254
263
|
{:else if fieldType === 'number'}
|
|
@@ -261,6 +270,7 @@
|
|
|
261
270
|
step={schema.step}
|
|
262
271
|
{required}
|
|
263
272
|
ariaDescribedBy={descriptionId}
|
|
273
|
+
disabled={isReadOnly}
|
|
264
274
|
onChange={(val) => onChange(val)}
|
|
265
275
|
/>
|
|
266
276
|
{:else if fieldType === 'range'}
|
|
@@ -272,6 +282,7 @@
|
|
|
272
282
|
step={schema.step}
|
|
273
283
|
{required}
|
|
274
284
|
ariaDescribedBy={descriptionId}
|
|
285
|
+
disabled={isReadOnly}
|
|
275
286
|
onChange={(val) => onChange(val)}
|
|
276
287
|
/>
|
|
277
288
|
{:else if fieldType === 'toggle'}
|
|
@@ -279,6 +290,7 @@
|
|
|
279
290
|
id={fieldKey}
|
|
280
291
|
value={booleanValue}
|
|
281
292
|
ariaDescribedBy={descriptionId}
|
|
293
|
+
disabled={isReadOnly}
|
|
282
294
|
onChange={(val) => onChange(val)}
|
|
283
295
|
/>
|
|
284
296
|
{:else if fieldType === 'select-options'}
|
|
@@ -288,6 +300,7 @@
|
|
|
288
300
|
options={selectOptions}
|
|
289
301
|
{required}
|
|
290
302
|
ariaDescribedBy={descriptionId}
|
|
303
|
+
disabled={isReadOnly}
|
|
291
304
|
onChange={(val) => onChange(val)}
|
|
292
305
|
/>
|
|
293
306
|
{:else if fieldType === 'array' && schema.items}
|
|
@@ -298,6 +311,7 @@
|
|
|
298
311
|
minItems={schema.minItems}
|
|
299
312
|
maxItems={schema.maxItems}
|
|
300
313
|
addLabel={`Add ${schema.items.title ?? 'Item'}`}
|
|
314
|
+
disabled={isReadOnly}
|
|
301
315
|
onChange={(val) => onChange(val)}
|
|
302
316
|
/>
|
|
303
317
|
{:else if fieldType === 'code-editor'}
|
|
@@ -310,6 +324,7 @@
|
|
|
310
324
|
darkTheme={(schema.darkTheme as boolean | undefined) ?? false}
|
|
311
325
|
autoFormat={(schema.autoFormat as boolean | undefined) ?? true}
|
|
312
326
|
ariaDescribedBy={descriptionId}
|
|
327
|
+
disabled={isReadOnly}
|
|
313
328
|
onChange={(val) => onChange(val)}
|
|
314
329
|
/>
|
|
315
330
|
{:else if fieldType === 'markdown-editor'}
|
|
@@ -323,6 +338,7 @@
|
|
|
323
338
|
showStatusBar={(schema.showStatusBar as boolean | undefined) ?? true}
|
|
324
339
|
spellChecker={(schema.spellChecker as boolean | undefined) ?? false}
|
|
325
340
|
ariaDescribedBy={descriptionId}
|
|
341
|
+
disabled={isReadOnly}
|
|
326
342
|
onChange={(val) => onChange(val)}
|
|
327
343
|
/>
|
|
328
344
|
{:else if fieldType === 'template-editor'}
|
|
@@ -338,6 +354,7 @@
|
|
|
338
354
|
placeholderExample={(schema.placeholderExample as string | undefined) ??
|
|
339
355
|
'Hello {{ name }}, your order #{{ order_id }} is ready!'}
|
|
340
356
|
ariaDescribedBy={descriptionId}
|
|
357
|
+
disabled={isReadOnly}
|
|
341
358
|
onChange={(val) => onChange(val)}
|
|
342
359
|
/>
|
|
343
360
|
{:else if fieldType === 'autocomplete' && schema.autocomplete}
|
|
@@ -348,6 +365,7 @@
|
|
|
348
365
|
placeholder={schema.placeholder ?? ''}
|
|
349
366
|
{required}
|
|
350
367
|
ariaDescribedBy={descriptionId}
|
|
368
|
+
disabled={isReadOnly}
|
|
351
369
|
onChange={(val) => onChange(val)}
|
|
352
370
|
/>
|
|
353
371
|
{:else}
|
|
@@ -357,6 +375,7 @@
|
|
|
357
375
|
value={stringValue}
|
|
358
376
|
placeholder={schema.placeholder ?? ''}
|
|
359
377
|
ariaDescribedBy={descriptionId}
|
|
378
|
+
disabled={isReadOnly}
|
|
360
379
|
onChange={(val) => onChange(val)}
|
|
361
380
|
/>
|
|
362
381
|
{/if}
|