youssefreda9 commited on
Commit
d4d18df
·
1 Parent(s): 036913a

feat: Premium formatting toolbar redesign - Custom dropdown menus for font family and size (animated, styled) - Pill-shaped button groups with gradient active states - Alignment applies to paragraph with selection (standard behavior) - Undo/Redo works for typing + formatting (browser native stack) - Font preview in size dropdown - Professional visual polish

Browse files
Files changed (3) hide show
  1. src/css/components.css +134 -25
  2. src/index.html +38 -21
  3. src/js/format.js +118 -50
src/css/components.css CHANGED
@@ -288,8 +288,8 @@
288
  display: flex;
289
  flex-wrap: wrap;
290
  align-items: center;
291
- gap: 4px;
292
- padding: 6px var(--spacing-md);
293
  border-bottom: 1px solid var(--color-border);
294
  background: var(--color-surface);
295
  }
@@ -297,23 +297,32 @@
297
  .fmt-group {
298
  display: flex;
299
  align-items: center;
300
- gap: 2px;
 
 
 
 
 
 
 
 
 
301
  }
302
 
303
  .fmt-btn {
304
  display: flex;
305
  align-items: center;
306
  justify-content: center;
307
- width: 32px;
308
- height: 32px;
309
  border: none;
310
- border-radius: var(--radius-sm, 4px);
311
- background: transparent;
312
  color: var(--color-text-secondary);
313
  cursor: pointer;
314
  font-family: 'Georgia', 'Times New Roman', serif;
315
  font-size: 14px;
316
- transition: all var(--transition-base);
 
317
  }
318
 
319
  .fmt-btn:hover {
@@ -322,9 +331,8 @@
322
  }
323
 
324
  .fmt-btn.fmt-active {
325
- background: var(--color-primary);
326
- color: var(--color-text-inverse);
327
- border-radius: var(--radius-sm, 4px);
328
  }
329
 
