@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,463 @@
1
+ <!--
2
+ FormTemplateEditor Component
3
+ CodeMirror-based template editor for Twig/Liquid-style templates
4
+
5
+ Features:
6
+ - Custom syntax highlighting for {{ variable }} placeholders
7
+ - Dark/light theme support
8
+ - Consistent styling with other form components
9
+ - Line wrapping for better template readability
10
+ - Optional variable hints display
11
+ - Proper ARIA attributes for accessibility
12
+
13
+ Usage:
14
+ Use with schema format: "template" 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 {
22
+ Decoration,
23
+ type DecorationSet,
24
+ ViewPlugin,
25
+ type ViewUpdate,
26
+ MatchDecorator
27
+ } from '@codemirror/view';
28
+ import { oneDark } from '@codemirror/theme-one-dark';
29
+
30
+ interface Props {
31
+ /** Field identifier */
32
+ id: string;
33
+ /** Current template value */
34
+ value: string;
35
+ /** Placeholder text shown when empty */
36
+ placeholder?: string;
37
+ /** Whether the field is required */
38
+ required?: boolean;
39
+ /** Whether to use dark theme */
40
+ darkTheme?: boolean;
41
+ /** Editor height in pixels or CSS value */
42
+ height?: string;
43
+ /** Available variable names for hints (optional) */
44
+ variableHints?: string[];
45
+ /** Placeholder variable example for the hint */
46
+ placeholderExample?: string;
47
+ /** ARIA description ID */
48
+ ariaDescribedBy?: string;
49
+ /** Callback when value changes */
50
+ onChange: (value: string) => void;
51
+ }
52
+
53
+ let {
54
+ id,
55
+ value = '',
56
+ placeholder = 'Enter your template here...\nUse {{ variable }} for dynamic values.',
57
+ required = false,
58
+ darkTheme = false,
59
+ height = '250px',
60
+ variableHints = [],
61
+ placeholderExample = 'Hello {{ name }}, your order #{{ order_id }} is ready!',
62
+ ariaDescribedBy,
63
+ onChange
64
+ }: Props = $props();
65
+
66
+ /** Reference to the container element */
67
+ let containerRef: HTMLDivElement | undefined = $state(undefined);
68
+
69
+ /** CodeMirror editor instance */
70
+ let editorView: EditorView | undefined = $state(undefined);
71
+
72
+ /** Flag to prevent update loops */
73
+ let isInternalUpdate = false;
74
+
75
+ /**
76
+ * Create a MatchDecorator for {{ variable }} patterns
77
+ * This highlights the entire {{ variable }} expression
78
+ */
79
+ const variableMatcher = new MatchDecorator({
80
+ // Match {{ variable_name }} patterns (with optional whitespace)
81
+ regexp: /\{\{\s*[\w.]+\s*\}\}/g,
82
+ decoration: Decoration.mark({ class: 'cm-template-variable' })
83
+ });
84
+
85
+ /**
86
+ * ViewPlugin that applies the variable highlighting decorations
87
+ */
88
+ const variableHighlighter = ViewPlugin.fromClass(
89
+ class {
90
+ decorations: DecorationSet;
91
+ constructor(view: EditorView) {
92
+ this.decorations = variableMatcher.createDeco(view);
93
+ }
94
+ update(update: ViewUpdate) {
95
+ this.decorations = variableMatcher.updateDeco(update, this.decorations);
96
+ }
97
+ },
98
+ {
99
+ decorations: (v) => v.decorations
100
+ }
101
+ );
102
+
103
+ /**
104
+ * Handle editor content changes
105
+ */
106
+ function handleUpdate(update: { docChanged: boolean; state: EditorState }): void {
107
+ if (!update.docChanged || isInternalUpdate) {
108
+ return;
109
+ }
110
+
111
+ const content = update.state.doc.toString();
112
+ onChange(content);
113
+ }
114
+
115
+ /**
116
+ * Create editor extensions array for template editing
117
+ */
118
+ function createExtensions() {
119
+ const extensions = [
120
+ basicSetup,
121
+ variableHighlighter,
122
+ EditorView.updateListener.of(handleUpdate),
123
+ EditorView.theme({
124
+ '&': {
125
+ height: height,
126
+ fontSize: '0.875rem',
127
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace"
128
+ },
129
+ '.cm-scroller': {
130
+ overflow: 'auto'
131
+ },
132
+ '.cm-content': {
133
+ minHeight: '100px',
134
+ padding: '0.5rem 0'
135
+ },
136
+ '&.cm-focused': {
137
+ outline: 'none'
138
+ },
139
+ '.cm-line': {
140
+ padding: '0 0.5rem'
141
+ },
142
+ // Style for the highlighted {{ variable }} pattern
143
+ '.cm-template-variable': {
144
+ color: '#a855f7',
145
+ backgroundColor: 'rgba(168, 85, 247, 0.1)',
146
+ borderRadius: '3px',
147
+ padding: '1px 2px',
148
+ fontWeight: '500'
149
+ }
150
+ }),
151
+ EditorView.lineWrapping,
152
+ EditorState.tabSize.of(2)
153
+ ];
154
+
155
+ if (darkTheme) {
156
+ extensions.push(oneDark);
157
+ // Add dark theme override for variable highlighting
158
+ extensions.push(
159
+ EditorView.theme({
160
+ '.cm-template-variable': {
161
+ color: '#c084fc',
162
+ backgroundColor: 'rgba(192, 132, 252, 0.15)'
163
+ }
164
+ })
165
+ );
166
+ }
167
+
168
+ return extensions;
169
+ }
170
+
171
+ /**
172
+ * Insert a variable placeholder at current cursor position
173
+ */
174
+ function insertVariable(varName: string): void {
175
+ if (!editorView) {
176
+ return;
177
+ }
178
+
179
+ const insertText = `{{ ${varName} }}`;
180
+ const { from, to } = editorView.state.selection.main;
181
+
182
+ editorView.dispatch({
183
+ changes: { from, to, insert: insertText },
184
+ selection: { anchor: from + insertText.length }
185
+ });
186
+
187
+ editorView.focus();
188
+ }
189
+
190
+ /**
191
+ * Initialize CodeMirror editor on mount
192
+ */
193
+ onMount(() => {
194
+ if (!containerRef) {
195
+ return;
196
+ }
197
+
198
+ editorView = new EditorView({
199
+ state: EditorState.create({
200
+ doc: value,
201
+ extensions: createExtensions()
202
+ }),
203
+ parent: containerRef
204
+ });
205
+ });
206
+
207
+ /**
208
+ * Clean up editor on destroy
209
+ */
210
+ onDestroy(() => {
211
+ if (editorView) {
212
+ editorView.destroy();
213
+ }
214
+ });
215
+
216
+ /**
217
+ * Update editor content when value prop changes externally
218
+ */
219
+ $effect(() => {
220
+ if (!editorView) {
221
+ return;
222
+ }
223
+
224
+ const currentContent = editorView.state.doc.toString();
225
+
226
+ // Only update if content actually changed and wasn't from internal edit
227
+ if (value !== currentContent && !isInternalUpdate) {
228
+ isInternalUpdate = true;
229
+ editorView.dispatch({
230
+ changes: {
231
+ from: 0,
232
+ to: editorView.state.doc.length,
233
+ insert: value
234
+ }
235
+ });
236
+ isInternalUpdate = false;
237
+ }
238
+ });
239
+ </script>
240
+
241
+ <div class="form-template-editor">
242
+ <!-- Hidden input for form submission compatibility -->
243
+ <input
244
+ type="hidden"
245
+ {id}
246
+ name={id}
247
+ {value}
248
+ aria-describedby={ariaDescribedBy}
249
+ aria-required={required}
250
+ />
251
+
252
+ <!-- CodeMirror container -->
253
+ <div
254
+ bind:this={containerRef}
255
+ class="form-template-editor__container"
256
+ class:form-template-editor__container--dark={darkTheme}
257
+ role="textbox"
258
+ aria-multiline="true"
259
+ aria-label="Template editor"
260
+ ></div>
261
+
262
+ <!-- Variable hints section (shown when variables are available) -->
263
+ {#if variableHints.length > 0}
264
+ <div class="form-template-editor__hints">
265
+ <span class="form-template-editor__hints-label">Available variables:</span>
266
+ <div class="form-template-editor__hints-list">
267
+ {#each variableHints as varName (varName)}
268
+ <button
269
+ type="button"
270
+ class="form-template-editor__hint-btn"
271
+ onclick={() => insertVariable(varName)}
272
+ title={`Insert {{ ${varName} }}`}
273
+ >
274
+ <code>{'{{ '}{varName}{' }}'}</code>
275
+ </button>
276
+ {/each}
277
+ </div>
278
+ </div>
279
+ {/if}
280
+
281
+ <!-- Placeholder hint when empty -->
282
+ {#if !value && placeholderExample}
283
+ <div class="form-template-editor__placeholder">
284
+ <span class="form-template-editor__placeholder-label">Example template:</span>
285
+ <code class="form-template-editor__placeholder-example">{placeholderExample}</code>
286
+ </div>
287
+ {/if}
288
+
289
+ <!-- Syntax help -->
290
+ <div class="form-template-editor__help">
291
+ <svg
292
+ xmlns="http://www.w3.org/2000/svg"
293
+ viewBox="0 0 20 20"
294
+ fill="currentColor"
295
+ class="form-template-editor__help-icon"
296
+ >
297
+ <path
298
+ fill-rule="evenodd"
299
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
300
+ clip-rule="evenodd"
301
+ />
302
+ </svg>
303
+ <span
304
+ >Use <code>{'{{ variable }}'}</code> syntax to insert dynamic values from the data input</span
305
+ >
306
+ </div>
307
+ </div>
308
+
309
+ <style>
310
+ .form-template-editor {
311
+ position: relative;
312
+ width: 100%;
313
+ }
314
+
315
+ .form-template-editor__container {
316
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
317
+ border-radius: 0.5rem;
318
+ overflow: hidden;
319
+ background-color: var(--color-ref-gray-50, #f9fafb);
320
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
321
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
322
+ }
323
+
324
+ .form-template-editor__container:hover {
325
+ border-color: var(--color-ref-gray-300, #d1d5db);
326
+ background-color: #ffffff;
327
+ }
328
+
329
+ .form-template-editor__container:focus-within {
330
+ border-color: var(--color-ref-purple-500, #a855f7);
331
+ background-color: #ffffff;
332
+ box-shadow:
333
+ 0 0 0 3px rgba(168, 85, 247, 0.12),
334
+ 0 1px 2px rgba(0, 0, 0, 0.04);
335
+ }
336
+
337
+ /* Dark theme overrides */
338
+ .form-template-editor__container--dark {
339
+ background-color: #282c34;
340
+ }
341
+
342
+ .form-template-editor__container--dark:hover,
343
+ .form-template-editor__container--dark:focus-within {
344
+ background-color: #282c34;
345
+ }
346
+
347
+ /* CodeMirror styling overrides */
348
+ .form-template-editor__container :global(.cm-editor) {
349
+ border-radius: 0.5rem;
350
+ }
351
+
352
+ .form-template-editor__container :global(.cm-gutters) {
353
+ background-color: var(--color-ref-gray-100, #f3f4f6);
354
+ border-right: 1px solid var(--color-ref-gray-200, #e5e7eb);
355
+ border-radius: 0.5rem 0 0 0.5rem;
356
+ }
357
+
358
+ .form-template-editor__container--dark :global(.cm-gutters) {
359
+ background-color: #21252b;
360
+ border-right-color: #3e4451;
361
+ }
362
+
363
+ /* Variable hints section */
364
+ .form-template-editor__hints {
365
+ margin-top: 0.625rem;
366
+ padding: 0.625rem;
367
+ background-color: var(--color-ref-purple-50, #faf5ff);
368
+ border: 1px solid var(--color-ref-purple-200, #e9d5ff);
369
+ border-radius: 0.375rem;
370
+ }
371
+
372
+ .form-template-editor__hints-label {
373
+ display: block;
374
+ font-size: 0.6875rem;
375
+ font-weight: 500;
376
+ color: var(--color-ref-purple-700, #7e22ce);
377
+ text-transform: uppercase;
378
+ letter-spacing: 0.05em;
379
+ margin-bottom: 0.375rem;
380
+ }
381
+
382
+ .form-template-editor__hints-list {
383
+ display: flex;
384
+ flex-wrap: wrap;
385
+ gap: 0.375rem;
386
+ }
387
+
388
+ .form-template-editor__hint-btn {
389
+ padding: 0.25rem 0.5rem;
390
+ background-color: var(--color-ref-purple-100, #f3e8ff);
391
+ border: 1px solid var(--color-ref-purple-300, #d8b4fe);
392
+ border-radius: 0.25rem;
393
+ cursor: pointer;
394
+ transition: all 0.15s ease;
395
+ }
396
+
397
+ .form-template-editor__hint-btn:hover {
398
+ background-color: var(--color-ref-purple-200, #e9d5ff);
399
+ border-color: var(--color-ref-purple-400, #c084fc);
400
+ }
401
+
402
+ .form-template-editor__hint-btn:active {
403
+ transform: scale(0.98);
404
+ }
405
+
406
+ .form-template-editor__hint-btn code {
407
+ font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace;
408
+ font-size: 0.6875rem;
409
+ color: var(--color-ref-purple-800, #6b21a8);
410
+ }
411
+
412
+ /* Placeholder hint */
413
+ .form-template-editor__placeholder {
414
+ margin-top: 0.5rem;
415
+ padding: 0.5rem 0.75rem;
416
+ background-color: var(--color-ref-gray-50, #f9fafb);
417
+ border: 1px dashed var(--color-ref-gray-300, #d1d5db);
418
+ border-radius: 0.375rem;
419
+ }
420
+
421
+ .form-template-editor__placeholder-label {
422
+ display: block;
423
+ font-size: 0.6875rem;
424
+ font-weight: 500;
425
+ color: var(--color-ref-gray-500, #6b7280);
426
+ text-transform: uppercase;
427
+ letter-spacing: 0.05em;
428
+ margin-bottom: 0.25rem;
429
+ }
430
+
431
+ .form-template-editor__placeholder-example {
432
+ display: block;
433
+ font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace;
434
+ font-size: 0.75rem;
435
+ color: var(--color-ref-gray-700, #374151);
436
+ word-break: break-all;
437
+ }
438
+
439
+ /* Help text */
440
+ .form-template-editor__help {
441
+ display: flex;
442
+ align-items: flex-start;
443
+ gap: 0.375rem;
444
+ margin-top: 0.5rem;
445
+ font-size: 0.6875rem;
446
+ color: var(--color-ref-gray-500, #6b7280);
447
+ }
448
+
449
+ .form-template-editor__help-icon {
450
+ width: 0.875rem;
451
+ height: 0.875rem;
452
+ flex-shrink: 0;
453
+ margin-top: 0.0625rem;
454
+ }
455
+
456
+ .form-template-editor__help code {
457
+ padding: 0.0625rem 0.25rem;
458
+ background-color: var(--color-ref-gray-100, #f3f4f6);
459
+ border-radius: 0.1875rem;
460
+ font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', monospace;
461
+ font-size: 0.625rem;
462
+ }
463
+ </style>
@@ -0,0 +1,25 @@
1
+ interface Props {
2
+ /** Field identifier */
3
+ id: string;
4
+ /** Current template value */
5
+ value: string;
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
+ /** Available variable names for hints (optional) */
15
+ variableHints?: string[];
16
+ /** Placeholder variable example for the hint */
17
+ placeholderExample?: string;
18
+ /** ARIA description ID */
19
+ ariaDescribedBy?: string;
20
+ /** Callback when value changes */
21
+ onChange: (value: string) => void;
22
+ }
23
+ declare const FormTemplateEditor: import("svelte").Component<Props, {}, "">;
24
+ type FormTemplateEditor = ReturnType<typeof FormTemplateEditor>;
25
+ export default FormTemplateEditor;
@@ -40,3 +40,6 @@ export { default as FormToggle } from "./FormToggle.svelte";
40
40
  export { default as FormSelect } from "./FormSelect.svelte";
41
41
  export { default as FormCheckboxGroup } from "./FormCheckboxGroup.svelte";
42
42
  export { default as FormArray } from "./FormArray.svelte";
43
+ export { default as FormCodeEditor } from "./FormCodeEditor.svelte";
44
+ export { default as FormMarkdownEditor } from "./FormMarkdownEditor.svelte";
45
+ export { default as FormTemplateEditor } from "./FormTemplateEditor.svelte";
@@ -44,3 +44,6 @@ export { default as FormToggle } from "./FormToggle.svelte";
44
44
  export { default as FormSelect } from "./FormSelect.svelte";
45
45
  export { default as FormCheckboxGroup } from "./FormCheckboxGroup.svelte";
46
46
  export { default as FormArray } from "./FormArray.svelte";
47
+ export { default as FormCodeEditor } from "./FormCodeEditor.svelte";
48
+ export { default as FormMarkdownEditor } from "./FormMarkdownEditor.svelte";
49
+ export { default as FormTemplateEditor } from "./FormTemplateEditor.svelte";
@@ -15,8 +15,12 @@ export type FieldType = "string" | "number" | "integer" | "boolean" | "select" |
15
15
  * - multiline: Renders as textarea
16
16
  * - hidden: Field is hidden from UI but included in form submission
17
17
  * - range: Renders as range slider for numeric values
18
+ * - json: Renders as CodeMirror JSON editor
19
+ * - code: Alias for json, renders as CodeMirror editor
20
+ * - markdown: Renders as SimpleMDE Markdown editor
21
+ * - template: Renders as CodeMirror editor with Twig/Liquid syntax highlighting
18
22
  */
19
- export type FieldFormat = "multiline" | "hidden" | "range" | string;
23
+ export type FieldFormat = "multiline" | "hidden" | "range" | "json" | "code" | "markdown" | "template" | string;
20
24
  /**
21
25
  * Option type for select and checkbox group fields
22
26
  */
@@ -131,6 +135,55 @@ export interface ArrayFieldProps extends BaseFieldProps {
131
135
  addLabel?: string;
132
136
  onChange: (value: unknown[]) => void;
133
137
  }
138
+ /**
139
+ * Properties for code editor fields (CodeMirror-based)
140
+ */
141
+ export interface CodeEditorFieldProps extends BaseFieldProps {
142
+ /** Current value - can be string (raw JSON) or object */
143
+ value: unknown;
144
+ /** Whether to use dark theme */
145
+ darkTheme?: boolean;
146
+ /** Editor height in pixels or CSS value */
147
+ height?: string;
148
+ /** Whether to auto-format JSON on blur */
149
+ autoFormat?: boolean;
150
+ /** Callback when value changes */
151
+ onChange: (value: unknown) => void;
152
+ }
153
+ /**
154
+ * Properties for markdown editor fields (SimpleMDE-based)
155
+ */
156
+ export interface MarkdownEditorFieldProps extends BaseFieldProps {
157
+ /** Current value (markdown string) */
158
+ value: string;
159
+ /** Editor height in pixels or CSS value */
160
+ height?: string;
161
+ /** Whether to show the toolbar */
162
+ showToolbar?: boolean;
163
+ /** Whether to show the status bar */
164
+ showStatusBar?: boolean;
165
+ /** Whether to enable spell checking */
166
+ spellChecker?: boolean;
167
+ /** Callback when value changes */
168
+ onChange: (value: string) => void;
169
+ }
170
+ /**
171
+ * Properties for template editor fields (CodeMirror with Twig/Liquid syntax)
172
+ */
173
+ export interface TemplateEditorFieldProps extends BaseFieldProps {
174
+ /** Current template value */
175
+ value: string;
176
+ /** Whether to use dark theme */
177
+ darkTheme?: boolean;
178
+ /** Editor height in pixels or CSS value */
179
+ height?: string;
180
+ /** Available variable names for hints (optional) */
181
+ variableHints?: string[];
182
+ /** Placeholder variable example for the hint */
183
+ placeholderExample?: string;
184
+ /** Callback when value changes */
185
+ onChange: (value: string) => void;
186
+ }
134
187
  /**
135
188
  * Field schema definition derived from JSON Schema property
136
189
  * Used to determine which field component to render
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@d34dman/flowdrop",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "0.0.24",
5
+ "version": "0.0.25",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "build": "vite build && npm run prepack",
@@ -104,6 +104,7 @@
104
104
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
105
105
  "@types/marked": "^6.0.0",
106
106
  "@types/node": "^20",
107
+ "@types/uuid": "^10.0.0",
107
108
  "@vitest/browser": "^3.2.3",
108
109
  "eslint": "^9.18.0",
109
110
  "eslint-config-prettier": "^10.0.1",
@@ -125,8 +126,7 @@
125
126
  "vite": "^6.2.6",
126
127
  "vite-plugin-devtools-json": "^0.2.1",
127
128
  "vitest": "^3.2.3",
128
- "vitest-browser-svelte": "^0.1.0",
129
- "@types/uuid": "^10.0.0"
129
+ "vitest-browser-svelte": "^0.1.0"
130
130
  },
131
131
  "overrides": {
132
132
  "@sveltejs/kit": {
@@ -137,7 +137,13 @@
137
137
  "svelte"
138
138
  ],
139
139
  "dependencies": {
140
+ "@codemirror/autocomplete": "^6.20.0",
141
+ "@codemirror/lang-json": "^6.0.2",
142
+ "@codemirror/lint": "^6.9.2",
143
+ "@codemirror/theme-one-dark": "^6.1.3",
140
144
  "@xyflow/svelte": "~1.2",
145
+ "codemirror": "^6.0.2",
146
+ "easymde": "^2.20.0",
141
147
  "marked": "^16.1.1",
142
148
  "svelte-5-french-toast": "^2.0.6",
143
149
  "uuid": "^11.1.0"