text-formatting-toolbar.svelte RawHistoryFolder
1<script lang="ts">
2 import {
3 RETRO_FONTS,
4 DEFAULT_TEXT_STYLE,
5 type TextStyle,
6 type FontFamily
7 } from '../types/text-formatting';
8 import ColorPicker256 from './color-picker-256.svelte';
9 import { SMILEYS } from '$lib/aim/smileys';
10
11 // Props
12 let {
13 style = $bindable(DEFAULT_TEXT_STYLE),
14 compact = true,
15 showFontSelector = false,
16 onSmiley
17 } = $props<{
18 style?: TextStyle;
19 compact?: boolean;
20 showFontSelector?: boolean;
21 /** When set, shows the AIM smiley picker and inserts the chosen code */
22 onSmiley?: (code: string) => void;
23 }>();
24
25 let smileyOpen = $state(false);
26
27 // State
28 let currentColor = $state(style.color || '#000000');
29 let currentGradient = $state(style.gradient);
30
31 // Toggle functions
32 function toggleBold() {
33 style = { ...style, bold: !style.bold };
34 }
35
36 function toggleItalic() {
37 style = { ...style, italic: !style.italic };
38 }
39
40 function toggleUnderline() {
41 style = { ...style, underline: !style.underline };
42 }
43
44 function handleColorChange(color: string) {
45 currentColor = color;
46 style = { ...style, color, gradient: undefined };
47 }
48
49 function handleGradientChange(gradient: string[]) {
50 currentGradient = gradient;
51 style = { ...style, gradient, color: gradient[0] };
52 }
53
54 function selectFont(fontFamily: FontFamily) {
55 style = { ...style, fontFamily };
56 }
57
58 // Keyboard shortcuts handler
59 function handleKeydown(event: KeyboardEvent) {
60 if (event.ctrlKey || event.metaKey) {
61 switch (event.key.toLowerCase()) {
62 case 'b':
63 event.preventDefault();
64 toggleBold();
65 break;
66 case 'i':
67 event.preventDefault();
68 toggleItalic();
69 break;
70 case 'u':
71 event.preventDefault();
72 toggleUnderline();
73 break;
74 }
75 }
76 }
77
78 // Update internal state when style prop changes
79 $effect(() => {
80 currentColor = style.color || '#000000';
81 currentGradient = style.gradient;
82 });
83</script>
84
85<svelte:window onkeydown={handleKeydown} />
86
87<div class="text-formatting-toolbar sunken-panel" class:compact>
88 <!-- Font selector (if enabled) -->
89 {#if showFontSelector}
90 <select
91 value={style.fontFamily}
92 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}
93 title="Font Family"
94 >
95 {#each Object.entries(RETRO_FONTS) as [key, font] (key)}
96 <option value={key}>{font.shortName}</option>
97 {/each}
98 </select>
99 {/if}
100
101 <!-- Size slider -->
102 <div class="size-control">
103 <input
104 type="range"
105 min="8"
106 max="18"
107 bind:value={style.fontSize}
108 class="size-slider"
109 title="Font Size: {style.fontSize}px"
110 />
111 <span class="size-display">{style.fontSize}</span>
112 </div>
113
114 <!-- Style toggle buttons -->
115 <div class="style-toggles">
116 <button
117 onclick={toggleBold}
118 title="Bold (Ctrl+B)"
119 style="width: 20px; height: 20px; padding: 0; font-size: 10px;"
120 >
121 <strong>B</strong>
122 </button>
123
124 <button
125 onclick={toggleItalic}
126 title="Italic (Ctrl+I)"
127 style="width: 20px; height: 20px; padding: 0; font-size: 10px;"
128 >
129 <em>I</em>
130 </button>
131
132 <button
133 onclick={toggleUnderline}
134 title="Underline (Ctrl+U)"
135 style="width: 20px; height: 20px; padding: 0; font-size: 10px;"
136 >
137 <u>U</u>
138 </button>
139 </div>
140
141 {#if onSmiley}
142 <div class="smiley-control">
143 <button
144 type="button"
145 title="Smileys"
146 aria-haspopup="true"
147 aria-expanded={smileyOpen}
148 onclick={() => (smileyOpen = !smileyOpen)}
149 class="smiley-trigger"
150 >
151 <img src="/smileys/01.gif" alt="🙂" width="15" height="15" />
152 </button>
153 {#if smileyOpen}
154 <div class="smiley-picker" role="menu">
155 {#each SMILEYS as smiley (smiley.file)}
156 <button
157 type="button"
158 role="menuitem"
159 title="{smiley.label} {smiley.codes[0]}"
160 onclick={() => {
161 smileyOpen = false;
162 onSmiley(smiley.codes[0]);
163 }}
164 >
165 <img src="/smileys/{smiley.file}.gif" alt={smiley.emoji} width="19" height="19" />
166 </button>
167 {/each}
168 </div>
169 {/if}
170 </div>
171 {/if}
172
173 <!-- Color controls -->
174 <div class="color-controls">
175 <ColorPicker256
176 bind:selectedColor={currentColor}
177 bind:selectedGradient={currentGradient}
178 onColorChange={handleColorChange}
179 onGradientChange={handleGradientChange}
180 />
181 </div>
182</div>
183
184<style>
185 .text-formatting-toolbar {
186 display: flex;
187 align-items: center;
188 gap: 4px;
189 padding: 2px;
190 font-size: 11px;
191 position: relative;
192 flex-wrap: wrap;
193 }
194
195 .size-control {
196 display: flex;
197 align-items: center;
198 gap: 2px;
199 }
200
201 .size-slider {
202 width: 50px;
203 height: 12px;
204 }
205
206 .size-display {
207 font-size: 9px;
208 color: #666;
209 min-width: 18px;
210 text-align: center;
211 }
212
213 .style-toggles {
214 display: flex;
215 gap: 1px;
216 }
217
218 /* xp.css gives every button min-width: 75px; keep toolbar buttons square */
219 .style-toggles button,
220 .color-controls :global(.color-picker-container > button) {
221 width: 22px !important;
222 height: 20px !important;
223 min-width: 0;
224 min-height: 0;
225 padding: 0 !important;
226 display: flex;
227 align-items: center;
228 justify-content: center;
229 }
230
231 .smiley-control {
232 position: relative;
233 display: flex;
234 }
235 .smiley-trigger {
236 width: 24px;
237 height: 20px;
238 min-width: 0;
239 min-height: 0;
240 padding: 0;
241 display: flex;
242 align-items: center;
243 justify-content: center;
244 line-height: 0;
245 }
246 .smiley-control img {
247 display: block;
248 image-rendering: pixelated;
249 }
250 .smiley-picker {
251 position: absolute;
252 bottom: calc(100% + 2px);
253 left: 0;
254 z-index: 60;
255 display: grid;
256 grid-template-columns: repeat(4, 25px);
257 gap: 1px;
258 padding: 3px;
259 background: #fff;
260 border: 1px solid #7f9db9;
261 box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
262 }
263 .smiley-picker button {
264 width: 25px;
265 height: 25px;
266 min-width: 0;
267 min-height: 0;
268 padding: 0;
269 background: transparent;
270 box-shadow: none;
271 border: 1px solid transparent;
272 display: flex;
273 align-items: center;
274 justify-content: center;
275 }
276 .smiley-picker button:hover {
277 border-color: #316ac5;
278 background: #e8eefb;
279 }
280 .color-controls {
281 display: flex;
282 gap: 1px;
283 }
284
285 /* Fix font dropdown text alignment and overflow */
286 select {
287 height: 20px;
288 line-height: 1;
289 vertical-align: middle;
290 padding: 1px 21px 1px 4px;
291 font-size: 11px;
292 max-width: 120px;
293 overflow: hidden;
294 text-overflow: ellipsis;
295 white-space: nowrap;
296 }
297
298 select option {
299 padding: 2px 4px;
300 line-height: normal;
301 font-family: inherit;
302 }
303</style>