@d34dman/flowdrop 0.0.21 → 0.0.22

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.
@@ -1,29 +1,46 @@
1
1
  <!--
2
2
  ConfigForm Component
3
- Handles dynamic form rendering for node configuration
3
+ Handles dynamic form rendering for node or entity configuration
4
+ Supports both node-based config and direct schema/values
4
5
  Uses reactive $state for proper Svelte 5 reactivity
6
+
7
+ Accessibility features:
8
+ - Proper label associations with for/id attributes
9
+ - ARIA describedby for field descriptions
10
+ - Focus-visible states for keyboard navigation
11
+ - Required field indicators
5
12
  -->
6
13
 
7
14
  <script lang="ts">
15
+ import Icon from '@iconify/svelte';
8
16
  import type { ConfigSchema, WorkflowNode } from '../types/index.js';
9
17
 
10
18
  interface Props {
11
- node: WorkflowNode;
19
+ /** Optional workflow node (if provided, schema and values are derived from it) */
20
+ node?: WorkflowNode;
21
+ /** Direct config schema (used when node is not provided) */
22
+ schema?: ConfigSchema;
23
+ /** Direct config values (used when node is not provided) */
24
+ values?: Record<string, unknown>;
25
+ /** Callback when form is saved */
12
26
  onSave: (config: Record<string, unknown>) => void;
27
+ /** Callback when form is cancelled */
13
28
  onCancel: () => void;
14
29
  }
15
30
 
16
- let { node, onSave, onCancel }: Props = $props();
31
+ let { node, schema, values, onSave, onCancel }: Props = $props();
17
32
 
18
33
  /**
19
- * Get the configuration schema from node metadata
34
+ * Get the configuration schema from node metadata or direct prop
20
35
  */
21
- const configSchema = $derived(node.data.metadata?.configSchema as ConfigSchema | undefined);
36
+ const configSchema = $derived(
37
+ schema ?? (node?.data.metadata?.configSchema as ConfigSchema | undefined)
38
+ );
22
39
 
23
40
  /**
24
- * Get the current node configuration
41
+ * Get the current configuration from node or direct prop
25
42
  */
26
- const nodeConfig = $derived(node.data.config || {});
43
+ const initialConfig = $derived(values ?? node?.data.config ?? {});
27
44
 
28
45
  /**
29
46
  * Create reactive configuration values using $state
@@ -32,59 +49,71 @@
32
49
  let configValues = $state<Record<string, unknown>>({});
33
50
 
34
51
  /**
35
- * Initialize config values when node or schema changes
52
+ * Initialize config values when node/schema changes
36
53
  */
37
54
  $effect(() => {
38
55
  if (configSchema?.properties) {
39
56
  const mergedConfig: Record<string, unknown> = {};
40
57
  Object.entries(configSchema.properties).forEach(([key, field]) => {
41
- const fieldConfig = field as any;
58
+ const fieldConfig = field as Record<string, unknown>;
42
59
  // Use existing value if available, otherwise use default
43
- mergedConfig[key] = nodeConfig[key] !== undefined ? nodeConfig[key] : fieldConfig.default;
60
+ mergedConfig[key] =
61
+ initialConfig[key] !== undefined ? initialConfig[key] : fieldConfig.default;
44
62
  });
45
63
  configValues = mergedConfig;
46
64
  }
47
65
  });
48
66
 
67
+ /**
68
+ * Check if a field is required based on schema
69
+ */
70
+ function isFieldRequired(key: string): boolean {
71
+ if (!configSchema?.required) return false;
72
+ return configSchema.required.includes(key);
73
+ }
74
+
49
75
  /**
50
76
  * Handle form submission
51
77
  */
