@widgetic/canvas 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +45 -0
  2. package/dist/canvas/Canvas.svelte +10678 -0
  3. package/dist/canvas/Canvas.svelte.d.ts +147 -0
  4. package/dist/canvas/CanvasToolbar.svelte +1422 -0
  5. package/dist/canvas/CanvasToolbar.svelte.d.ts +54 -0
  6. package/dist/canvas/ContextMenu.svelte +279 -0
  7. package/dist/canvas/ContextMenu.svelte.d.ts +36 -0
  8. package/dist/canvas/PanZoomPanel.svelte +315 -0
  9. package/dist/canvas/PanZoomPanel.svelte.d.ts +32 -0
  10. package/dist/canvas/canvasLogger.d.ts +5 -0
  11. package/dist/canvas/canvasLogger.js +19 -0
  12. package/dist/canvas/index.d.ts +13 -0
  13. package/dist/canvas/index.js +12 -0
  14. package/dist/canvas/props-panel/PropsPanel.svelte +1902 -0
  15. package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +73 -0
  16. package/dist/canvas/props-panel/TextPropsICSection.svelte +238 -0
  17. package/dist/canvas/props-panel/TextPropsICSection.svelte.d.ts +36 -0
  18. package/dist/canvas/shapes/FrameShape.d.ts +97 -0
  19. package/dist/canvas/shapes/FrameShape.js +951 -0
  20. package/dist/canvas/shapes/ImageShape.d.ts +38 -0
  21. package/dist/canvas/shapes/ImageShape.js +245 -0
  22. package/dist/canvas/shapes/ShapeLibrary.d.ts +64 -0
  23. package/dist/canvas/shapes/ShapeLibrary.js +526 -0
  24. package/dist/canvas/shapes/WidgetShape.d.ts +21 -0
  25. package/dist/canvas/shapes/WidgetShape.js +132 -0
  26. package/dist/canvas/types.d.ts +26 -0
  27. package/dist/canvas/types.js +5 -0
  28. package/dist/components/Tooltip.svelte +179 -0
  29. package/dist/components/Tooltip.svelte.d.ts +21 -0
  30. package/dist/components/index.d.ts +11 -0
  31. package/dist/components/index.js +13 -0
  32. package/dist/components/input-controls/ColorIC.svelte +388 -0
  33. package/dist/components/input-controls/ColorIC.svelte.d.ts +22 -0
  34. package/dist/components/input-controls/FontSelectorIC.svelte +69 -0
  35. package/dist/components/input-controls/FontSelectorIC.svelte.d.ts +17 -0
  36. package/dist/components/input-controls/SliderUnitIC.svelte +217 -0
  37. package/dist/components/input-controls/SliderUnitIC.svelte.d.ts +24 -0
  38. package/dist/components/input-controls/TextAlignIC.svelte +84 -0
  39. package/dist/components/input-controls/TextAlignIC.svelte.d.ts +17 -0
  40. package/dist/components/input-controls/TextStyleIC.svelte +85 -0
  41. package/dist/components/input-controls/TextStyleIC.svelte.d.ts +21 -0
  42. package/dist/icons/arrow.svg +3 -0
  43. package/dist/icons/checkmark.svg +3 -0
  44. package/dist/icons/draw.svg +22 -0
  45. package/dist/icons/eraser.svg +23 -0
  46. package/dist/icons/hand.svg +6 -0
  47. package/dist/icons/select.svg +3 -0
  48. package/dist/icons/text.svg +5 -0
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.js +12 -0
  51. package/package.json +101 -0