330
  .fmt-btn svg {
@@ -333,33 +341,134 @@
333
 
334
  .fmt-divider {
335
  width: 1px;
336
- height: 24px;
337
  background: var(--color-border);
338
- margin: 0 4px;
 
339
  }
340
 
341
- .fmt-select {
342
- padding: 4px 8px;
 
 
 
 
 
 
 
 
343
  border: 1px solid var(--color-border);
344
- border-radius: var(--radius-sm, 4px);
345
  background: var(--color-surface);
346
  color: var(--color-text-primary);
347
  font-family: inherit;
348
- font-size: var(--font-size-label, 12px);
 
349
  cursor: pointer;
350
- height: 32px;
351
- min-width: 100px;
352
- transition: border-color var(--transition-base);
 
353
  }
354
 
355
- .fmt-select:hover,
356
- .fmt-select:focus {
357
  border-color: var(--color-primary);
358
- outline: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
 
361
- .fmt-select--size {
362
- min-width: 80px;
363
  }
364
 
365
  .editor-surface {
 
288
  display: flex;
289
  flex-wrap: wrap;
290
  align-items: center;
291
+ gap: 6px;
292
+ padding: 8px var(--spacing-md);
293
  border-bottom: 1px solid var(--color-border);
294
  background: var(--color-surface);
295
  }
 
297
  .fmt-group {
298
  display: flex;
299
  align-items: center;
300
+ gap: 1px;
301
+ background: var(--color-border);
302
+ border-radius: 6px;
303
+ overflow: hidden;
304
+ }
305
+
306
+ .fmt-group--font {
307
+ background: transparent;
308
+ gap: 6px;
309
+ overflow: visible;
310
  }
311
 
312
  .fmt-btn {
313
  display: flex;
314
  align-items: center;
315
  justify-content: center;
316
+ width: 34px;
317
+ height: 34px;
318
  border: none;
319
+ background: var(--color-surface);
 
320
  color: var(--color-text-secondary);
321
  cursor: pointer;
322
  font-family: 'Georgia', 'Times New Roman', serif;
323
  font-size: 14px;
324
+ transition: all 0.15s ease;
325
+ position: relative;
326
  }
327
 
328
  .fmt-btn:hover {
 
331
  }
332
 
333
  .fmt-btn.fmt-active {
334
+ background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
335
+ color: #fff;
 
336
  }
337
 
338
  .fmt-btn svg {
 
341
 
342
  .fmt-divider {
343
  width: 1px;
344
+ height: 28px;
345
  background: var(--color-border);
346
+ margin: 0 2px;
347
+ flex-shrink: 0;
348
  }
349
 
350
+ /* ── Custom Dropdowns ── */
351
+ .fmt-dropdown {
352
+ position: relative;
353
+ }
354
+
355
+ .fmt-dropdown__trigger {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 6px;
359
+ padding: 6px 10px;
360
  border: 1px solid var(--color-border);
361
+ border-radius: 6px;
362
  background: var(--color-surface);
363
  color: var(--color-text-primary);
364
  font-family: inherit;
365
+ font-size: 13px;
366
+ font-weight: 500;
367
  cursor: pointer;
368
+ height: 34px;
369
+ min-width: 120px;
370
+ transition: all 0.15s ease;
371
+ white-space: nowrap;
372
  }
373
 
374
+ .fmt-dropdown__trigger:hover {
 
375
  border-color: var(--color-primary);
376
+ background: var(--color-surface-elevated);
377
+ }
378
+
379
+ .fmt-dropdown__trigger--size {
380
+ min-width: 56px;
381
+ justify-content: center;
382
+ font-variant-numeric: tabular-nums;
383
+ font-weight: 600;
384
+ }
385
+
386
+ .fmt-dropdown__icon {
387
+ flex-shrink: 0;
388
+ color: var(--color-text-secondary);
389
+ }
390
+
391
+ .fmt-dropdown__chevron {
392
+ flex-shrink: 0;
393
+ color: var(--color-text-secondary);
394
+ transition: transform 0.2s ease;
395
+ margin-inline-start: auto;
396
+ }
397
+
398
+ .fmt-dropdown.open .fmt-dropdown__chevron {
399
+ transform: rotate(180deg);
400
+ }
401
+
402
+ .fmt-dropdown__menu {
403
+ position: absolute;
404
+ top: calc(100% + 4px);
405
+ right: 0;
406
+ min-width: 180px;
407
+ max-height: 280px;
408
+ overflow-y: auto;
409
+ background: var(--color-surface);
410
+ border: 1px solid var(--color-border);
411
+ border-radius: 8px;
412
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
413
+ padding: 4px;
414
+ z-index: 100;
415
+ opacity: 0;
416
+ visibility: hidden;
417
+ transform: translateY(-8px);
418
+ transition: all 0.2s ease;
419
+ }
420
+
421
+ .fmt-dropdown.open .fmt-dropdown__menu {
422
+ opacity: 1;
423
+ visibility: visible;
424
+ transform: translateY(0);
425
+ }
426
+
427
+ .fmt-dropdown__menu--size {
428
+ min-width: 100px;
429
+ }
430
+
431
+ .fmt-dropdown__item {
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 8px;
435
+ width: 100%;
436
+ padding: 8px 12px;
437
+ border: none;
438
+ border-radius: 6px;
439
+ background: transparent;
440
+ color: var(--color-text-primary);
441
+ font-family: inherit;
442
+ font-size: 13px;
443
+ cursor: pointer;
444
+ transition: background 0.12s ease;
445
+ text-align: right;
446
+ }
447
+
448
+ .fmt-dropdown__item:hover {
449
+ background: var(--color-surface-elevated);
450
+ }
451
+
452
+ .fmt-dropdown__item--active {
453
+ background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
454
+ color: #fff;
455
+ }
456
+
457
+ .fmt-dropdown__item--active:hover {
458
+ opacity: 0.9;
459
+ }
460
+
461
+ .fmt-size-preview {
462
+ display: inline-block;
463
+ width: 24px;
464
+ text-align: center;
465
+ font-weight: 700;
466
+ color: var(--color-text-secondary);
467
+ line-height: 1;
468
  }
469
 
470
+ .fmt-dropdown__item--active .fmt-size-preview {
471
+ color: #fff;
472
  }
473
 
474
  .editor-surface {
src/index.html CHANGED
@@ -492,6 +492,41 @@
492
  </div>
493
  <!-- Formatting Toolbar -->
494
  <div class="format-toolbar" id="format-toolbar">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  <div class="fmt-group">
496
  <button id="fmt-bold" class="fmt-btn" onclick="formatBold()" type="button" title="غامق (Ctrl+B)"><b>B</b></button>
497
  <button id="fmt-italic" class="fmt-btn" onclick="formatItalic()" type="button" title="مائل (Ctrl+I)"><i>I</i></button>
@@ -499,8 +534,9 @@
499
  <button id="fmt-strikethrough" class="fmt-btn" onclick="formatStrikethrough()" type="button" title="يتوسطه خط"><s>S</s></button>
500
  </div>
501
  <div class="fmt-divider"></div>
 
502
  <div class="fmt-group">
503
- <button id="fmt-align-right" class="fmt-btn" onclick="formatAlignRight()" type="button" title="محاذاة لليمين">
504
  <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M3 5h18v2H3V5zm4 4h14v2H7V9zm-4 4h18v2H3v-2zm4 4h14v2H7v-2z"/></svg>
505
  </button>
506
  <button id="fmt-align-center" class="fmt-btn" onclick="formatAlignCenter()" type="button" title="توسيط">
@@ -511,6 +547,7 @@
511
  </button>
512
  </div>
513
  <div class="fmt-divider"></div>
 
514
  <div class="fmt-group">
515
  <button class="fmt-btn" onclick="formatUndo()" type="button" title="تراجع (Ctrl+Z)">
516
  <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a5 5 0 015 5v2M3 10l4-4M3 10l4 4"/></svg>
@@ -519,26 +556,6 @@
519
  <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10H11a5 5 0 00-5 5v2M21 10l-4-4M21 10l-4 4"/></svg>
520
  </button>
521
  </div>
522
- <div class="fmt-divider"></div>
523
- <div class="fmt-group">
524
- <select id="fmt-font-family" class="fmt-select" title="نوع الخط">
525
- <option value="Cairo" selected>Cairo</option>
526
- <option value="Arial">Arial</option>
527
- <option value="Times New Roman">Times New Roman</option>
528
- <option value="Courier New">Courier New</option>
529
- <option value="Tahoma">Tahoma</option>
530
- <option value="Simplified Arabic">Simplified Arabic</option>
531
- <option value="Traditional Arabic">Traditional Arabic</option>
532
- </select>
533
- <select id="fmt-font-size" class="fmt-select fmt-select--size" title="حجم الخط">
534
- <option value="12px">صغير</option>
535
- <option value="16px" selected>عادي</option>
536
- <option value="20px">فوق المتوسط</option>
537
- <option value="24px">كبير</option>
538
- <option value="32px">كبير جداً</option>
539
- <option value="48px">الأقصى</option>
540
- </select>
541
- </div>
542
  </div>
543
  <input type="file" id="doc-import-input" class="sr-only" accept=".txt,.docx,text/plain,application/vnd.openxmlformats-officedocument.wordprocessingml.document" aria-hidden="true">
544
  <div id="write-area">
 
492
  </div>
493
  <!-- Formatting Toolbar -->
494
  <div class="format-toolbar" id="format-toolbar">
495
+ <!-- Font group -->
496
+ <div class="fmt-group fmt-group--font">
497
+ <div class="fmt-dropdown" id="fmt-font-wrap">
498
+ <button class="fmt-dropdown__trigger" type="button" id="fmt-font-trigger" aria-haspopup="true" aria-expanded="false">
499
+ <svg class="fmt-dropdown__icon" width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7V4h16v3M9 20h6M12 4v16"/></svg>
500
+ <span id="fmt-font-label">Cairo</span>
501
+ <svg class="fmt-dropdown__chevron" width="10" height="10" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>
502
+ </button>
503
+ <div class="fmt-dropdown__menu" id="fmt-font-menu" role="menu">
504
+ <button class="fmt-dropdown__item" data-font="Cairo" role="menuitem" style="font-family:Cairo">Cairo</button>
505
+ <button class="fmt-dropdown__item" data-font="Arial" role="menuitem" style="font-family:Arial">Arial</button>
506
+ <button class="fmt-dropdown__item" data-font="Times New Roman" role="menuitem" style="font-family:'Times New Roman'">Times New Roman</button>
507
+ <button class="fmt-dropdown__item" data-font="Tahoma" role="menuitem" style="font-family:Tahoma">Tahoma</button>
508
+ <button class="fmt-dropdown__item" data-font="Simplified Arabic" role="menuitem" style="font-family:'Simplified Arabic'">Simplified Arabic</button>
509
+ <button class="fmt-dropdown__item" data-font="Traditional Arabic" role="menuitem" style="font-family:'Traditional Arabic'">Traditional Arabic</button>
510
+ <button class="fmt-dropdown__item" data-font="Courier New" role="menuitem" style="font-family:'Courier New'">Courier New</button>
511
+ </div>
512
+ </div>
513
+ <div class="fmt-dropdown" id="fmt-size-wrap">
514
+ <button class="fmt-dropdown__trigger fmt-dropdown__trigger--size" type="button" id="fmt-size-trigger" aria-haspopup="true" aria-expanded="false">
515
+ <span id="fmt-size-label">16</span>
516
+ <svg class="fmt-dropdown__chevron" width="10" height="10" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>
517
+ </button>
518
+ <div class="fmt-dropdown__menu fmt-dropdown__menu--size" id="fmt-size-menu" role="menu">
519
+ <button class="fmt-dropdown__item" data-size="12px" role="menuitem"><span class="fmt-size-preview" style="font-size:12px">أ</span> 12</button>
520
+ <button class="fmt-dropdown__item fmt-dropdown__item--active" data-size="16px" role="menuitem"><span class="fmt-size-preview" style="font-size:16px">أ</span> 16</button>
521
+ <button class="fmt-dropdown__item" data-size="20px" role="menuitem"><span class="fmt-size-preview" style="font-size:18px">أ</span> 20</button>
522
+ <button class="fmt-dropdown__item" data-size="24px" role="menuitem"><span class="fmt-size-preview" style="font-size:20px">أ</span> 24</button>
523
+ <button class="fmt-dropdown__item" data-size="32px" role="menuitem"><span class="fmt-size-preview" style="font-size:24px">أ</span> 32</button>
524
+ <button class="fmt-dropdown__item" data-size="48px" role="menuitem"><span class="fmt-size-preview" style="font-size:28px">أ</span> 48</button>
525
+ </div>
526
+ </div>
527
+ </div>
528
+ <div class="fmt-divider"></div>
529
+ <!-- Text style group -->
530
  <div class="fmt-group">
531
  <button id="fmt-bold" class="fmt-btn" onclick="formatBold()" type="button" title="غامق (Ctrl+B)"><b>B</b></button>
532
  <button id="fmt-italic" class="fmt-btn" onclick="formatItalic()" type="button" title="مائل (Ctrl+I)"><i>I</i></button>
 
534
  <button id="fmt-strikethrough" class="fmt-btn" onclick="formatStrikethrough()" type="button" title="يتوسطه خط"><s>S</s></button>
535
  </div>
536
  <div class="fmt-divider"></div>
537
+ <!-- Alignment group -->
538
  <div class="fmt-group">
539
+ <button id="fmt-align-right" class="fmt-btn fmt-active" onclick="formatAlignRight()" type="button" title="محاذاة لليمين">
540
  <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M3 5h18v2H3V5zm4 4h14v2H7V9zm-4 4h18v2H3v-2zm4 4h14v2H7v-2z"/></svg>
541
  </button>
542
  <button id="fmt-align-center" class="fmt-btn" onclick="formatAlignCenter()" type="button" title="توسيط">
 
547
  </button>
548
  </div>
549
  <div class="fmt-divider"></div>
550
+ <!-- Undo/Redo group -->
551
  <div class="fmt-group">
552
  <button class="fmt-btn" onclick="formatUndo()" type="button" title="تراجع (Ctrl+Z)">
553
  <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a5 5 0 015 5v2M3 10l4-4M3 10l4 4"/></svg>
 
556
  <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10H11a5 5 0 00-5 5v2M21 10l-4-4M21 10l-4 4"/></svg>
557
  </button>
558
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  </div>
560
  <input type="file" id="doc-import-input" class="sr-only" accept=".txt,.docx,text/plain,application/vnd.openxmlformats-officedocument.wordprocessingml.document" aria-hidden="true">
561
  <div id="write-area">
src/js/format.js CHANGED
@@ -17,11 +17,11 @@ function formatItalic() { execFormat('italic'); }
17
  function formatUnderline() { execFormat('underline'); }
18
  function formatStrikethrough() { execFormat('strikethrough'); }
19
 
20
- /* ── Undo / Redo ── */
21
  function formatUndo() { execFormat('undo'); }
22
  function formatRedo() { execFormat('redo'); }
23
 
24
- /* ── Alignment ── */
25
  function formatAlignRight() { execFormat('justifyRight'); }
26
  function formatAlignCenter() { execFormat('justifyCenter'); }
27
  function formatAlignLeft() { execFormat('justifyLeft'); }
@@ -29,36 +29,64 @@ function formatAlignLeft() { execFormat('justifyLeft'); }
29
  /* ── Font family ── */
30
  function formatFont(fontName) {
31
  execFormat('fontName', fontName);
 
 
 
 
32
  }
33
 
34
  /* ── Font size ── */
35
- // execCommand fontSize only supports 1-7, so we use CSS instead
36
  function formatFontSize(size) {
37
  const sel = window.getSelection();
38
  if (!sel.rangeCount) return;
39
 
40
  const range = sel.getRangeAt(0);
41
- if (range.collapsed) return; // no selection
42
-
43
- // Wrap selected text in a span with the font size
44
- const span = document.createElement('span');
45
- span.style.fontSize = size;
46
- try {
47
- range.surroundContents(span);
48
- } catch (e) {
49
- // If selection crosses elements, use execCommand as fallback
50
- execFormat('fontSize', '4');
51
- // Then find the font element and fix the size
52
- const editor = getEditorElement();
53
- if (editor) {
54
- editor.querySelectorAll('font[size="4"]').forEach(f => {
55
- const s = document.createElement('span');
56
- s.style.fontSize = size;
57
- s.innerHTML = f.innerHTML;
58
- f.replaceWith(s);
59
- });
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  updateFormatState();
63
  }
64
 
@@ -71,33 +99,45 @@ function updateFormatState() {
71
  'fmt-italic': 'italic',
72
  'fmt-underline': 'underline',
73
  'fmt-strikethrough': 'strikeThrough',
74
- 'fmt-align-right': 'justifyRight',
75
- 'fmt-align-center': 'justifyCenter',
76
- 'fmt-align-left': 'justifyLeft',
77
  };
78
 
79
  Object.entries(btnMap).forEach(([id, command]) => {
80
  const btn = document.getElementById(id);
81
  if (btn) {
82
- const isActive = document.queryCommandState(command);
83
- btn.classList.toggle('fmt-active', isActive);
84
  }
85
  });
86
 
87
- // Font family
88
- const fontSelect = document.getElementById('fmt-font-family');
89
- if (fontSelect) {
90
- const currentFont = document.queryCommandValue('fontName').replace(/['"]/g, '');
91
- if (currentFont) {
92
- // Try to match
93
- for (let i = 0; i < fontSelect.options.length; i++) {
94
- if (fontSelect.options[i].value === currentFont) {
95
- fontSelect.selectedIndex = i;
96
- break;
97
- }
98
- }
99
  }
100
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
 
103
  /**
@@ -114,19 +154,47 @@ function initFormatToolbar() {
114
  }
115
  });
116
 
117
- // Font family change
118
- const fontSelect = document.getElementById('fmt-font-family');
119
- if (fontSelect) {
120
- fontSelect.addEventListener('change', () => {
121
- formatFont(fontSelect.value);
 
122
  });
123
  }
124
 
125
- // Font size change
126
- const sizeSelect = document.getElementById('fmt-font-size');
127
- if (sizeSelect) {
128
- sizeSelect.addEventListener('change', () => {
129
- formatFontSize(sizeSelect.value);
 
 
 
 
 
 
 
 
130
  });
131
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  }
 
17
  function formatUnderline() { execFormat('underline'); }
18
  function formatStrikethrough() { execFormat('strikethrough'); }
19
 
20
+ /* ── Undo / Redo (handles both typing and formatting) ── */
21
  function formatUndo() { execFormat('undo'); }
22
  function formatRedo() { execFormat('redo'); }
23
 
24
+ /* ── Alignment (applies to paragraph containing selection) ── */
25
  function formatAlignRight() { execFormat('justifyRight'); }
26
  function formatAlignCenter() { execFormat('justifyCenter'); }
27
  function formatAlignLeft() { execFormat('justifyLeft'); }
 
29
  /* ── Font family ── */
30
  function formatFont(fontName) {
31
  execFormat('fontName', fontName);
32
+ // Update the dropdown label
33
+ const label = document.getElementById('fmt-font-label');
34
+ if (label) label.textContent = fontName;
35
+ closeAllFmtDropdowns();
36
  }
37
 
38
  /* ── Font size ── */
 
39
  function formatFontSize(size) {
40
  const sel = window.getSelection();
41
  if (!sel.rangeCount) return;
42
 
43
  const range = sel.getRangeAt(0);
44
+ if (range.collapsed) {
45
+ // No selection — size will apply to next typed text
46
+ // Use a zero-width space trick
47
+ const span = document.createElement('span');
48
+ span.style.fontSize = size;
49
+ span.textContent = '\u200B';
50
+ range.insertNode(span);
51
+ // Place cursor after the span
52
+ const newRange = document.createRange();
53
+ newRange.setStartAfter(span);
54
+ newRange.collapse(true);
55
+ sel.removeAllRanges();
56
+ sel.addRange(newRange);
57
+ } else {
58
+ // Wrap selected text
59
+ const span = document.createElement('span');
60
+ span.style.fontSize = size;
61
+ try {
62
+ range.surroundContents(span);
63
+ } catch (e) {
64
+ // Fallback: use execCommand
65
+ execFormat('fontSize', '4');
66
+ const editor = getEditorElement();
67
+ if (editor) {
68
+ editor.querySelectorAll('font[size="4"]').forEach(f => {
69
+ const s = document.createElement('span');
70
+ s.style.fontSize = size;
71
+ s.innerHTML = f.innerHTML;
72
+ f.replaceWith(s);
73
+ });
74
+ }
75
  }
76
  }
77
+
78
+ // Update label
79
+ const label = document.getElementById('fmt-size-label');
80
+ if (label) label.textContent = parseInt(size);
81
+
82
+ // Update active item
83
+ document.querySelectorAll('#fmt-size-menu .fmt-dropdown__item').forEach(item => {
84
+ item.classList.toggle('fmt-dropdown__item--active', item.dataset.size === size);
85
+ });
86
+
87
+ closeAllFmtDropdowns();
88
+ const editor = getEditorElement();
89
+ if (editor) editor.focus();
90
  updateFormatState();
91
  }
92
 
 
99
  'fmt-italic': 'italic',
100
  'fmt-underline': 'underline',
101
  'fmt-strikethrough': 'strikeThrough',
 
 
 
102
  };
103
 
104
  Object.entries(btnMap).forEach(([id, command]) => {
105
  const btn = document.getElementById(id);
106
  if (btn) {
107
+ btn.classList.toggle('fmt-active', document.queryCommandState(command));
 
108
  }
109
  });
110
 
111
+ // Alignment — mutually exclusive
112
+ const alignMap = {
113
+ 'fmt-align-right': 'justifyRight',
114
+ 'fmt-align-center': 'justifyCenter',
115
+ 'fmt-align-left': 'justifyLeft',
116
+ };
117
+ Object.entries(alignMap).forEach(([id, command]) => {
118
+ const btn = document.getElementById(id);
119
+ if (btn) {
120
+ btn.classList.toggle('fmt-active', document.queryCommandState(command));
 
 
121
  }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Close all formatting dropdowns
127
+ */
128
+ function closeAllFmtDropdowns() {
129
+ document.querySelectorAll('.fmt-dropdown').forEach(d => d.classList.remove('open'));
130
+ }
131
+
132
+ /**
133
+ * Toggle a specific dropdown
134
+ */
135
+ function toggleFmtDropdown(wrapperId) {
136
+ const wrap = document.getElementById(wrapperId);
137
+ if (!wrap) return;
138
+ const isOpen = wrap.classList.contains('open');
139
+ closeAllFmtDropdowns();
140
+ if (!isOpen) wrap.classList.add('open');
141
  }
142
 
143
  /**
 
154
  }
155
  });
156
 
157
+ // Font dropdown trigger
158
+ const fontTrigger = document.getElementById('fmt-font-trigger');
159
+ if (fontTrigger) {
160
+ fontTrigger.addEventListener('click', (e) => {
161
+ e.stopPropagation();
162
+ toggleFmtDropdown('fmt-font-wrap');
163
  });
164
  }
165
 
166
+ // Font items
167
+ document.querySelectorAll('#fmt-font-menu .fmt-dropdown__item').forEach(item => {
168
+ item.addEventListener('click', () => {
169
+ formatFont(item.dataset.font);
170
+ });
171
+ });
172
+
173
+ // Size dropdown trigger
174
+ const sizeTrigger = document.getElementById('fmt-size-trigger');
175
+ if (sizeTrigger) {
176
+ sizeTrigger.addEventListener('click', (e) => {
177
+ e.stopPropagation();
178
+ toggleFmtDropdown('fmt-size-wrap');
179
  });
180
  }
181
+
182
+ // Size items
183
+ document.querySelectorAll('#fmt-size-menu .fmt-dropdown__item').forEach(item => {
184
+ item.addEventListener('click', () => {
185
+ formatFontSize(item.dataset.size);
186
+ });
187
+ });
188
+
189
+ // Close dropdowns when clicking outside
190
+ document.addEventListener('click', (e) => {
191
+ if (!e.target.closest('.fmt-dropdown')) {
192
+ closeAllFmtDropdowns();
193
+ }
194
+ });
195
+
196
+ // Close dropdowns on Escape
197
+ document.addEventListener('keydown', (e) => {
198
+ if (e.key === 'Escape') closeAllFmtDropdowns();
199
+ });
200
  }