text-style-selector.svelte RawHistoryFolder
1<script lang="ts">
2 import {
3 RETRO_FONTS,
4 DEFAULT_TEXT_STYLE,
5 type TextStyle,
6 type FontFamily,
7 generateCSSStyle
8 } from '../types/text-formatting';
9 import ColorPicker256 from './color-picker-256.svelte';
10 import { formatText, createGradientText } from '../utils/text-formatter';
11
12 // Props
13 let {
14 style = $bindable(DEFAULT_TEXT_STYLE),
15 previewText = 'Sample text preview',
16 showPreview = true,
17 compact = false
18 } = $props<{
19 style?: TextStyle;
20 previewText?: string;
21 showPreview?: boolean;
22 compact?: boolean;
23 }>();
24
25 // Local state
26 let currentColor = $state(style.color || '#000000');
27 let currentGradient = $state(style.gradient);
28 let formattedPreview = $state('');
29 let isProcessing = $state(false);
30
31 // Update preview when style changes
32 $effect(() => {
33 if (!showPreview) return;
34
35 async function updatePreview() {
36 isProcessing = true;
37 try {
38 if (style.gradient && Array.isArray(style.gradient) && style.gradient.length > 1) {
39 formattedPreview = createGradientText(previewText, style.gradient, 'preview-gradient');
40 } else {
41 formattedPreview = await formatText(previewText, style, false);
42 }
43 } catch (error) {
44 console.error('Preview formatting error:', error);
45 formattedPreview = previewText;
46 } finally {
47 isProcessing = false;
48 }
49 }
50
51 updatePreview();
52 });
53
54 // Font family selection
55 function selectFont(fontFamily: FontFamily) {
56 style = { ...style, fontFamily };
57 }
58
59 // Color selection
60 function handleColorChange(color: string) {
61 currentColor = color;
62 style = { ...style, color, gradient: undefined };
63 }
64
65 // Gradient selection
66 function handleGradientChange(gradient: string[]) {
67 currentGradient = gradient;
68 style = { ...style, gradient, color: gradient[0] };
69 }
70
71 // Update internal state when style prop changes
72 $effect(() => {
73 currentColor = style.color || '#000000';
74 currentGradient = style.gradient;
75 });
76
77 // Toggle style options
78 function toggleBold() {
79 style = { ...style, bold: !style.bold };
80 }
81
82 function toggleItalic() {
83 style = { ...style, italic: !style.italic };
84 }
85
86 function toggleUnderline() {
87 style = { ...style, underline: !style.underline };
88 }
89
90 function toggleStrikethrough() {
91 style = { ...style, strikethrough: !style.strikethrough };
92 }
93
94 // Font size adjustment
95 function updateFontSize(size: number) {
96 style = { ...style, fontSize: Math.max(8, Math.min(18, size)) };
97 }
98</script>
99
100<div class="text-style-selector" class:compact>
101 <!-- Font Family Selection -->
102 <div class="style-group">
103 <label for="font-family-select">Font:</label>
104 <select
105 id="font-family-select"
106 bind:value={style.fontFamily}
107 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}
108 >
109 {#each Object.entries(RETRO_FONTS) as [key, font] (key)}
110 <option value={key} style="font-family: {font.stack}">
111 {font.name}
112 </option>
113 {/each}
114 </select>
115 </div>
116
117 <!-- Font Size -->
118 <div class="style-group">
119 <label for="font-size-range">Size:</label>
120 <input
121 id="font-size-range"
122 type="range"
123 min="8"
124 max="18"
125 bind:value={style.fontSize}
126 oninput={(e) => updateFontSize(parseInt((e.target as HTMLInputElement).value))}
127 />
128 <span class="size-display">{style.fontSize}px</span>
129 </div>
130
131 <!-- Style Toggles -->
132 <div class="style-group toggles">
133 <button
134 class="style-toggle"
135 class:active={style.bold}
136 onclick={toggleBold}
137 title="Bold (Ctrl+B)"
138 >
139 <strong>B</strong>
140 </button>
141 <button
142 class="style-toggle"
143 class:active={style.italic}
144 onclick={toggleItalic}
145 title="Italic (Ctrl+I)"
146 >
147 <em>I</em>
148 </button>
149 <button
150 class="style-toggle"
151 class:active={style.underline}
152 onclick={toggleUnderline}
153 title="Underline (Ctrl+U)"
154 >
155 <u>U</u>
156 </button>
157 <button
158 class="style-toggle"
159 class:active={style.strikethrough}
160 onclick={toggleStrikethrough}
161 title="Strikethrough"
162 >
163 <s>S</s>
164 </button>
165 </div>
166
167 <!-- Color & Gradient Selection -->
168 <div class="style-group">
169 <span class="label-text">Color:</span>
170 <div class="color-controls">
171 <ColorPicker256
172 bind:selectedColor={currentColor}
173 bind:selectedGradient={currentGradient}
174 onColorChange={handleColorChange}
175 onGradientChange={handleGradientChange}
176 />
177 </div>
178 </div>
179
180 <!-- Preview -->
181 {#if showPreview}
182 <div class="preview-section">
183 <span class="label-text">Preview:</span>
184 <div class="preview-text" style={generateCSSStyle(style)}>
185 {#if isProcessing}
186 <span class="loading">Processing...</span>
187 {:else}
188 <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedPreview is sanitized by formatText/createGradientText -->
189 {@html formattedPreview}
190 {/if}
191 </div>
192 </div>
193 {/if}
194</div>
195
196<style>
197 .text-style-selector {
198 background: #f0f0f0;
199 border: 2px inset #dfdfdf;
200 padding: 8px;
201 font-family: 'MS Sans Serif', sans-serif;
202 font-size: 11px;
203 }
204
205 .text-style-selector.compact {
206 padding: 4px;
207 }
208
209 .style-group {
210 display: flex;
211 align-items: center;
212 gap: 8px;
213 margin-bottom: 8px;
214 }
215
216 .style-group label,
217 .style-group .label-text {
218 font-weight: bold;
219 min-width: 50px;
220 }
221
222 .style-group select {
223 flex: 1;
224 padding: 2px;
225 border: 1px inset #dfdfdf;
226 background: white;
227 }
228
229 .style-group input[type='range'] {
230 flex: 1;
231 }
232
233 .size-display {
234 min-width: 35px;
235 font-size: 10px;
236 color: #666;
237 }
238
239 .toggles {
240 gap: 4px;
241 }
242
243 .style-toggle {
244 width: 24px;
245 height: 24px;
246 border: 1px outset #dfdfdf;
247 background: #f0f0f0;
248 cursor: pointer;
249 display: flex;
250 align-items: center;
251 justify-content: center;
252 font-size: 12px;
253 font-weight: bold;
254 }
255
256 .style-toggle:hover {
257 background: #e0e0e0;
258 }
259
260 .style-toggle.active {
261 border: 1px inset #dfdfdf;
262 background: #d0d0d0;
263 }
264
265 .color-controls {
266 display: flex;
267 gap: 4px;
268 }
269
270 .preview-section {
271 margin-top: 12px;
272 padding-top: 8px;
273 border-top: 1px solid #ccc;
274 }
275
276 .preview-section .label-text {
277 display: block;
278 margin-bottom: 4px;
279 font-weight: bold;
280 }
281
282 .preview-text {
283 padding: 8px;
284 border: 1px inset #dfdfdf;
285 background: white;
286 min-height: 24px;
287 line-height: 1.4;
288 }
289
290 .loading {
291 color: #666;
292 font-style: italic;
293 }
294
295 /* Gradient text support */
296 :global(.preview-gradient) {
297 display: inline;
298 }
299
300 /* Compact mode adjustments */
301 .compact .style-group {
302 margin-bottom: 4px;
303 }
304
305 .compact .style-group label,
306 .compact .style-group .label-text {
307 min-width: 40px;
308 font-size: 10px;
309 }
310
311 .compact .style-toggle {
312 width: 20px;
313 height: 20px;
314 font-size: 10px;
315 }
316</style>