@d34dman/flowdrop 0.0.24 → 0.0.25

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,415 @@
1
+ <!--
2
+ FormCodeEditor Component
3
+ CodeMirror-based code editor for JSON and other structured data
4
+
5
+ Features:
6
+ - JSON syntax highlighting with CodeMirror 6
7
+ - Real-time JSON validation with error display
8
+ - Auto-formatting on blur (optional)
9
+ - Dark/light theme support
10
+ - Consistent styling with other form components
11
+ - Proper ARIA attributes for accessibility
12
+
13
+ Usage:
14
+ Use with schema format: "json" or format: "code" to render this editor
15
+ -->
16
+
17
+ <script lang="ts">
18
+ import { onMount, onDestroy } from 'svelte';
19
+ import { EditorView, basicSetup } from 'codemirror';
20
+ import { EditorState } from '@codemirror/state';
21
+ import { json, jsonParseLinter } from '@codemirror/lang-json';
22
+ import { oneDark } from '@codemirror/theme-one-dark';
23
+ import { linter, lintGutter } from '@codemirror/lint';
24
+
25
+ interface Props {
26
+ /** Field identifier */
27
+ id: string;
28
+ /** Current value - can be string (raw JSON) or object */
29
+ value: unknown;
30
+ /** Placeholder text shown when empty */
31
+ placeholder?: string;
32
+ /** Whether the field is required */
33
+ required?: boolean;
34
+ /** Whether to use dark theme */
35
+ darkTheme?: boolean;
36
+ /** Editor height in pixels or CSS value */
37
+ height?: string;
38
+ /** Whether to auto-format JSON on blur */
39
+ autoFormat?: boolean;
40
+ /** ARIA description ID */
41
+ ariaDescribedBy?: string;
42
+ /** Callback when value changes */
43
+ onChange: (value: unknown) => void;
44
+ }
45
+
46
+ let {
47
+ id,
48
+ value = '',
49
+ placeholder = '{}',
50
+ required = false,
51
+ darkTheme = false,
52
+ height = '200px',
53
+ autoFormat = true,
54
+ ariaDescribedBy,
55
+ onChange
56
+ }: Props = $props();
57
+
58
+ /** Reference to the container element */
59
+ let containerRef: HTMLDivElement | undefined = $state(undefined);
60
+
61
+ /** CodeMirror editor instance */
62
+ let editorView: EditorView | undefined = $state(undefined);
63
+
64
+ /** Current validation error message */
65
+ let validationError: string | undefined = $state(undefined);
66
+
67
+ /** Flag to prevent update loops */
68
+ let isInternalUpdate = false;
69
+
70
+ /**
71
+ * Convert value to JSON string for editor display
72
+ */
73
+ function valueToString(val: unknown): string {
74
+ if (val === undefined || val === null) {
75
+ return '';
76
+ }
77
+ if (typeof val === 'string') {
78
+ // Check if it's already a valid JSON string
79
+ try {
80
+ JSON.parse(val);
81
+ return val;
82
+ } catch {
83
+ // Not valid JSON, return as-is
84
+ return val;
85
+ }
86
+ }
87
+ // Convert object to formatted JSON string
88
+ return JSON.stringify(val, null, 2);
89
+ }
90
+
91
+ /**
92
+ * Validate JSON and return parsed value or undefined if invalid
93
+ */
94
+ function validateAndParse(content: string): { valid: boolean; value?: unknown; error?: string } {
95
+ if (!content.trim()) {
96
+ return { valid: true, value: undefined };
97
+ }
98
+
99
+ try {
100
+ const parsed = JSON.parse(content);
101
+ return { valid: true, value: parsed };
102
+ } catch (e) {
103
+ const error = e instanceof Error ? e.message : 'Invalid JSON';
104
+ return { valid: false, error };
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Handle editor content changes
110
+ */
111
+ function handleUpdate(update: { docChanged: boolean; state: EditorState }): void {
112
+ if (!update.docChanged || isInternalUpdate) {
113
+ return;
114
+ }
115
+
116
+ const content = update.state.doc.toString();
117
+ const result = validateAndParse(content);
118
+
119
+ if (result.valid) {
120
+ validationError = undefined;
121
+ // Emit the parsed value (object) not the string
122
+ onChange(result.value);
123
+ } else {
124
+ validationError = result.error;
125
+ // Still emit the raw string so user can continue editing
126
+ onChange(content);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Format JSON content (used on blur if autoFormat is enabled)
132
+ */
133
+ function formatContent(): void {
134
+ if (!editorView || !autoFormat) {
135
+ return;
136
+ }
137
+
138
+ const content = editorView.state.doc.toString();
139
+ const result = validateAndParse(content);
140
+
141
+ if (result.valid && result.value !== undefined) {
142
+ const formatted = JSON.stringify(result.value, null, 2);
143
+ if (formatted !== content) {
144
+ isInternalUpdate = true;
145
+ editorView.dispatch({
146
+ changes: {
147
+ from: 0,
148
+ to: editorView.state.doc.length,
149
+ insert: formatted
150
+ }
151
+ });
152
+ isInternalUpdate = false;
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Create editor extensions array
159
+ */
160
+ function createExtensions() {
161
+ const extensions = [
162
+ basicSetup,
163
+ json(),
164
+ linter(jsonParseLinter()),
165
+ lintGutter(),
166
+ EditorView.updateListener.of(handleUpdate),
167
+ EditorView.theme({
168
+ '&': {
169
+ height: height,
170
+ fontSize: '0.8125rem',
171
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace"
172
+ },
173
+ '.cm-scroller': {
174
+ overflow: 'auto'
175
+ },
176
+ '.cm-content': {
177
+ minHeight: '100px'
178
+ },
179
+ '&.cm-focused': {
180
+ outline: 'none'
181
+ }
182
+ }),
183
+ EditorView.lineWrapping
184
+ ];
185
+
186
+ if (darkTheme) {
187
+ extensions.push(oneDark);
188
+ }
189
+
190
+ return extensions;
191
+ }
192
+
193
+ /**
194
+ * Initialize CodeMirror editor on mount
195
+ */
196
+ onMount(() => {
197
+ if (!containerRef) {
198
+ return;
199
+ }
200
+
201
+ const initialContent = valueToString(value);
202
+
203
+ editorView = new EditorView({
204
+ state: EditorState.create({
205
+ doc: initialContent,
206
+ extensions: createExtensions()
207
+ }),
208
+ parent: containerRef
209
+ });
210
+
211
+ // Validate initial content
212
+ if (initialContent) {
213
+ const result = validateAndParse(initialContent);
214
+ if (!result.valid) {
215
+ validationError = result.error;
216
+ }
217
+ }
218
+ });
219
+
220
+ /**
221
+ * Clean up editor on destroy
222
+ */
223
+ onDestroy(() => {
224
+ if (editorView) {
225
+ editorView.destroy();
226
+ }
227
+ });
228
+
229
+ /**
230
+ * Update editor content when value prop changes externally
231
+ */
232
+ $effect(() => {
233
+ if (!editorView) {
234
+ return;
235
+ }
236
+
237
+ const newContent = valueToString(value);
238
+ const currentContent = editorView.state.doc.toString();
239
+
240
+ // Only update if content actually changed and wasn't from internal edit
241
+ if (newContent !== currentContent && !isInternalUpdate) {
242
+ isInternalUpdate = true;
243
+ editorView.dispatch({
244
+ changes: {
245
+ from: 0,
246
+ to: editorView.state.doc.length,
247
+ insert: newContent
248
+ }
249
+ });
250
+ isInternalUpdate = false;
251
+
252
+ // Validate new content
253
+ const result = validateAndParse(newContent);
254
+ validationError = result.valid ? undefined : result.error;
255
+ }
256
+ });
257
+ </script>
258
+
259
+ <div class="form-code-editor" class:form-code-editor--error={validationError}>
260
+ <!-- Hidden input for form submission compatibility -->
261
+ <input
262
+ type="hidden"
263
+ {id}
264
+ name={id}
265
+ value={typeof value === 'string' ? value : JSON.stringify(value)}
266
+ aria-describedby={ariaDescribedBy}
267
+ aria-required={required}
268
+ />
269
+
270
+ <!-- CodeMirror container -->
271
+ <div
272
+ bind:this={containerRef}
273
+ class="form-code-editor__container"
274
+ class:form-code-editor__container--dark={darkTheme}
275
+ role="textbox"
276
+ aria-multiline="true"
277
+ aria-label="JSON editor"
278
+ onblur={formatContent}
279
+ ></div>
280
+
281
+ <!-- Validation error display -->
282
+ {#if validationError}
283
+ <div class="form-code-editor__error" role="alert">
284
+ <svg
285
+ xmlns="http://www.w3.org/2000/svg"
286
+ viewBox="0 0 20 20"
287
+ fill="currentColor"
288
+ class="form-code-editor__error-icon"
289
+ >
290
+ <path
291
+ fill-rule="evenodd"
292
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
293
+ clip-rule="evenodd"
294
+ />
295
+ </svg>
296
+ <span>{validationError}</span>
297
+ </div>
298
+ {/if}
299
+
300
+ <!-- Placeholder hint when empty -->
301
+ {#if !value && placeholder}
302
+ <div class="form-code-editor__placeholder">
303
+ Start typing or paste JSON. Example: <code>{placeholder}</code>
304
+ </div>
305
+ {/if}
306
+ </div>
307
+
308
+ <style>
309
+ .form-code-editor {
310
+ position: relative;
311
+ width: 100%;
312
+ }
313
+
314
+ .form-code-editor__container {
315
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
316
+ border-radius: 0.5rem;
317
+ overflow: hidden;
318
+ background-color: var(--color-ref-gray-50, #f9fafb);
319
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
320
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
321
+ }
322
+
323
+ .form-code-editor__container:hover {
324
+ border-color: var(--color-ref-gray-300, #d1d5db);
325
+ background-color: #ffffff;
326
+ }
327
+
328
+ .form-code-editor__container:focus-within {
329
+ border-color: var(--color-ref-blue-500, #3b82f6);
330
+ background-color: #ffffff;
331
+ box-shadow:
332
+ 0 0 0 3px rgba(59, 130, 246, 0.12),
333
+ 0 1px 2px rgba(0, 0, 0, 0.04);
334
+ }
335
+
336
+ .form-code-editor--error .form-code-editor__container {
337
+ border-color: var(--color-ref-red-400, #f87171);
338
+ }
339
+
340
+ .form-code-editor--error .form-code-editor__container:focus-within {
341
+ border-color: var(--color-ref-red-500, #ef4444);
342
+ box-shadow:
343
+ 0 0 0 3px rgba(239, 68, 68, 0.12),
344
+ 0 1px 2px rgba(0, 0, 0, 0.04);
345
+ }
346
+
347
+ /* Dark theme overrides */
348
+ .form-code-editor__container--dark {
349
+ background-color: #282c34;
350
+ }
351
+
352
+ .form-code-editor__container--dark:hover,
353
+ .form-code-editor__container--dark:focus-within {
354
+ background-color: #282c34;
355
+ }
356
+
357
+ /* CodeMirror styling overrides */
358
+ .form-code-editor__container :global(.cm-editor) {
359
+ border-radius: 0.5rem;
360
+ }
361
+
362
+ .form-code-editor__container :global(.cm-gutters) {
363
+ background-color: var(--color-ref-gray-100, #f3f4f6);
364
+ border-right: 1px solid var(--color-ref-gray-200, #e5e7eb);
365
+ border-radius: 0.5rem 0 0 0.5rem;
366
+ }
367
+
368
+ .form-code-editor__container--dark :global(.cm-gutters) {
369
+ background-color: #21252b;
370
+ border-right-color: #3e4451;
371
+ }
372
+
373
+ /* Error message */
374
+ .form-code-editor__error {
375
+ display: flex;
376
+ align-items: flex-start;
377
+ gap: 0.375rem;
378
+ margin-top: 0.5rem;
379
+ padding: 0.5rem 0.75rem;
380
+ background-color: var(--color-ref-red-50, #fef2f2);
381
+ border: 1px solid var(--color-ref-red-200, #fecaca);
382
+ border-radius: 0.375rem;
383
+ color: var(--color-ref-red-700, #b91c1c);
384
+ font-size: 0.75rem;
385
+ line-height: 1.4;
386
+ }
387
+
388
+ .form-code-editor__error-icon {
389
+ width: 1rem;
390
+ height: 1rem;
391
+ flex-shrink: 0;
392
+ margin-top: 0.0625rem;
393
+ }
394
+
395
+ .form-code-editor__error span {
396
+ word-break: break-word;
397
+ }
398
+
399
+ /* Placeholder hint */
400
+ .form-code-editor__placeholder {
401
+ margin-top: 0.5rem;
402
+ font-size: 0.75rem;
403
+ color: var(--color-ref-gray-500, #6b7280);
404
+ font-style: italic;
405
+ }
406
+
407
+ .form-code-editor__placeholder code {
408
+ padding: 0.125rem 0.375rem;
409
+ background-color: var(--color-ref-gray-100, #f3f4f6);
410
+ border-radius: 0.25rem;
411
+ font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace;
412
+ font-size: 0.6875rem;
413
+ font-style: normal;
414
+ }
415
+ </style>
@@ -0,0 +1,23 @@
1
+ interface Props {
2
+ /** Field identifier */
3
+ id: string;
4
+ /** Current value - can be string (raw JSON) or object */
5
+ value: unknown;
6
+ /** Placeholder text shown when empty */
7
+ placeholder?: string;
8
+ /** Whether the field is required */
9
+ required?: boolean;
10
+ /** Whether to use dark theme */
11
+ darkTheme?: boolean;
12
+ /** Editor height in pixels or CSS value */
13
+ height?: string;
14
+ /** Whether to auto-format JSON on blur */
15
+ autoFormat?: boolean;
16
+ /** ARIA description ID */
17
+ ariaDescribedBy?: string;
18
+ /** Callback when value changes */
19
+ onChange: (value: unknown) => void;
20
+ }
21
+ declare const FormCodeEditor: import("svelte").Component<Props, {}, "">;
22
+ type FormCodeEditor = ReturnType<typeof FormCodeEditor>;
23
+ export default FormCodeEditor;