@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.
- package/README.md +45 -0
- package/dist/canvas/Canvas.svelte +10678 -0
- package/dist/canvas/Canvas.svelte.d.ts +147 -0
- package/dist/canvas/CanvasToolbar.svelte +1422 -0
- package/dist/canvas/CanvasToolbar.svelte.d.ts +54 -0
- package/dist/canvas/ContextMenu.svelte +279 -0
- package/dist/canvas/ContextMenu.svelte.d.ts +36 -0
- package/dist/canvas/PanZoomPanel.svelte +315 -0
- package/dist/canvas/PanZoomPanel.svelte.d.ts +32 -0
- package/dist/canvas/canvasLogger.d.ts +5 -0
- package/dist/canvas/canvasLogger.js +19 -0
- package/dist/canvas/index.d.ts +13 -0
- package/dist/canvas/index.js +12 -0
- package/dist/canvas/props-panel/PropsPanel.svelte +1902 -0
- package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +73 -0
- package/dist/canvas/props-panel/TextPropsICSection.svelte +238 -0
- package/dist/canvas/props-panel/TextPropsICSection.svelte.d.ts +36 -0
- package/dist/canvas/shapes/FrameShape.d.ts +97 -0
- package/dist/canvas/shapes/FrameShape.js +951 -0
- package/dist/canvas/shapes/ImageShape.d.ts +38 -0
- package/dist/canvas/shapes/ImageShape.js +245 -0
- package/dist/canvas/shapes/ShapeLibrary.d.ts +64 -0
- package/dist/canvas/shapes/ShapeLibrary.js +526 -0
- package/dist/canvas/shapes/WidgetShape.d.ts +21 -0
- package/dist/canvas/shapes/WidgetShape.js +132 -0
- package/dist/canvas/types.d.ts +26 -0
- package/dist/canvas/types.js +5 -0
- package/dist/components/Tooltip.svelte +179 -0
- package/dist/components/Tooltip.svelte.d.ts +21 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.js +13 -0
- package/dist/components/input-controls/ColorIC.svelte +388 -0
- package/dist/components/input-controls/ColorIC.svelte.d.ts +22 -0
- package/dist/components/input-controls/FontSelectorIC.svelte +69 -0
- package/dist/components/input-controls/FontSelectorIC.svelte.d.ts +17 -0
- package/dist/components/input-controls/SliderUnitIC.svelte +217 -0
- package/dist/components/input-controls/SliderUnitIC.svelte.d.ts +24 -0
- package/dist/components/input-controls/TextAlignIC.svelte +84 -0
- package/dist/components/input-controls/TextAlignIC.svelte.d.ts +17 -0
- package/dist/components/input-controls/TextStyleIC.svelte +85 -0
- package/dist/components/input-controls/TextStyleIC.svelte.d.ts +21 -0
- package/dist/icons/arrow.svg +3 -0
- package/dist/icons/checkmark.svg +3 -0
- package/dist/icons/draw.svg +22 -0
- package/dist/icons/eraser.svg +23 -0
- package/dist/icons/hand.svg +6 -0
- package/dist/icons/select.svg +3 -0
- package/dist/icons/text.svg +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- 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>
|