@@ -0,0 +1,388 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ let className = '';
5
+ export { className as class };
6
+ export let style = '';
7
+
8
+ // Current color value
9
+ export let value: string = '#000000';
10
+
11
+ // Label for the color picker
12
+ export let label: string = 'Color';
13
+
14
+ // Callback when color changes
15
+ export let onChange: (color: string) => void = () => {};
16
+
17
+ // Optional fixed palette — overrides default presets with a custom list
18
+ export let presets: string[] | null = null;
19
+
20
+ // Hide the color picker swatch + hex input row (show only label + presets)
21
+ export let showPicker: boolean = true;
22
+
23
+ // Storage key for custom presets (shared across all instances)
24
+ const PRESETS_STORAGE_KEY = 'fabric-canvas-color-presets';
25
+
26
+ // Default color presets (cannot be deleted)
27
+ const defaultPresets = [
28
+ '#000000', '#FFFFFF', '#EF4444', '#F97316', '#EAB308',
29
+ '#22C55E', '#3B82F6', '#A855F7', '#EC4899', '#6B7280',
30
+ '#1F2937', '#DC2626', '#EA580C', '#CA8A04', '#16A34A'
31
+ ];
32
+
33
+ // Custom presets (loaded from storage + added during session)
34
+ let customPresets: string[] = [];
35
+
36
+ // Combined presets: fixed palette overrides default + custom
37
+ $: allPresets = presets
38
+ ? presets
39
+ : [...defaultPresets, ...customPresets.filter(c => !defaultPresets.includes(c.toUpperCase()))];
40
+
41
+ // Local hex input value
42
+ let hexInput = value;
43
+ $: hexInput = normalizeToHex(value) || '#000000';
44
+
45
+ // Convert any color format to HEX
46
+ function normalizeToHex(color: string | null | undefined): string {
47
+ if (!color) return '#000000';
48
+
49
+ // Already HEX
50
+ if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
51
+ return color.toUpperCase();
52
+ }
53
+
54
+ // HSL format: hsl(h, s%, l%) or hsla(h, s%, l%, a)
55
+ const hslMatch = color.match(/hsla?\(([^)]+)\)/i);
56
+ if (hslMatch) {
57
+ const parts = hslMatch[1].split(',').map(s => parseFloat(s.trim()));
58
+ if (parts.length >= 3) {
59
+ const [h, s, l] = parts;
60
+ return hslToHex(h, s, l);
61
+ }
62
+ }
63
+
64
+ // RGB format: rgb(r, g, b) or rgba(r, g, b, a)
65
+ const rgbMatch = color.match(/rgba?\(([^)]+)\)/i);
66
+ if (rgbMatch) {
67
+ const parts = rgbMatch[1].split(',').map(s => parseInt(s.trim()));
68
+ if (parts.length >= 3) {
69
+ const [r, g, b] = parts;
70
+ return rgbToHex(r, g, b);
71
+ }
72
+ }
73
+
74
+ // Try as-is (browser might normalize it)
75
+ return color.toUpperCase();
76
+ }
77
+
78
+ // Convert HSL to HEX
79
+ function hslToHex(h: number, s: number, l: number): string {
80
+ s /= 100;
81
+ l /= 100;
82
+ const a = s * Math.min(l, 1 - l);
83
+ const f = (n: number) => {
84
+ const k = (n + h / 30) % 12;
85
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
86
+ return Math.round(255 * color).toString(16).padStart(2, '0');
87
+ };
88
+ return `#${f(0)}${f(8)}${f(4)}`.toUpperCase();
89
+ }
90
+
91
+ // Convert RGB to HEX
92
+ function rgbToHex(r: number, g: number, b: number): string {
93
+ const toHex = (n: number) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');
94
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
95
+ }
96
+
97
+ // Load custom presets from localStorage on mount
98
+ onMount(() => {
99
+ try {
100
+ const stored = localStorage.getItem(PRESETS_STORAGE_KEY);
101
+ if (stored) {
102
+ customPresets = JSON.parse(stored);
103
+ }
104
+ } catch (e) {
105
+ console.warn('ColorPicker: Failed to load custom presets', e);
106
+ }
107
+ });
108
+
109
+ // Save custom presets to localStorage
110
+ function saveCustomPresets() {
111
+ try {
112
+ localStorage.setItem(PRESETS_STORAGE_KEY, JSON.stringify(customPresets));
113
+ } catch (e) {
114
+ console.warn('ColorPicker: Failed to save custom presets', e);
115
+ }
116
+ }
117
+
118
+ // Add color to custom presets if not already present
119
+ function addToCustomPresets(color: string) {
120
+ const normalizedColor = normalizeToHex(color);
121
+ if (!defaultPresets.includes(normalizedColor) && !customPresets.includes(normalizedColor)) {
122
+ customPresets = [...customPresets, normalizedColor];
123
+ saveCustomPresets();
124
+ }
125
+ }
126
+
127
+ // Remove color from custom presets
128
+ function removeFromCustomPresets(color: string) {
129
+ const normalizedColor = color.toUpperCase();
130
+ customPresets = customPresets.filter(c => c.toUpperCase() !== normalizedColor);
131
+ saveCustomPresets();
132
+ }
133
+
134
+ // Check if color is a custom preset (can be deleted)
135
+ function isCustomPreset(color: string): boolean {
136
+ return !defaultPresets.includes(color.toUpperCase()) && customPresets.includes(color.toUpperCase());
137
+ }
138
+
139
+ // Handle color picker input (while dragging) - only update UI, don't save
140
+ function handleColorPickerInput(e: Event) {
141
+ const input = e.target as HTMLInputElement;
142
+ const newColor = input.value.toUpperCase();
143
+ hexInput = newColor;
144
+ onChange(newColor);
145
+ // Don't add to presets while dragging
146
+ }
147
+
148
+ // Handle color picker change (on release) - save to presets
149
+ function handleColorPickerChange(e: Event) {
150
+ const input = e.target as HTMLInputElement;
151
+ const newColor = input.value.toUpperCase();
152
+ hexInput = newColor;
153
+ onChange(newColor);
154
+ addToCustomPresets(newColor);
155
+ }
156
+
157
+ // Handle hex input change
158
+ function handleHexInput(e: Event) {
159
+ const input = e.target as HTMLInputElement;
160
+ let newValue = input.value.toUpperCase();
161
+
162
+ // Add # if missing
163
+ if (!newValue.startsWith('#')) {
164
+ newValue = '#' + newValue;
165
+ }
166
+
167
+ // Validate hex format
168
+ if (/^#[0-9A-F]{6}$/i.test(newValue)) {
169
+ hexInput = newValue;
170
+ onChange(newValue);
171
+ } else {
172
+ hexInput = newValue; // Still update input for typing
173
+ }
174
+ }
175
+
176
+ // Handle hex input blur - validate, reset if invalid, and add to presets
177
+ function handleHexBlur() {
178
+ if (/^#[0-9A-F]{6}$/i.test(hexInput)) {
179
+ addToCustomPresets(hexInput);
180
+ } else {
181
+ hexInput = normalizeToHex(value) || '#000000';
182
+ }
183
+ }
184
+
185
+ // Handle preset click
186
+ function handlePresetClick(color: string) {
187
+ const normalized = color.toUpperCase();
188
+ hexInput = normalized;
189
+ onChange(normalized);
190
+ }
191
+
192
+ // Handle delete preset click
193
+ function handleDeletePreset(e: Event, color: string) {
194
+ e.stopPropagation();
195
+ removeFromCustomPresets(color);
196
+ }
197
+ </script>
198
+
199
+ <div class="{className ? className + ' ' : ''}color-picker-component" style={style}>
200
+ <span class="prop-label">{label}</span>
201
+
202
+ {#if showPicker}
203
+ <div class="color-input-row">
204
+ <input
205
+ type="color"
206
+ value={normalizeToHex(value) || '#000000'}
207
+ oninput={handleColorPickerInput}
208
+ onchange={handleColorPickerChange}
209
+ class="color-swatch"
210
+ title="Open color picker"
211
+ />
212
+ <input
213
+ type="text"
214
+ value={hexInput}
215
+ oninput={handleHexInput}
216
+ onblur={handleHexBlur}
217
+ class="hex-input"
218
+ maxlength="7"
219
+ placeholder="#000000"
220
+ />
221
+ </div>
222
+ {/if}
223
+
224
+ <div class="presets-container overflow-x-hidden p-2 border border-gray-300 rounded-md {presets ? 'fixed-palette' : ''}"
225
+ style={presets ? `grid-template-columns: repeat(${Math.min(presets.length, 8)}, 1fr); max-height: none;` : ''}>
226
+ {#each allPresets as color}
227
+ <div class="preset-wrapper">
228
+ <button
229
+ class="preset-btn"
230
+ class:active={normalizeToHex(value) === color.toUpperCase()}
231
+ style="background-color: {color}"
232
+ title={color}
233
+ onclick={() => handlePresetClick(color)}
234
+ ></button>
235
+ {#if !presets && isCustomPreset(color)}
236
+ <button
237
+ class="delete-btn"
238
+ title="Remove color"
239
+ onclick={(e) => handleDeletePreset(e, color)}
240
+ >×</button>
241
+ {/if}
242
+ </div>
243
+ {/each}
244
+ </div>
245
+ </div>
246
+
247
+ <style>
248
+ .color-picker-component {
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: 6px;
252
+ }
253
+
254
+ .prop-label {
255
+ font-size: 11px;
256
+ font-weight: 600;
257
+ color: #6b7280;
258
+ text-transform: uppercase;
259
+ letter-spacing: 0.5px;
260
+ }
261
+
262
+ .color-input-row {
263
+ display: flex;
264
+ flex-direction: row;
265
+ gap: 8px;
266
+ align-items: center;
267
+ }
268
+
269
+ .color-swatch {
270
+ width: 32px;
271
+ height: 32px;
272
+ padding: 0;
273
+ border: 1px solid #d1d5db;
274
+ border-radius: 4px;
275
+ cursor: pointer;
276
+ flex-shrink: 0;
277
+ }
278
+
279
+ .color-swatch::-webkit-color-swatch-wrapper {
280
+ padding: 2px;
281
+ }
282
+
283
+ .color-swatch::-webkit-color-swatch {
284
+ border-radius: 2px;
285
+ border: none;
286
+ }
287
+
288
+ .hex-input {
289
+ flex: 1;
290
+ padding: 6px 8px;
291
+ border: 1px solid #d1d5db;
292
+ border-radius: 4px;
293
+ font-size: 12px;
294
+ font-family: monospace;
295
+ text-transform: uppercase;
296
+ }
297
+
298
+ .hex-input:focus {
299
+ outline: none;
300
+ border-color: var(--accent, #314F2F);
301
+ }
302
+
303
+ .presets-container {
304
+ display: grid;
305
+ grid-template-columns: repeat(5, 1fr);
306
+ gap: 6px;
307
+ /* Height for exactly 3 rows without scroll:
308
+ - 3 rows of ~30px buttons = 90px
309
+ - 2 vertical gaps of 6px = 12px
310
+ - padding top + bottom = 12px
311
+ - Total = ~114px, add some buffer for borders = 120px */
312
+ max-height: 142px;
313
+ overflow-y: auto;
314
+ padding: 6px;
315
+ }
316
+
317
+ /* Transparent scrollbar */
318
+ .presets-container::-webkit-scrollbar {
319
+ width: 6px;
320
+ }
321
+
322
+ .presets-container::-webkit-scrollbar-track {
323
+ background: transparent;
324
+ }
325
+
326
+ .presets-container::-webkit-scrollbar-thumb {
327
+ background: rgba(0, 0, 0, 0.2);
328
+ border-radius: 3px;
329
+ }
330
+
331
+ .presets-container::-webkit-scrollbar-thumb:hover {
332
+ background: rgba(0, 0, 0, 0.3);
333
+ }
334
+
335
+ .preset-wrapper {
336
+ position: relative;
337
+ }
338
+
339
+ .preset-btn {
340
+ width: 100%;
341
+ aspect-ratio: 1;
342
+ min-height: 26px;
343
+ border: 1px solid #d1d5db;
344
+ border-radius: 4px;
345
+ cursor: pointer;
346
+ /* Use outline-based hover — no layout change, no scrollbar flash */
347
+ transition: box-shadow 0.1s ease;
348
+ }
349
+
350
+ .preset-btn:hover {
351
+ /* Ring highlight: doesn't affect layout or cause overflow unlike transform: scale() */
352
+ box-shadow: 0 0 0 2px #6b7280;
353
+ z-index: 1;
354
+ }
355
+
356
+ .preset-btn.active {
357
+ box-shadow: 0 0 0 2px var(--accent, #314F2F);
358
+ }
359
+
360
+ .delete-btn {
361
+ position: absolute;
362
+ top: -4px;
363
+ right: -4px;
364
+ width: 14px;
365
+ height: 14px;
366
+ padding: 0;
367
+ border: none;
368
+ border-radius: 50%;
369
+ background: #ef4444;
370
+ color: white;
371
+ font-size: 10px;
372
+ line-height: 1;
373
+ cursor: pointer;
374
+ opacity: 0;
375
+ transition: opacity 0.15s ease;
376
+ display: flex;
377
+ align-items: center;
378
+ justify-content: center;
379
+ }
380
+
381
+ .preset-wrapper:hover .delete-btn {
382
+ opacity: 1;
383
+ }
384
+
385
+ .delete-btn:hover {
386
+ background: #dc2626;
387
+ }
388
+ </style>
@@ -0,0 +1,22 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ class?: string;
5
+ style?: string;
6
+ value?: string;
7
+ label?: string;
8
+ onChange?: (color: string) => void;
9
+ presets?: string[] | null;
10
+ showPicker?: boolean;
11
+ };
12
+ events: {
13
+ [evt: string]: CustomEvent<any>;
14
+ };
15
+ slots: {};
16
+ };
17
+ export type ColorICProps = typeof __propDef.props;
18
+ export type ColorICEvents = typeof __propDef.events;
19
+ export type ColorICSlots = typeof __propDef.slots;
20
+ export default class ColorIC extends SvelteComponentTyped<ColorICProps, ColorICEvents, ColorICSlots> {
21
+ }
22
+ export {};
@@ -0,0 +1,69 @@
1
+ <script lang="ts">
2
+ // Font Selector Input Controller - Reusable component
3
+ export let value: string = 'Arial';
4
+ export let onChange: (family: string) => void = () => {};
5
+
6
+ const fonts = [
7
+ 'Arial',
8
+ 'Helvetica',
9
+ 'Times New Roman',
10
+ 'Georgia',
11
+ 'Verdana',
12
+ 'Comic Sans MS',
13
+ 'Courier New',
14
+ 'Impact'
15
+ ];
16
+ </script>
17
+
18
+ <div class="font-selector-ic">
19
+ <span class="label">Font Family</span>
20
+ <select
21
+ class="font-select"
22
+ {value}
23
+ onchange={(e) => onChange((e.target as HTMLSelectElement).value)}
24
+ >
25
+ {#each fonts as font}
26
+ <option value={font} style="font-family: {font}">{font}</option>
27
+ {/each}
28
+ </select>
29
+ </div>
30
+
31
+ <style>
32
+ .font-selector-ic {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 6px;
36
+ }
37
+
38
+ .label {
39
+ font-size: 11px;
40
+ font-weight: 600;
41
+ color: #6b7280;
42
+ text-transform: uppercase;
43
+ letter-spacing: 0.5px;
44
+ }
45
+
46
+ .font-select {
47
+ padding: 8px 12px;
48
+ border: 2px solid #e5e7eb;
49
+ border-radius: 6px;
50
+ font-size: 14px;
51
+ background: white;
52
+ cursor: pointer;
53
+ appearance: none;
54
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
55
+ background-repeat: no-repeat;
56
+ background-position: right 12px center;
57
+ padding-right: 32px;
58
+ }
59
+
60
+ .font-select:hover {
61
+ border-color: #3b82f6;
62
+ }
63
+
64
+ .font-select:focus {
65
+ outline: none;
66
+ border-color: #3b82f6;
67
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
68
+ }
69
+ </style>
@@ -0,0 +1,17 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ value?: string;
5
+ onChange?: (family: string) => void;
6
+ };
7
+ events: {
8
+ [evt: string]: CustomEvent<any>;
9
+ };
10
+ slots: {};
11
+ };
12
+ export type FontSelectorICProps = typeof __propDef.props;
13
+ export type FontSelectorICEvents = typeof __propDef.events;
14
+ export type FontSelectorICSlots = typeof __propDef.slots;
15
+ export default class FontSelectorIC extends SvelteComponentTyped<FontSelectorICProps, FontSelectorICEvents, FontSelectorICSlots> {
16
+ }
17
+ export {};
@@ -0,0 +1,217 @@
1
+ <script lang="ts">
2
+ let className = '';
3
+ export { className as class };
4
+ export let style = '';
5
+
6
+ // Current value
7
+ export let value: number = 0;
8
+
9
+ // Label for the slider
10
+ export let label: string = 'Value';
11
+
12
+ // Min/max/step for the slider
13
+ export let min: number = 0;
14
+ export let max: number = 100;
15
+ export let step: number = 1;
16
+
17
+ // Unit to display (%, px, etc.)
18
+ export let unit: string = '%';
19
+
20
+ // Callback when value changes
21
+ export let onChange: (value: number) => void = () => {};
22
+
23
+ // Local input value for editing
24
+ let inputValue = value.toString();
25
+ $: inputValue = value.toString();
26
+
27
+ // Handle slider change
28
+ function handleSliderChange(e: Event) {
29
+ const input = e.target as HTMLInputElement;
30
+ const newValue = parseFloat(input.value);
31
+ inputValue = newValue.toString();
32
+ onChange(newValue);
33
+ }
34
+
35
+ // Handle text input change
36
+ function handleInputChange(e: Event) {
37
+ const input = e.target as HTMLInputElement;
38
+ inputValue = input.value;
39
+
40
+ const newValue = parseFloat(input.value);
41
+ if (!isNaN(newValue)) {
42
+ // Clamp value to min/max
43
+ const clampedValue = Math.min(max, Math.max(min, newValue));
44
+ onChange(clampedValue);
45
+ }
46
+ }
47
+
48
+ // Handle input blur - validate and reset if invalid
49
+ function handleInputBlur() {
50
+ const newValue = parseFloat(inputValue);
51
+ if (isNaN(newValue)) {
52
+ inputValue = value.toString();
53
+ } else {
54
+ // Clamp and update
55
+ const clampedValue = Math.min(max, Math.max(min, newValue));
56
+ inputValue = clampedValue.toString();
57
+ if (clampedValue !== value) {
58
+ onChange(clampedValue);
59
+ }
60
+ }
61
+ }
62
+
63
+ // Handle Enter key
64
+ function handleKeydown(e: KeyboardEvent) {
65
+ if (e.key === 'Enter') {
66
+ handleInputBlur();
67
+ (e.target as HTMLInputElement).blur();
68
+ }
69
+ }
70
+
71
+ // Progress percentage for the filled portion of the track
72
+ $: progressPercent = max !== min ? ((value - min) / (max - min)) * 100 : 0;
73
+ </script>
74
+
75
+ <div class="{className ? className + ' ' : ''}slider-input-component" style={style}>
76
+ <span class="prop-label">{label}</span>
77
+
78
+ <div class="slider-row">
79
+ <input
80
+ type="range"
81
+ {min}
82
+ {max}
83
+ {step}
84
+ value={value}
85
+ oninput={handleSliderChange}
86
+ class="slider"
87
+ style="--progress: {progressPercent}%"
88
+ />
89
+ <div class="value-input-wrapper">
90
+ <input
91
+ type="text"
92
+ value={inputValue}
93
+ oninput={handleInputChange}
94
+ onblur={handleInputBlur}
95
+ onkeydown={handleKeydown}
96
+ class="value-input"
97
+ />
98
+ <span class="unit">{unit}</span>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <style>
104
+ .slider-input-component {
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 6px;
108
+ }
109
+
110
+ .prop-label {
111
+ font-size: 11px;
112
+ font-weight: 600;
113
+ color: #6b7280;
114
+ text-transform: uppercase;
115
+ letter-spacing: 0.5px;
116
+ }
117
+
118
+ .slider-row {
119
+ display: flex;
120
+ flex-direction: row;
121
+ gap: 8px;
122
+ align-items: center;
123
+ }
124
+
125
+ .slider {
126
+ flex: 1;
127
+ cursor: pointer;
128
+ height: 8px;
129
+ -webkit-appearance: none;
130
+ appearance: none;
131
+ background: transparent;
132
+ /* Increase hit zone above and below the thin track line */
133
+ padding: 9px 0;
134
+ box-sizing: content-box;
135
+ margin: 0;
136
+ }
137
+
138
+ /* Track with rounded ends and progress fill */
139
+ .slider::-webkit-slider-runnable-track {
140
+ height: 8px;
141
+ border-radius: 4px;
142
+ background: linear-gradient(
143
+ to right,
144
+ var(--accent, #314F2F) 0%,
145
+ var(--accent, #314F2F) var(--progress, 0%),
146
+ #e5e7eb var(--progress, 0%),
147
+ #e5e7eb 100%
148
+ );
149
+ }
150
+
151
+ .slider::-moz-range-track {
152
+ height: 8px;
153
+ border-radius: 4px;
154
+ background: #e5e7eb;
155
+ border: none;
156
+ }
157
+
158
+ /* Firefox progress fill */
159
+ .slider::-moz-range-progress {
160
+ height: 8px;
161
+ border-radius: 4px 0 0 4px;
162
+ background: var(--accent, #314F2F);
163
+ }
164
+
165
+ .slider::-webkit-slider-thumb {
166
+ -webkit-appearance: none;
167
+ appearance: none;
168
+ width: 14px;
169
+ height: 14px;
170
+ background: var(--accent, #314F2F);
171
+ border-radius: 50%;
172
+ cursor: pointer;
173
+ margin-top: -3px;
174
+ }
175
+
176
+ .slider::-moz-range-thumb {
177
+ width: 14px;
178
+ height: 14px;
179
+ background: var(--accent, #314F2F);
180
+ border-radius: 50%;
181
+ cursor: pointer;
182
+ border: none;
183
+ }
184
+
185
+ .value-input-wrapper {
186
+ display: flex;
187
+ align-items: center;
188
+ border: 1px solid #d1d5db;
189
+ border-radius: 4px;
190
+ overflow: hidden;
191
+ min-width: 60px;
192
+ }
193
+
194
+ .value-input {
195
+ width: 35px;
196
+ padding: 4px 6px;
197
+ border: none;
198
+ font-size: 12px;
199
+ text-align: right;
200
+ outline: none;
201
+ }
202
+
203
+ .value-input:focus {
204
+ background: transparent;
205
+ }
206
+
207
+ .value-input-wrapper:focus-within {
208
+ border-color: var(--accent, #314F2F);
209
+ }
210
+
211
+ .unit {
212
+ padding: 4px 6px 4px 0;
213
+ font-size: 11px;
214
+ color: #6b7280;
215
+ background: white;
216
+ }
217
+ </style>