52
78
  function handleSave(): void {
53
79
  // Collect all form values including hidden fields
54
- const form = document.querySelector('.flowdrop-config-sidebar__form');
80
+ const form = document.querySelector('.config-form');
55
81
  const updatedConfig: Record<string, unknown> = { ...configValues };
56
82
 
57
83
  if (form) {
58
84
  const inputs = form.querySelectorAll('input, select, textarea');
59
- inputs.forEach((input: any) => {
60
- if (input.id) {
61
- if (input.type === 'checkbox') {
62
- updatedConfig[input.id] = input.checked;
63
- } else if (input.type === 'number') {
64
- updatedConfig[input.id] = input.value ? Number(input.value) : input.value;
65
- } else if (input.type === 'hidden') {
85
+ inputs.forEach((input: Element) => {
86
+ const inputEl = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
87
+ if (inputEl.id) {
88
+ if (inputEl instanceof HTMLInputElement && inputEl.type === 'checkbox') {
89
+ updatedConfig[inputEl.id] = inputEl.checked;
90
+ } else if (inputEl instanceof HTMLInputElement && inputEl.type === 'number') {
91
+ updatedConfig[inputEl.id] = inputEl.value ? Number(inputEl.value) : inputEl.value;
92
+ } else if (inputEl instanceof HTMLInputElement && inputEl.type === 'hidden') {
66
93
  // Parse hidden field values that might be JSON
67
94
  try {
68
- const parsed = JSON.parse(input.value);
69
- updatedConfig[input.id] = parsed;
95
+ const parsed = JSON.parse(inputEl.value);
96
+ updatedConfig[inputEl.id] = parsed;
70
97
  } catch {
71
98
  // If not JSON, use raw value
72
- updatedConfig[input.id] = input.value;
99
+ updatedConfig[inputEl.id] = inputEl.value;
73
100
  }
74
101
  } else {
75
- updatedConfig[input.id] = input.value;
102
+ updatedConfig[inputEl.id] = inputEl.value;
76
103
  }
77
104
  }
78
105
  });
79
106
  }
80
107
 
81
108
  // Preserve hidden field values from original config if not collected from form
82
- if (node.data.config && configSchema?.properties) {
83
- Object.entries(configSchema.properties).forEach(([key, property]: [string, any]) => {
84
- if (property.format === 'hidden' && !(key in updatedConfig) && key in node.data.config) {
85
- updatedConfig[key] = node.data.config[key];
109
+ if (initialConfig && configSchema?.properties) {
110
+ Object.entries(configSchema.properties).forEach(
111
+ ([key, property]: [string, Record<string, unknown>]) => {
112
+ if (property.format === 'hidden' && !(key in updatedConfig) && key in initialConfig) {
113
+ updatedConfig[key] = initialConfig[key];
114
+ }
86
115
  }
87
- });
116
+ );
88
117
  }
89
118
 
90
119
  onSave(updatedConfig);
@@ -92,301 +121,741 @@
92
121
  </script>
93
122
 
94
123
  {#if configSchema}
95
- <div class="flowdrop-config-sidebar__form">
124
+ <form
125
+ class="config-form"
126
+ onsubmit={(e) => {
127
+ e.preventDefault();
128
+ handleSave();
129
+ }}
130
+ >
96
131
  {#if configSchema.properties}
97
- {#each Object.entries(configSchema.properties) as [key, field] (key)}
98
- {@const fieldConfig = field as any}
99
- {#if fieldConfig.format !== 'hidden'}
100
- <div class="flowdrop-config-sidebar__field">
101
- <label class="flowdrop-config-sidebar__field-label" for={key}>
102
- {fieldConfig.title || fieldConfig.description || key}
103
- </label>
104
- {#if fieldConfig.enum && fieldConfig.multiple}
105
- <!-- Checkboxes for enum with multiple selection -->
106
- <div class="flowdrop-config-sidebar__checkbox-group">
107
- {#each fieldConfig.enum as option (String(option))}
108
- <label class="flowdrop-config-sidebar__checkbox-item">
132
+ <div class="config-form__fields">
133
+ {#each Object.entries(configSchema.properties) as [key, field], index (key)}
134
+ {@const fieldConfig = field as Record<string, unknown>}
135
+ {@const required = isFieldRequired(key)}
136
+ {@const hasDescription = Boolean(fieldConfig.description)}
137
+ {@const descriptionId = hasDescription ? `${key}-description` : undefined}
138
+
139
+ {#if fieldConfig.format !== 'hidden'}
140
+ <div class="config-form__field" style="animation-delay: {index * 30}ms">
141
+ <!-- Field Label -->
142
+ <label class="config-form__label" for={key}>
143
+ <span class="config-form__label-text">
144
+ {String(fieldConfig.title || fieldConfig.description || key)}
145
+ </span>
146
+ {#if required}
147
+ <span class="config-form__required" aria-label="required">*</span>
148
+ {/if}
149
+ </label>
150
+
151
+ <!-- Field Input Container -->
152
+ <div class="config-form__input-wrapper">
153
+ {#if fieldConfig.enum && fieldConfig.multiple}
154
+ <!-- Checkboxes for enum with multiple selection -->
155
+ {@const enumOptions = fieldConfig.enum as string[]}
156
+ <div
157
+ class="config-form__checkbox-group"
158
+ role="group"
159
+ aria-labelledby="{key}-label"
160
+ aria-describedby={descriptionId}
161
+ >
162
+ {#each enumOptions as option (String(option))}
163
+ {@const currentValue = configValues[key]}
164
+ {@const valueArray = Array.isArray(currentValue) ? currentValue : []}
165
+ <label class="config-form__checkbox-item">
166
+ <input
167
+ type="checkbox"
168
+ class="config-form__checkbox-input"
169
+ value={String(option)}
170
+ checked={valueArray.includes(String(option))}
171
+ onchange={(e) => {
172
+ const checked = e.currentTarget.checked;
173
+ const existingValue = configValues[key];
174
+ const currentValues: unknown[] = Array.isArray(existingValue)
175
+ ? [...existingValue]
176
+ : [];
177
+ if (checked) {
178
+ if (!currentValues.includes(String(option))) {
179
+ configValues[key] = [...currentValues, String(option)];
180
+ }
181
+ } else {
182
+ configValues[key] = currentValues.filter((v) => v !== String(option));
183
+ }
184
+ }}
185
+ />
186
+ <span class="config-form__checkbox-custom" aria-hidden="true">
187
+ <Icon icon="heroicons:check" />
188
+ </span>
189
+ <span class="config-form__checkbox-label">
190
+ {String(option)}
191
+ </span>
192
+ </label>
193
+ {/each}
194
+ </div>
195
+ {:else if fieldConfig.enum}
196
+ <!-- Select for enum with single selection -->
197
+ {@const enumOptions = fieldConfig.enum as string[]}
198
+ <div class="config-form__select-wrapper">
199
+ <select
200
+ id={key}
201
+ class="config-form__select"
202
+ bind:value={configValues[key]}
203
+ aria-describedby={descriptionId}
204
+ aria-required={required}
205
+ >
206
+ {#each enumOptions as option (String(option))}
207
+ <option value={String(option)}>{String(option)}</option>
208
+ {/each}
209
+ </select>
210
+ <span class="config-form__select-icon" aria-hidden="true">
211
+ <Icon icon="heroicons:chevron-down" />
212
+ </span>
213
+ </div>
214
+ {:else if fieldConfig.type === 'string' && fieldConfig.format === 'multiline'}
215
+ <!-- Textarea for multiline strings -->
216
+ <textarea
217
+ id={key}
218
+ class="config-form__textarea"
219
+ bind:value={configValues[key]}
220
+ placeholder={String(fieldConfig.placeholder || '')}
221
+ rows="4"
222
+ aria-describedby={descriptionId}
223
+ aria-required={required}
224
+ ></textarea>
225
+ {:else if fieldConfig.type === 'string'}
226
+ <input
227
+ id={key}
228
+ type="text"
229
+ class="config-form__input"
230
+ bind:value={configValues[key]}
231
+ placeholder={String(fieldConfig.placeholder || '')}
232
+ aria-describedby={descriptionId}
233
+ aria-required={required}
234
+ />
235
+ {:else if fieldConfig.type === 'number'}
236
+ <input
237
+ id={key}
238
+ type="number"
239
+ class="config-form__input config-form__input--number"
240
+ bind:value={configValues[key]}
241
+ placeholder={String(fieldConfig.placeholder || '')}
242
+ aria-describedby={descriptionId}
243
+ aria-required={required}
244
+ />
245
+ {:else if fieldConfig.type === 'boolean'}
246
+ <!-- Toggle Switch for boolean -->
247
+ <label class="config-form__toggle">
109
248
  <input
249
+ id={key}
110
250
  type="checkbox"
111
- class="flowdrop-config-sidebar__checkbox"
112
- value={String(option)}
113
- checked={Array.isArray(configValues[key]) &&
114
- configValues[key].includes(String(option))}
251
+ class="config-form__toggle-input"
252
+ checked={Boolean(configValues[key] || fieldConfig.default || false)}
115
253
  onchange={(e) => {
116
- const checked = e.currentTarget.checked;
117
- const currentValues = Array.isArray(configValues[key])
118
- ? [...(configValues[key] as unknown[])]
119
- : [];
120
- if (checked) {
121
- if (!currentValues.includes(String(option))) {
122
- configValues[key] = [...currentValues, String(option)];
123
- }
124
- } else {
125
- configValues[key] = currentValues.filter((v) => v !== String(option));
126
- }
254
+ configValues[key] = e.currentTarget.checked;
127
255
  }}
256
+ aria-describedby={descriptionId}
128
257
  />
129
- <span class="flowdrop-config-sidebar__checkbox-label">
130
- {String(option)}
258
+ <span class="config-form__toggle-track">
259
+ <span class="config-form__toggle-thumb"></span>
260
+ </span>
261
+ <span class="config-form__toggle-label">
262
+ {configValues[key] ? 'Enabled' : 'Disabled'}
131
263
  </span>
132
264
  </label>
133
- {/each}
134
- </div>
135
- {:else if fieldConfig.enum}
136
- <!-- Select for enum with single selection -->
137
- <select
138
- id={key}
139
- class="flowdrop-config-sidebar__select"
140
- bind:value={configValues[key]}
141
- >
142
- {#each fieldConfig.enum as option (String(option))}
143
- <option value={String(option)}>{String(option)}</option>
144
- {/each}
145
- </select>
146
- {:else if fieldConfig.type === 'string' && fieldConfig.format === 'multiline'}
147
- <!-- Textarea for multiline strings -->
148
- <textarea
149
- id={key}
150
- class="flowdrop-config-sidebar__textarea"
151
- bind:value={configValues[key]}
152
- placeholder={String(fieldConfig.placeholder || '')}
153
- rows="4"
154
- ></textarea>
155
- {:else if fieldConfig.type === 'string'}
156
- <input
157
- id={key}
158
- type="text"
159
- class="flowdrop-config-sidebar__input"
160
- bind:value={configValues[key]}
161
- placeholder={String(fieldConfig.placeholder || '')}
162
- />
163
- {:else if fieldConfig.type === 'number'}
164
- <input
165
- id={key}
166
- type="number"
167
- class="flowdrop-config-sidebar__input"
168
- bind:value={configValues[key]}
169
- placeholder={String(fieldConfig.placeholder || '')}
170
- />
171
- {:else if fieldConfig.type === 'boolean'}
172
- <input
173
- id={key}
174
- type="checkbox"
175
- class="flowdrop-config-sidebar__checkbox"
176
- checked={Boolean(configValues[key] || fieldConfig.default || false)}
177
- onchange={(e) => {
178
- configValues[key] = e.currentTarget.checked;
179
- }}
180
- />
181
- {:else if fieldConfig.type === 'select' || fieldConfig.options}
182
- <select
183
- id={key}
184
- class="flowdrop-config-sidebar__select"
185
- bind:value={configValues[key]}
186
- >
187
- {#if fieldConfig.options}
188
- {#each fieldConfig.options as option (String(option.value))}
189
- {@const optionConfig = option as any}
190
- <option value={String(optionConfig.value)}>{String(optionConfig.label)}</option>
191
- {/each}
265
+ {:else if fieldConfig.type === 'select' || fieldConfig.options}
266
+ {@const selectOptions = (fieldConfig.options ?? []) as Array<{
267
+ value: unknown;
268
+ label: unknown;
269
+ }>}
270
+ <div class="config-form__select-wrapper">
271
+ <select
272
+ id={key}
273
+ class="config-form__select"
274
+ bind:value={configValues[key]}
275
+ aria-describedby={descriptionId}
276
+ aria-required={required}
277
+ >
278
+ {#each selectOptions as option (String(option.value))}
279
+ <option value={String(option.value)}>{String(option.label)}</option>
280
+ {/each}
281
+ </select>
282
+ <span class="config-form__select-icon" aria-hidden="true">
283
+ <Icon icon="heroicons:chevron-down" />
284
+ </span>
285
+ </div>
286
+ {:else}
287
+ <!-- Fallback for unknown field types -->
288
+ <input
289
+ id={key}
290
+ type="text"
291
+ class="config-form__input"
292
+ bind:value={configValues[key]}
293
+ placeholder={String(fieldConfig.placeholder || '')}
294
+ aria-describedby={descriptionId}
295
+ />
192
296
  {/if}
193
- </select>
194
- {:else}
195
- <!-- Fallback for unknown field types -->
196
- <input
197
- id={key}
198
- type="text"
199
- class="flowdrop-config-sidebar__input"
200
- bind:value={configValues[key]}
201
- placeholder={String(fieldConfig.placeholder || '')}
202
- />
203
- {/if}
204
- {#if fieldConfig.description}
205
- <p class="flowdrop-config-sidebar__field-description">
206
- {String(fieldConfig.description)}
207
- </p>
208
- {/if}
209
- </div>
210
- {/if}
211
- {/each}
297
+ </div>
298
+
299
+ <!-- Field Description -->
300
+ {#if hasDescription && fieldConfig.title}
301
+ <p id={descriptionId} class="config-form__description">
302
+ {String(fieldConfig.description)}
303
+ </p>
304
+ {/if}
305
+ </div>
306
+ {/if}
307
+ {/each}
308
+ </div>
212
309
  {:else}
213
310
  <!-- If no properties, show the raw schema for debugging -->
214
- <div class="flowdrop-config-sidebar__debug">
215
- <p><strong>Debug - Config Schema:</strong></p>
216
- <pre>{JSON.stringify(configSchema, null, 2)}</pre>
311
+ <div class="config-form__debug">
312
+ <div class="config-form__debug-header">
313
+ <Icon icon="heroicons:bug-ant" class="config-form__debug-icon" />
314
+ <span>Debug - Config Schema</span>
315
+ </div>
316
+ <pre class="config-form__debug-content">{JSON.stringify(configSchema, null, 2)}</pre>
217
317
  </div>
218
318
  {/if}
219
- </div>
220
319
 
221
- <!-- Footer -->
222
- <div class="flowdrop-config-sidebar__footer">
223
- <button
224
- class="flowdrop-config-sidebar__button flowdrop-config-sidebar__button--secondary"
225
- onclick={onCancel}
226
- >
227
- Cancel
228
- </button>
229
- <button
230
- class="flowdrop-config-sidebar__button flowdrop-config-sidebar__button--primary"
231
- onclick={handleSave}
232
- >
233
- Save Changes
234
- </button>
235
- </div>
320
+ <!-- Footer Actions -->
321
+ <div class="config-form__footer">
322
+ <button
323
+ type="button"
324
+ class="config-form__button config-form__button--secondary"
325
+ onclick={onCancel}
326
+ >
327
+ <Icon icon="heroicons:x-mark" class="config-form__button-icon" />
328
+ <span>Cancel</span>
329
+ </button>
330
+ <button type="submit" class="config-form__button config-form__button--primary">
331
+ <Icon icon="heroicons:check" class="config-form__button-icon" />
332
+ <span>Save Changes</span>
333
+ </button>
334
+ </div>
335
+ </form>
236
336
  {:else}
237
- <p class="flowdrop-config-sidebar__no-config">
238
- No configuration options available for this node.
239
- </p>
337
+ <div class="config-form__empty">
338
+ <div class="config-form__empty-icon">
339
+ <Icon icon="heroicons:cog-6-tooth" />
340
+ </div>
341
+ <p class="config-form__empty-text">No configuration options available for this node.</p>
342
+ </div>
240
343
  {/if}
241
344
 
242
345
  <style>
243
- .flowdrop-config-sidebar__form {
346
+ /* ============================================
347
+ CONFIG FORM - Modern, Accessible Design
348
+ ============================================ */
349
+
350
+ .config-form {
351
+ display: flex;
352
+ flex-direction: column;
353
+ gap: 1.5rem;
354
+ }
355
+
356
+ .config-form__fields {
244
357
  display: flex;
245
358
  flex-direction: column;
246
- gap: 1rem;
359
+ gap: 1.25rem;
247
360
  }
248
361
 
249
- .flowdrop-config-sidebar__field {
362
+ /* ============================================
363
+ FIELD CONTAINER
364
+ ============================================ */
365
+
366
+ .config-form__field {
250
367
  display: flex;
251
368
  flex-direction: column;
252
369
  gap: 0.5rem;
370
+ animation: fieldFadeIn 0.3s ease-out forwards;
371
+ opacity: 0;
372
+ transform: translateY(4px);
253
373
  }
254
374
 
255
- .flowdrop-config-sidebar__field-label {
256
- font-size: 0.875rem;
375
+ @keyframes fieldFadeIn {
376
+ to {
377
+ opacity: 1;
378
+ transform: translateY(0);
379
+ }
380
+ }
381
+
382
+ /* ============================================
383
+ LABELS
384
+ ============================================ */
385
+
386
+ .config-form__label {
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 0.25rem;
390
+ font-size: 0.8125rem;
391
+ font-weight: 600;
392
+ color: var(--color-ref-gray-700, #374151);
393
+ letter-spacing: -0.01em;
394
+ }
395
+
396
+ .config-form__label-text {
397
+ line-height: 1.4;
398
+ }
399
+
400
+ .config-form__required {
401
+ color: var(--color-ref-red-500, #ef4444);
257
402
  font-weight: 500;
258
- color: #374151;
259
403
  }
260
404
 
261
- .flowdrop-config-sidebar__input,
262
- .flowdrop-config-sidebar__select {
263
- padding: 0.5rem;
264
- border: 1px solid #d1d5db;
265
- border-radius: 0.375rem;
405
+ /* ============================================
406
+ INPUT WRAPPER
407
+ ============================================ */
408
+
409
+ .config-form__input-wrapper {
410
+ position: relative;
411
+ }
412
+
413
+ /* ============================================
414
+ TEXT INPUTS
415
+ ============================================ */
416
+
417
+ .config-form__input {
418
+ width: 100%;
419
+ padding: 0.625rem 0.875rem;
420
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
421
+ border-radius: 0.5rem;
266
422
  font-size: 0.875rem;
267
- transition:
268
- border-color 0.2s,
269
- box-shadow 0.2s;
423
+ font-family: inherit;
424
+ color: var(--color-ref-gray-900, #111827);
425
+ background-color: var(--color-ref-gray-50, #f9fafb);
426
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
427
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
428
+ }
429
+
430
+ .config-form__input::placeholder {
431
+ color: var(--color-ref-gray-400, #9ca3af);
432
+ }
433
+
434
+ .config-form__input:hover {
435
+ border-color: var(--color-ref-gray-300, #d1d5db);
436
+ background-color: #ffffff;
270
437
  }
271
438
 
272
- .flowdrop-config-sidebar__input:focus,
273
- .flowdrop-config-sidebar__select:focus {
439
+ .config-form__input:focus {
274
440
  outline: none;
275
- border-color: #3b82f6;
276
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
441
+ border-color: var(--color-ref-blue-500, #3b82f6);
442
+ background-color: #ffffff;
443
+ box-shadow:
444
+ 0 0 0 3px rgba(59, 130, 246, 0.12),
445
+ 0 1px 2px rgba(0, 0, 0, 0.04);
277
446
  }
278
447
 
279
- .flowdrop-config-sidebar__checkbox-group {
280
- display: flex;
281
- flex-direction: column;
282
- gap: 0.5rem;
448
+ .config-form__input--number {
449
+ font-variant-numeric: tabular-nums;
283
450
  }
284
451
 
285
- .flowdrop-config-sidebar__checkbox-item {
452
+ /* ============================================
453
+ TEXTAREA
454
+ ============================================ */
455
+
456
+ .config-form__textarea {
457
+ width: 100%;
458
+ padding: 0.625rem 0.875rem;
459
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
460
+ border-radius: 0.5rem;
461
+ font-size: 0.875rem;
462
+ font-family: inherit;
463
+ color: var(--color-ref-gray-900, #111827);
464
+ background-color: var(--color-ref-gray-50, #f9fafb);
465
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
466
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
467
+ resize: vertical;
468
+ min-height: 5rem;
469
+ line-height: 1.5;
470
+ }
471
+
472
+ .config-form__textarea::placeholder {
473
+ color: var(--color-ref-gray-400, #9ca3af);
474
+ }
475
+
476
+ .config-form__textarea:hover {
477
+ border-color: var(--color-ref-gray-300, #d1d5db);
478
+ background-color: #ffffff;
479
+ }
480
+
481
+ .config-form__textarea:focus {
482
+ outline: none;
483
+ border-color: var(--color-ref-blue-500, #3b82f6);
484
+ background-color: #ffffff;
485
+ box-shadow:
486
+ 0 0 0 3px rgba(59, 130, 246, 0.12),
487
+ 0 1px 2px rgba(0, 0, 0, 0.04);
488
+ }
489
+
490
+ /* ============================================
491
+ SELECT DROPDOWN
492
+ ============================================ */
493
+
494
+ .config-form__select-wrapper {
495
+ position: relative;
496
+ }
497
+
498
+ .config-form__select {
499
+ width: 100%;
500
+ padding: 0.625rem 2.5rem 0.625rem 0.875rem;
501
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
502
+ border-radius: 0.5rem;
503
+ font-size: 0.875rem;
504
+ font-family: inherit;
505
+ color: var(--color-ref-gray-900, #111827);
506
+ background-color: var(--color-ref-gray-50, #f9fafb);
507
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
508
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
509
+ cursor: pointer;
510
+ appearance: none;
511
+ }
512
+
513
+ .config-form__select:hover {
514
+ border-color: var(--color-ref-gray-300, #d1d5db);
515
+ background-color: #ffffff;
516
+ }
517
+
518
+ .config-form__select:focus {
519
+ outline: none;
520
+ border-color: var(--color-ref-blue-500, #3b82f6);
521
+ background-color: #ffffff;
522
+ box-shadow:
523
+ 0 0 0 3px rgba(59, 130, 246, 0.12),
524
+ 0 1px 2px rgba(0, 0, 0, 0.04);
525
+ }
526
+
527
+ .config-form__select-icon {
528
+ position: absolute;
529
+ right: 0.75rem;
530
+ top: 50%;
531
+ transform: translateY(-50%);
532
+ pointer-events: none;
533
+ color: var(--color-ref-gray-400, #9ca3af);
286
534
  display: flex;
287
535
  align-items: center;
288
- gap: 0.5rem;
289
- cursor: pointer;
536
+ transition: color 0.2s;
290
537
  }
291
538
 
292
- .flowdrop-config-sidebar__checkbox {
539
+ .config-form__select-icon :global(svg) {
293
540
  width: 1rem;
294
541
  height: 1rem;
295
- accent-color: #3b82f6;
542
+ }
543
+
544
+ .config-form__select:focus + .config-form__select-icon {
545
+ color: var(--color-ref-blue-500, #3b82f6);
546
+ }
547
+
548
+ /* ============================================
549
+ CHECKBOX GROUP
550
+ ============================================ */
551
+
552
+ .config-form__checkbox-group {
553
+ display: flex;
554
+ flex-direction: column;
555
+ gap: 0.625rem;
556
+ padding: 0.75rem;
557
+ background-color: var(--color-ref-gray-50, #f9fafb);
558
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
559
+ border-radius: 0.5rem;
560
+ }
561
+
562
+ .config-form__checkbox-item {
563
+ display: flex;
564
+ align-items: center;
565
+ gap: 0.625rem;
296
566
  cursor: pointer;
567
+ padding: 0.375rem;
568
+ margin: -0.375rem;
569
+ border-radius: 0.375rem;
570
+ transition: background-color 0.15s;
571
+ }
572
+
573
+ .config-form__checkbox-item:hover {
574
+ background-color: var(--color-ref-gray-100, #f3f4f6);
575
+ }
576
+
577
+ .config-form__checkbox-input {
578
+ position: absolute;
579
+ opacity: 0;
580
+ width: 0;
581
+ height: 0;
297
582
  }
298
583
 
299
- .flowdrop-config-sidebar__checkbox-label {
584
+ .config-form__checkbox-custom {
585
+ width: 1.125rem;
586
+ height: 1.125rem;
587
+ border: 1.5px solid var(--color-ref-gray-300, #d1d5db);
588
+ border-radius: 0.25rem;
589
+ background-color: #ffffff;
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: center;
593
+ transition: all 0.15s;
594
+ flex-shrink: 0;
595
+ }
596
+
597
+ .config-form__checkbox-custom :global(svg) {
598
+ width: 0.75rem;
599
+ height: 0.75rem;
600
+ color: #ffffff;
601
+ opacity: 0;
602
+ transform: scale(0.5);
603
+ transition: all 0.15s;
604
+ }
605
+
606
+ .config-form__checkbox-input:checked + .config-form__checkbox-custom {
607
+ background-color: var(--color-ref-blue-500, #3b82f6);
608
+ border-color: var(--color-ref-blue-500, #3b82f6);
609
+ }
610
+
611
+ .config-form__checkbox-input:checked + .config-form__checkbox-custom :global(svg) {
612
+ opacity: 1;
613
+ transform: scale(1);
614
+ }
615
+
616
+ .config-form__checkbox-input:focus-visible + .config-form__checkbox-custom {
617
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
618
+ }
619
+
620
+ .config-form__checkbox-label {
300
621
  font-size: 0.875rem;
301
- color: #374151;
622
+ color: var(--color-ref-gray-700, #374151);
623
+ line-height: 1.4;
624
+ }
625
+
626
+ /* ============================================
627
+ TOGGLE SWITCH (Boolean)
628
+ ============================================ */
629
+
630
+ .config-form__toggle {
631
+ display: flex;
632
+ align-items: center;
633
+ gap: 0.75rem;
302
634
  cursor: pointer;
635
+ padding: 0.5rem 0;
303
636
  }
304
637
 
305
- .flowdrop-config-sidebar__textarea {
306
- width: 100%;
307
- padding: 0.5rem 0.75rem;
308
- border: 1px solid #d1d5db;
309
- border-radius: 0.375rem;
310
- font-size: 0.875rem;
638
+ .config-form__toggle-input {
639
+ position: absolute;
640
+ opacity: 0;
641
+ width: 0;
642
+ height: 0;
643
+ }
644
+
645
+ .config-form__toggle-track {
646
+ position: relative;
647
+ width: 2.75rem;
648
+ height: 1.5rem;
649
+ background-color: var(--color-ref-gray-300, #d1d5db);
650
+ border-radius: 0.75rem;
651
+ transition: background-color 0.2s;
652
+ flex-shrink: 0;
653
+ }
654
+
655
+ .config-form__toggle-thumb {
656
+ position: absolute;
657
+ top: 0.125rem;
658
+ left: 0.125rem;
659
+ width: 1.25rem;
660
+ height: 1.25rem;
311
661
  background-color: #ffffff;
312
- transition: all 0.2s ease-in-out;
313
- resize: vertical;
314
- min-height: 4rem;
662
+ border-radius: 50%;
663
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
664
+ transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
315
665
  }
316
666
 
317
- .flowdrop-config-sidebar__textarea:focus {
318
- outline: none;
319
- border-color: #3b82f6;
320
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
667
+ .config-form__toggle-input:checked + .config-form__toggle-track {
668
+ background-color: var(--color-ref-blue-500, #3b82f6);
669
+ }
670
+
671
+ .config-form__toggle-input:checked + .config-form__toggle-track .config-form__toggle-thumb {
672
+ transform: translateX(1.25rem);
673
+ }
674
+
675
+ .config-form__toggle-input:focus-visible + .config-form__toggle-track {
676
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
677
+ }
678
+
679
+ .config-form__toggle-label {
680
+ font-size: 0.875rem;
681
+ color: var(--color-ref-gray-600, #4b5563);
682
+ font-weight: 500;
683
+ min-width: 4.5rem;
684
+ }
685
+
686
+ .config-form__toggle-input:checked ~ .config-form__toggle-label {
687
+ color: var(--color-ref-blue-600, #2563eb);
321
688
  }
322
689
 
323
- .flowdrop-config-sidebar__field-description {
690
+ /* ============================================
691
+ FIELD DESCRIPTION
692
+ ============================================ */
693
+
694
+ .config-form__description {
324
695
  margin: 0;
325
696
  font-size: 0.75rem;
326
- color: #6b7280;
327
- line-height: 1.4;
697
+ color: var(--color-ref-gray-500, #6b7280);
698
+ line-height: 1.5;
699
+ padding-left: 0.125rem;
328
700
  }
329
701
 
330
- .flowdrop-config-sidebar__no-config {
331
- text-align: center;
332
- color: #6b7280;
333
- font-style: italic;
334
- padding: 2rem 1rem;
335
- }
702
+ /* ============================================
703
+ FOOTER ACTIONS
704
+ ============================================ */
336
705
 
337
- .flowdrop-config-sidebar__footer {
706
+ .config-form__footer {
338
707
  display: flex;
339
708
  gap: 0.75rem;
340
709
  justify-content: flex-end;
341
- height: 40px;
342
- align-items: center;
710
+ padding-top: 1rem;
711
+ border-top: 1px solid var(--color-ref-gray-100, #f3f4f6);
712
+ margin-top: 0.5rem;
343
713
  }
344
714
 
345
- .flowdrop-config-sidebar__button {
346
- padding: 0.5rem 1rem;
347
- border-radius: 0.375rem;
715
+ .config-form__button {
716
+ display: inline-flex;
717
+ align-items: center;
718
+ justify-content: center;
719
+ gap: 0.5rem;
720
+ padding: 0.625rem 1rem;
721
+ border-radius: 0.5rem;
348
722
  font-size: 0.875rem;
349
- font-weight: 500;
723
+ font-weight: 600;
724
+ font-family: inherit;
350
725
  cursor: pointer;
351
- transition: all 0.2s;
726
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
352
727
  border: 1px solid transparent;
728
+ min-height: 2.5rem;
353
729
  }
354
730
 
355
- .flowdrop-config-sidebar__button--secondary {
731
+ .config-form__button :global(svg) {
732
+ width: 1rem;
733
+ height: 1rem;
734
+ flex-shrink: 0;
735
+ }
736
+
737
+ .config-form__button--secondary {
356
738
  background-color: #ffffff;
357
- border-color: #d1d5db;
358
- color: #374151;
739
+ border-color: var(--color-ref-gray-200, #e5e7eb);
740
+ color: var(--color-ref-gray-700, #374151);
741
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
359
742
  }
360
743
 
361
- .flowdrop-config-sidebar__button--secondary:hover {
362
- background-color: #f9fafb;
363
- border-color: #9ca3af;
744
+ .config-form__button--secondary:hover {
745
+ background-color: var(--color-ref-gray-50, #f9fafb);
746
+ border-color: var(--color-ref-gray-300, #d1d5db);
747
+ color: var(--color-ref-gray-900, #111827);
364
748
  }
365
749
 
366
- .flowdrop-config-sidebar__button--primary {
367
- background-color: #3b82f6;
750
+ .config-form__button--secondary:focus-visible {
751
+ outline: none;
752
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
753
+ }
754
+
755
+ .config-form__button--primary {
756
+ background: linear-gradient(
757
+ 135deg,
758
+ var(--color-ref-blue-500, #3b82f6) 0%,
759
+ var(--color-ref-blue-600, #2563eb) 100%
760
+ );
368
761
  color: #ffffff;
762
+ box-shadow:
763
+ 0 1px 3px rgba(59, 130, 246, 0.3),
764
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
369
765
  }
370
766
 
371
- .flowdrop-config-sidebar__button--primary:hover {
372
- background-color: #2563eb;
767
+ .config-form__button--primary:hover {
768
+ background: linear-gradient(
769
+ 135deg,
770
+ var(--color-ref-blue-600, #2563eb) 0%,
771
+ var(--color-ref-blue-700, #1d4ed8) 100%
772
+ );
773
+ box-shadow:
774
+ 0 4px 12px rgba(59, 130, 246, 0.35),
775
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
776
+ transform: translateY(-1px);
373
777
  }
374
778
 
375
- .flowdrop-config-sidebar__debug {
376
- background-color: #f3f4f6;
377
- border: 1px solid #d1d5db;
378
- border-radius: 0.375rem;
379
- padding: 1rem;
380
- margin: 1rem 0;
779
+ .config-form__button--primary:active {
780
+ transform: translateY(0);
381
781
  }
382
782
 
383
- .flowdrop-config-sidebar__debug pre {
384
- background-color: #ffffff;
385
- border: 1px solid #e5e7eb;
386
- border-radius: 0.25rem;
387
- padding: 0.75rem;
783
+ .config-form__button--primary:focus-visible {
784
+ outline: none;
785
+ box-shadow:
786
+ 0 0 0 3px rgba(59, 130, 246, 0.4),
787
+ 0 4px 12px rgba(59, 130, 246, 0.35);
788
+ }
789
+
790
+ /* ============================================
791
+ DEBUG SECTION
792
+ ============================================ */
793
+
794
+ .config-form__debug {
795
+ background-color: var(--color-ref-amber-50, #fffbeb);
796
+ border: 1px solid var(--color-ref-amber-200, #fde68a);
797
+ border-radius: 0.5rem;
798
+ overflow: hidden;
799
+ }
800
+
801
+ .config-form__debug-header {
802
+ display: flex;
803
+ align-items: center;
804
+ gap: 0.5rem;
805
+ padding: 0.75rem 1rem;
806
+ background-color: var(--color-ref-amber-100, #fef3c7);
807
+ border-bottom: 1px solid var(--color-ref-amber-200, #fde68a);
808
+ font-size: 0.8125rem;
809
+ font-weight: 600;
810
+ color: var(--color-ref-amber-800, #92400e);
811
+ }
812
+
813
+ .config-form__debug-header :global(svg) {
814
+ width: 1rem;
815
+ height: 1rem;
816
+ }
817
+
818
+ .config-form__debug-content {
819
+ margin: 0;
820
+ padding: 1rem;
388
821
  font-size: 0.75rem;
822
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
823
+ color: var(--color-ref-gray-700, #374151);
389
824
  overflow-x: auto;
390
- margin: 0.5rem 0 0 0;
825
+ background-color: #ffffff;
826
+ line-height: 1.5;
827
+ }
828
+
829
+ /* ============================================
830
+ EMPTY STATE
831
+ ============================================ */
832
+
833
+ .config-form__empty {
834
+ display: flex;
835
+ flex-direction: column;
836
+ align-items: center;
837
+ justify-content: center;
838
+ padding: 3rem 1.5rem;
839
+ text-align: center;
840
+ }
841
+
842
+ .config-form__empty-icon {
843
+ width: 3rem;
844
+ height: 3rem;
845
+ margin-bottom: 1rem;
846
+ color: var(--color-ref-gray-300, #d1d5db);
847
+ }
848
+
849
+ .config-form__empty-icon :global(svg) {
850
+ width: 100%;
851
+ height: 100%;
852
+ }
853
+
854
+ .config-form__empty-text {
855
+ margin: 0;
856
+ font-size: 0.875rem;
857
+ color: var(--color-ref-gray-500, #6b7280);
858
+ font-style: italic;
859
+ line-height: 1.5;
391
860
  }
392
861
  </style>