@karbonjs/ui-svelte 0.2.3 → 0.2.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.
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { onMount, tick } from 'svelte'
3
- import type { MediaProvider } from '@karbonjs/ui-core'
3
+ import type { MediaProvider, MediaFile, EditorTheme } from '@karbonjs/ui-core'
4
4
 
5
5
  interface Props {
6
6
  value: string
7
7
  placeholder?: string
8
8
  media?: MediaProvider
9
+ theme?: EditorTheme
9
10
  class?: string
10
11
  }
11
12
 
@@ -13,6 +14,7 @@
13
14
  value = $bindable(''),
14
15
  placeholder = 'Rédigez votre contenu...',
15
16
  media,
17
+ theme = 'default',
16
18
  class: className = ''
17
19
  }: Props = $props()
18
20
 
@@ -89,13 +91,28 @@
89
91
  let elPropsTarget = $state<HTMLElement | null>(null)
90
92
 
91
93
  // ── Media explorer state ──
92
- let mediaFiles = $state<any[]>([])
94
+ let mediaCurrentPath = $state('')
95
+ let mediaEntries = $state<MediaFile[]>([])
93
96
  let mediaLoading = $state(false)
94
- let mediaSearch = $state('')
95
- let mediaPage = $state(1)
96
- let mediaTotal = $state(0)
97
- let mediaSelected = $state<any>(null)
98
97
  let mediaUploading = $state(false)
98
+ let mediaSearch = $state('')
99
+ let mediaSelected = $state<MediaFile | null>(null)
100
+ let mediaViewMode = $state<'grid' | 'list'>('grid')
101
+ let mediaShowNewFolder = $state(false)
102
+ let mediaNewFolderName = $state('')
103
+ let mediaDragOver = $state(false)
104
+
105
+ const mediaBreadcrumbs = $derived.by(() => {
106
+ const parts = mediaCurrentPath.split('/').filter(Boolean)
107
+ return parts.map((part, i) => ({
108
+ label: part,
109
+ path: parts.slice(0, i + 1).join('/')
110
+ }))
111
+ })
112
+
113
+ const mediaFiltered = $derived(
114
+ mediaSearch ? mediaEntries.filter(e => e.name.toLowerCase().includes(mediaSearch.toLowerCase())) : mediaEntries
115
+ )
99
116
 
100
117
  const COLORS = [
101
118
  '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#d9d9d9', '#ffffff',
@@ -107,10 +124,10 @@
107
124
  const FONT_SIZES = ['10px', '12px', '14px', '16px', '18px', '20px', '24px', '28px', '32px', '36px', '48px']
108
125
 
109
126
  const HTML_RULES: [RegExp, string][] = [
110
- [/(&lt;!--[\s\S]*?--&gt;)/g, '<span class="text-gray-600 italic">$1</span>'],
111
- [/(&lt;\/?)([\w-]+)/g, '<span class="text-gray-500">$1</span><span class="text-green-400">$2</span>'],
112
- [/([\w-]+)(=)(&quot;[^&]*&quot;|&#39;[^&]*&#39;)/g, '<span class="text-blue-400">$1</span><span class="text-gray-500">$2</span><span class="text-sky-300">$3</span>'],
113
- [/(&gt;)/g, '<span class="text-gray-500">$1</span>'],
127
+ [/(&lt;!--[\s\S]*?--&gt;)/g, '<span class="hl-comment">$1</span>'],
128
+ [/(&lt;\/?)([\w-]+)/g, '<span class="hl-bracket">$1</span><span class="hl-tag">$2</span>'],
129
+ [/([\w-]+)(=)(&quot;[^&]*&quot;|&#39;[^&]*&#39;)/g, '<span class="hl-attr">$1</span><span class="hl-eq">$2</span><span class="hl-val">$3</span>'],
130
+ [/(&gt;)/g, '<span class="hl-bracket">$1</span>'],
114
131
  ]
115
132
 
116
133
  /** Escape string for safe HTML attribute insertion */
@@ -118,6 +135,17 @@
118
135
  return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
119
136
  }
120
137
 
138
+ function isImage(entry: MediaFile): boolean {
139
+ return entry.mime?.startsWith('image/') ?? false
140
+ }
141
+
142
+ function formatSize(bytes: number): string {
143
+ if (bytes === 0) return '—'
144
+ if (bytes < 1024) return `${bytes} o`
145
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} Ko`
146
+ return `${(bytes / 1048576).toFixed(1)} Mo`
147
+ }
148
+
121
149
  onMount(() => {
122
150
  if (value) editor.innerHTML = value
123
151
  updateCounts()
@@ -269,7 +297,7 @@
269
297
  if (!files?.length || !media?.upload) return
270
298
  imageUploading = true
271
299
  try {
272
- const result = await media.upload(files[0])
300
+ const result = await media.upload(files[0], mediaCurrentPath)
273
301
  imageUrl = result.url
274
302
  imageAlt = files[0].name.replace(/\.[^/.]+$/, '')
275
303
  } catch { /* */ } finally { imageUploading = false }
@@ -283,16 +311,31 @@
283
311
  value = editor.innerHTML
284
312
  }
285
313
 
286
- function openMediaExplorerForImage() { mediaExplorerContext = 'imageModal'; showImageModal = false; showMediaExplorer = true; browseMedia() }
287
- function openMediaExplorerInline() { saveSelection(); mediaExplorerContext = 'editor'; showMediaExplorer = true; browseMedia() }
314
+ function openMediaExplorerForImage() { mediaExplorerContext = 'imageModal'; showImageModal = false; showMediaExplorer = true; browseMedia(mediaCurrentPath) }
315
+ function openMediaExplorerInline() { saveSelection(); mediaExplorerContext = 'editor'; showMediaExplorer = true; browseMedia(mediaCurrentPath) }
288
316
 
289
- async function browseMedia() {
317
+ // ── Media explorer ──
318
+ async function browseMedia(path: string) {
290
319
  if (!media?.browse) return
291
320
  mediaLoading = true
321
+ mediaSelected = null
292
322
  try {
293
- const result = await media.browse(mediaPage, mediaSearch)
294
- mediaFiles = result.files; mediaTotal = result.total
295
- } catch { mediaFiles = [] } finally { mediaLoading = false }
323
+ const result = await media.browse(path, mediaSearch || undefined)
324
+ mediaCurrentPath = result.path ?? path
325
+ mediaEntries = result.entries ?? []
326
+ } catch { mediaEntries = [] } finally { mediaLoading = false }
327
+ }
328
+
329
+ function mediaNavigateTo(path: string) { mediaCurrentPath = path; browseMedia(path) }
330
+ function mediaGoUp() { const parts = mediaCurrentPath.split('/').filter(Boolean); parts.pop(); mediaNavigateTo(parts.join('/')) }
331
+
332
+ function handleMediaEntryClick(entry: MediaFile) {
333
+ if (entry.is_dir) mediaNavigateTo(entry.path)
334
+ else mediaSelected = mediaSelected?.path === entry.path ? null : entry
335
+ }
336
+
337
+ function handleMediaEntryDblClick(entry: MediaFile) {
338
+ if (!entry.is_dir && entry.url) handleMediaSelect(entry.url)
296
339
  }
297
340
 
298
341
  function handleMediaSelect(url: string) {
@@ -307,6 +350,27 @@
307
350
  }
308
351
  }
309
352
 
353
+ async function mediaHandleUpload(files: FileList | null) {
354
+ if (!files?.length || !media?.upload) return
355
+ mediaUploading = true
356
+ try {
357
+ for (const file of files) await media.upload(file, mediaCurrentPath)
358
+ await browseMedia(mediaCurrentPath)
359
+ } catch { } finally { mediaUploading = false; mediaDragOver = false }
360
+ }
361
+
362
+ function mediaHandleDrop(e: DragEvent) { e.preventDefault(); mediaDragOver = false; mediaHandleUpload(e.dataTransfer?.files ?? null) }
363
+
364
+ async function mediaCreateFolder() {
365
+ if (!mediaNewFolderName.trim() || !media?.createFolder) return
366
+ try {
367
+ const path = mediaCurrentPath ? `${mediaCurrentPath}/${mediaNewFolderName}` : mediaNewFolderName
368
+ await media.createFolder(path)
369
+ mediaShowNewFolder = false; mediaNewFolderName = ''
370
+ await browseMedia(mediaCurrentPath)
371
+ } catch { }
372
+ }
373
+
310
374
  // ── Table ──
311
375
  function openTableModal() { saveSelection(); tableRows = 3; tableCols = 3; showTableModal = true }
312
376
 
@@ -353,10 +417,10 @@
353
417
  if (!findText) return
354
418
  const sel = window.getSelection(); const range = document.createRange()
355
419
  const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT)
356
- let node: Node | null; let found = false
420
+ let node: Node | null
357
421
  while ((node = walker.nextNode())) {
358
422
  const idx = (node.textContent ?? '').toLowerCase().indexOf(findText.toLowerCase())
359
- if (idx >= 0) { range.setStart(node, idx); range.setEnd(node, idx + findText.length); sel?.removeAllRanges(); sel?.addRange(range); found = true; break }
423
+ if (idx >= 0) { range.setStart(node, idx); range.setEnd(node, idx + findText.length); sel?.removeAllRanges(); sel?.addRange(range); break }
360
424
  }
361
425
  }
362
426
 
@@ -420,124 +484,137 @@
420
484
  function handleSourceInput() { value = sourceCode; updateLineNumbers() }
421
485
  function handleSourceScroll() { if (lineNumbers && sourceEl) lineNumbers.scrollTop = sourceEl.scrollTop }
422
486
  function insertColor(color: string) { exec('foreColor', color); showColorPicker = false }
423
- function isActive(cmd: string): string { return activeFormats.has(cmd) ? 'bg-violet-500/15 text-violet-400' : 'text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]' }
424
-
425
- const btnClass = 'flex items-center justify-center min-w-[32px] h-8 rounded-md transition-all cursor-pointer border-none bg-transparent px-1'
426
- const sepClass = 'w-px h-[22px] bg-[var(--karbon-border)] mx-0.5 shrink-0'
427
- const modalOverlay = 'fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm'
428
- const modalBox = 'w-full rounded-xl border border-[var(--karbon-border)] bg-[var(--karbon-bg-card)] p-6 shadow-2xl'
429
- const modalInput = 'block w-full rounded-lg border border-[var(--karbon-border-input)] bg-[var(--karbon-bg-input)] text-[var(--karbon-text)] px-3 py-2 text-sm outline-none focus:border-[var(--karbon-border-input-focus)] transition-colors'
430
- const modalLabel = 'block text-xs font-medium text-[var(--karbon-text-4)]'
431
- const modalBtnCancel = 'rounded-lg border border-[var(--karbon-border)] px-3 py-1.5 text-xs text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)]'
432
- const modalBtnPrimary = 'rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-violet-700 disabled:opacity-40'
487
+ function isActive(cmd: string): string { return activeFormats.has(cmd) ? 'rte-btn rte-btn-active' : 'rte-btn' }
433
488
  </script>
434
- <div role="toolbar" aria-label="Éditeur de texte riche" class="rounded-xl border border-[var(--karbon-border)] bg-[var(--karbon-bg-card)] shadow-sm overflow-hidden {fullscreen ? 'fixed inset-0 z-40 rounded-none flex flex-col' : ''} {className}">
489
+
490
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
491
+ <div role="toolbar" aria-label="Éditeur de texte riche" class="rte-wrapper {fullscreen ? 'fixed inset-0 z-40 rounded-none flex flex-col' : ''} {className}">
435
492
 
436
493
  <!-- ═══ TOOLBAR ═══ -->
437
- <div class="flex flex-wrap items-center gap-1 border-b border-[var(--karbon-border)] bg-[var(--karbon-bg-2)]/50 px-3 py-2 select-none">
438
- <button type="button" onclick={() => exec('undo')} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Annuler (Ctrl+Z)" aria-label="Annuler">
494
+ <div class="rte-toolbar">
495
+ <button type="button" onclick={() => exec('undo')} class="rte-btn" title="Annuler (Ctrl+Z)" aria-label="Annuler">
439
496
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
440
497
  </button>
441
- <button type="button" onclick={() => exec('redo')} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Rétablir" aria-label="Rétablir">
498
+ <button type="button" onclick={() => exec('redo')} class="rte-btn" title="Rétablir" aria-label="Rétablir">
442
499
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/></svg>
443
500
  </button>
444
- <span class={sepClass}></span>
501
+ <span class="rte-sep"></span>
445
502
 
446
- <button type="button" onclick={() => exec('formatBlock', 'p')} class="{btnClass} {isActive('p')}" title="Paragraphe" aria-label="Paragraphe">
503
+ <button type="button" onclick={() => exec('formatBlock', 'p')} class={isActive('p')} title="Paragraphe" aria-label="Paragraphe">
447
504
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 4v16"/><path d="M17 4v16"/><path d="M19 4H9.5a4.5 4.5 0 0 0 0 9H13"/></svg>
448
505
  </button>
449
- <button type="button" onclick={() => exec('formatBlock', 'h2')} class="{btnClass} text-xs font-bold {isActive('h2')}" title="Titre 2">H2</button>
450
- <button type="button" onclick={() => exec('formatBlock', 'h3')} class="{btnClass} text-xs font-bold {isActive('h3')}" title="Titre 3">H3</button>
451
- <button type="button" onclick={() => exec('formatBlock', 'h4')} class="{btnClass} text-xs font-bold {isActive('h4')}" title="Titre 4">H4</button>
452
- <span class={sepClass}></span>
506
+ <button type="button" onclick={() => exec('formatBlock', 'h2')} class="{isActive('h2')} text-xs font-bold" title="Titre 2">H2</button>
507
+ <button type="button" onclick={() => exec('formatBlock', 'h3')} class="{isActive('h3')} text-xs font-bold" title="Titre 3">H3</button>
508
+ <button type="button" onclick={() => exec('formatBlock', 'h4')} class="{isActive('h4')} text-xs font-bold" title="Titre 4">H4</button>
509
+ <span class="rte-sep"></span>
453
510
 
454
- <select onchange={(e) => setFontSize(e.currentTarget.value)} class="bg-[var(--karbon-bg-input)] border border-[var(--karbon-border-input)] text-[var(--karbon-text-3)] rounded-[5px] px-1 py-0.5 text-[0.6875rem] outline-none cursor-pointer" title="Taille">
511
+ <select onchange={(e) => setFontSize(e.currentTarget.value)} class="rte-select" title="Taille">
455
512
  <option value="">Taille</option>
456
513
  {#each FONT_SIZES as size}<option value={size}>{size}</option>{/each}
457
514
  </select>
458
- <span class={sepClass}></span>
515
+ <span class="rte-sep"></span>
459
516
 
460
- <button type="button" onclick={() => exec('bold')} class="{btnClass} {isActive('bold')}" title="Gras (Ctrl+B)" aria-label="Gras">
517
+ <button type="button" onclick={() => exec('bold')} class={isActive('bold')} title="Gras (Ctrl+B)" aria-label="Gras">
461
518
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/></svg>
462
519
  </button>
463
- <button type="button" onclick={() => exec('italic')} class="{btnClass} {isActive('italic')}" title="Italique (Ctrl+I)" aria-label="Italique">
520
+ <button type="button" onclick={() => exec('italic')} class={isActive('italic')} title="Italique (Ctrl+I)" aria-label="Italique">
464
521
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" x2="10" y1="4" y2="4"/><line x1="14" x2="5" y1="20" y2="20"/><line x1="15" x2="9" y1="4" y2="20"/></svg>
465
522
  </button>
466
- <button type="button" onclick={() => exec('underline')} class="{btnClass} {isActive('underline')}" title="Souligné (Ctrl+U)" aria-label="Souligné">
523
+ <button type="button" onclick={() => exec('underline')} class={isActive('underline')} title="Souligné (Ctrl+U)" aria-label="Souligné">
467
524
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" x2="20" y1="20" y2="20"/></svg>
468
525
  </button>
469
- <button type="button" onclick={() => exec('strikeThrough')} class="{btnClass} {isActive('strikeThrough')}" title="Barré" aria-label="Barré">
526
+ <button type="button" onclick={() => exec('strikeThrough')} class={isActive('strikeThrough')} title="Barré" aria-label="Barré">
470
527
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4H9a3 3 0 0 0-2.83 4"/><path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" x2="20" y1="12" y2="12"/></svg>
471
528
  </button>
472
- <span class={sepClass}></span>
529
+ <button type="button" onclick={() => exec('subscript')} class={isActive('subscript')} title="Indice" aria-label="Indice">
530
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"/></svg>
531
+ </button>
532
+ <button type="button" onclick={() => exec('superscript')} class={isActive('superscript')} title="Exposant" aria-label="Exposant">
533
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 9h-4c0-1.5.44-2 1.5-2.5S20 5.33 20 4c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"/></svg>
534
+ </button>
535
+ <span class="rte-sep"></span>
473
536
 
537
+ <!-- Color -->
474
538
  <div class="relative">
475
- <button type="button" onclick={() => showColorPicker = !showColorPicker} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Couleur" aria-label="Couleur du texte">
539
+ <button type="button" onclick={() => showColorPicker = !showColorPicker} class="rte-btn" title="Couleur" aria-label="Couleur du texte">
476
540
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 11-8-8-8.6 8.6a2 2 0 0 0 0 2.8l5.2 5.2c.8.8 2 .8 2.8 0L19 11Z"/><path d="m5 2 5 5"/><path d="M2 13h15"/><path d="M22 20a2 2 0 1 1-4 0c0-1.6 1.7-2.4 2-4 .3 1.6 2 2.4 2 4Z"/></svg>
477
541
  </button>
478
542
  {#if showColorPicker}
479
- <div class="absolute left-0 top-full z-20 mt-1 grid grid-cols-8 gap-1 rounded-lg border border-[var(--karbon-border)] bg-[var(--karbon-bg-card)] p-2 shadow-xl">
480
- {#each COLORS as color}<button type="button" onclick={() => insertColor(color)} class="h-5 w-5 rounded border border-[var(--karbon-border)] hover:scale-125 transition-transform cursor-pointer" style="background-color: {color}" aria-label="Couleur {color}"></button>{/each}
543
+ <div class="rte-color-grid">
544
+ {#each COLORS as color}<button type="button" onclick={() => insertColor(color)} class="rte-color-swatch" style="background-color: {color}" aria-label="Couleur {color}"></button>{/each}
481
545
  </div>
482
546
  {/if}
483
547
  </div>
484
- <button type="button" onclick={() => exec('removeFormat')} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Effacer formatage" aria-label="Effacer le formatage">
548
+ <button type="button" onclick={() => exec('removeFormat')} class="rte-btn" title="Effacer formatage" aria-label="Effacer le formatage">
485
549
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 21a4.6 4.6 0 0 1 0-9h10a4.6 4.6 0 1 1 0 9H7Z"/><path d="m3 3 18 18"/></svg>
486
550
  </button>
487
- <span class={sepClass}></span>
551
+ <span class="rte-sep"></span>
488
552
 
489
- <button type="button" onclick={() => exec('justifyLeft')} class="{btnClass} {isActive('justifyLeft')}" title="Gauche" aria-label="Aligner à gauche">
553
+ <!-- Alignment -->
554
+ <button type="button" onclick={() => exec('justifyLeft')} class={isActive('justifyLeft')} title="Gauche" aria-label="Aligner à gauche">
490
555
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" x2="3" y1="6" y2="6"/><line x1="15" x2="3" y1="12" y2="12"/><line x1="17" x2="3" y1="18" y2="18"/></svg>
491
556
  </button>
492
- <button type="button" onclick={() => exec('justifyCenter')} class="{btnClass} {isActive('justifyCenter')}" title="Centre" aria-label="Centrer">
557
+ <button type="button" onclick={() => exec('justifyCenter')} class={isActive('justifyCenter')} title="Centre" aria-label="Centrer">
493
558
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" x2="3" y1="6" y2="6"/><line x1="17" x2="7" y1="12" y2="12"/><line x1="19" x2="5" y1="18" y2="18"/></svg>
494
559
  </button>
495
- <button type="button" onclick={() => exec('justifyRight')} class="{btnClass} {isActive('justifyRight')}" title="Droite" aria-label="Aligner à droite">
560
+ <button type="button" onclick={() => exec('justifyRight')} class={isActive('justifyRight')} title="Droite" aria-label="Aligner à droite">
496
561
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" x2="3" y1="6" y2="6"/><line x1="21" x2="9" y1="12" y2="12"/><line x1="21" x2="7" y1="18" y2="18"/></svg>
497
562
  </button>
498
- <span class={sepClass}></span>
563
+ <button type="button" onclick={() => exec('justifyFull')} class={isActive('justifyFull')} title="Justifier" aria-label="Justifier">
564
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" x2="3" y1="6" y2="6"/><line x1="21" x2="3" y1="12" y2="12"/><line x1="21" x2="3" y1="18" y2="18"/></svg>
565
+ </button>
566
+ <span class="rte-sep"></span>
499
567
 
500
- <button type="button" onclick={() => exec('insertUnorderedList')} class="{btnClass} {isActive('insertunorderedlist')}" title="Puces" aria-label="Liste à puces">
568
+ <!-- Lists & blocks -->
569
+ <button type="button" onclick={() => exec('insertUnorderedList')} class={isActive('insertunorderedlist')} title="Puces" aria-label="Liste à puces">
501
570
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
502
571
  </button>
503
- <button type="button" onclick={() => exec('insertOrderedList')} class="{btnClass} {isActive('insertorderedlist')}" title="Numéros" aria-label="Liste numérotée">
572
+ <button type="button" onclick={() => exec('insertOrderedList')} class={isActive('insertorderedlist')} title="Numéros" aria-label="Liste numérotée">
504
573
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>
505
574
  </button>
506
- <button type="button" onclick={() => exec('formatBlock', 'blockquote')} class="{btnClass} {isActive('blockquote')}" title="Citation" aria-label="Citation">
575
+ <button type="button" onclick={() => exec('indent')} class="rte-btn" title="Indenter" aria-label="Indenter">
576
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 8 7 12 3 16"/><line x1="21" x2="11" y1="12" y2="12"/><line x1="21" x2="11" y1="6" y2="6"/><line x1="21" x2="11" y1="18" y2="18"/></svg>
577
+ </button>
578
+ <button type="button" onclick={() => exec('outdent')} class="rte-btn" title="Désindenter" aria-label="Désindenter">
579
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 8 3 12 7 16"/><line x1="21" x2="11" y1="12" y2="12"/><line x1="21" x2="11" y1="6" y2="6"/><line x1="21" x2="11" y1="18" y2="18"/></svg>
580
+ </button>
581
+ <button type="button" onclick={() => exec('formatBlock', 'blockquote')} class={isActive('blockquote')} title="Citation" aria-label="Citation">
507
582
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z"/></svg>
508
583
  </button>
509
- <button type="button" onclick={() => exec('insertHTML', '<hr />')} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Séparateur" aria-label="Séparateur">
584
+ <button type="button" onclick={() => exec('formatBlock', 'pre')} class="rte-btn" title="Bloc de code" aria-label="Bloc de code"><code class="text-[11px]">&lt;/&gt;</code></button>
585
+ <button type="button" onclick={() => exec('insertHTML', '<hr />')} class="rte-btn" title="Séparateur" aria-label="Séparateur">
510
586
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>
511
587
  </button>
512
- <span class={sepClass}></span>
588
+ <span class="rte-sep"></span>
513
589
 
514
- <button type="button" onclick={openLinkModal} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Lien (Ctrl+K)" aria-label="Insérer un lien">
590
+ <!-- Insert -->
591
+ <button type="button" onclick={openLinkModal} class="rte-btn" title="Lien (Ctrl+K)" aria-label="Insérer un lien">
515
592
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
516
593
  </button>
517
- <button type="button" onclick={openImageModal} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Image" aria-label="Insérer une image">
594
+ <button type="button" onclick={openImageModal} class="rte-btn" title="Image" aria-label="Insérer une image">
518
595
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
519
596
  </button>
520
597
  {#if media}
521
- <button type="button" onclick={openMediaExplorerInline} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Médias" aria-label="Explorer les médias">
598
+ <button type="button" onclick={openMediaExplorerInline} class="rte-btn" title="Médias" aria-label="Explorer les médias">
522
599
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>
523
600
  </button>
524
601
  {/if}
525
- <button type="button" onclick={openTableModal} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Tableau" aria-label="Insérer un tableau">
602
+ <button type="button" onclick={openTableModal} class="rte-btn" title="Tableau" aria-label="Insérer un tableau">
526
603
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/></svg>
527
604
  </button>
528
- <button type="button" onclick={openEmbedModal} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Vidéo / Embed" aria-label="Insérer une vidéo">
605
+ <button type="button" onclick={openEmbedModal} class="rte-btn" title="Vidéo / Embed" aria-label="Insérer une vidéo">
529
606
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"/><path d="m10 15 5-3-5-3z"/></svg>
530
607
  </button>
531
608
 
532
609
  <div class="flex-1"></div>
533
610
 
534
- <button type="button" onclick={() => showFindReplace = !showFindReplace} class="{btnClass} {showFindReplace ? 'bg-violet-500/15 text-violet-400' : 'text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]'}" title="Rechercher (Ctrl+H)" aria-label="Rechercher et remplacer">
611
+ <button type="button" onclick={() => showFindReplace = !showFindReplace} class="{showFindReplace ? 'rte-btn rte-btn-active' : 'rte-btn'}" title="Rechercher (Ctrl+H)" aria-label="Rechercher et remplacer">
535
612
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
536
613
  </button>
537
- <button type="button" onclick={toggleSource} class="{btnClass} {sourceMode ? 'bg-violet-500/15 text-violet-400' : 'text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]'}" title="Source" aria-label="Mode source HTML">
614
+ <button type="button" onclick={toggleSource} class="{sourceMode ? 'rte-btn rte-btn-active' : 'rte-btn'}" title="Source" aria-label="Mode source HTML">
538
615
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/></svg>
539
616
  </button>
540
- <button type="button" onclick={() => fullscreen = !fullscreen} class="{btnClass} text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" title="Plein écran" aria-label="Plein écran">
617
+ <button type="button" onclick={() => fullscreen = !fullscreen} class="rte-btn" title="Plein écran" aria-label="Plein écran">
541
618
  {#if fullscreen}
542
619
  <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
543
620
  {:else}
@@ -548,13 +625,13 @@
548
625
 
549
626
  <!-- ═══ FIND/REPLACE BAR ═══ -->
550
627
  {#if showFindReplace}
551
- <div class="flex items-center gap-2 border-b border-[var(--karbon-border)] bg-[var(--karbon-bg-2)]/30 px-4 py-2">
552
- <input type="text" bind:value={findText} placeholder="Rechercher..." class="{modalInput} w-40 !py-1 !px-2 !text-xs" onkeydown={(e) => { if (e.key === 'Enter') findNext() }} />
553
- <input type="text" bind:value={replaceText} placeholder="Remplacer..." class="{modalInput} w-40 !py-1 !px-2 !text-xs" />
554
- <button type="button" onclick={findNext} class="bg-[var(--karbon-bg-2)] border border-[var(--karbon-border)] text-[var(--karbon-text-3)] rounded-md px-2.5 py-1 text-[0.6875rem] cursor-pointer hover:bg-[var(--karbon-bg-card)] hover:text-[var(--karbon-text)] transition-all">Suivant</button>
555
- <button type="button" onclick={replaceNext} class="bg-[var(--karbon-bg-2)] border border-[var(--karbon-border)] text-[var(--karbon-text-3)] rounded-md px-2.5 py-1 text-[0.6875rem] cursor-pointer hover:bg-[var(--karbon-bg-card)] hover:text-[var(--karbon-text)] transition-all">Remplacer</button>
556
- <button type="button" onclick={replaceAll} class="bg-[var(--karbon-bg-2)] border border-[var(--karbon-border)] text-[var(--karbon-text-3)] rounded-md px-2.5 py-1 text-[0.6875rem] cursor-pointer hover:bg-[var(--karbon-bg-card)] hover:text-[var(--karbon-text)] transition-all">Tout</button>
557
- <button type="button" onclick={() => showFindReplace = false} class="{btnClass} text-[var(--karbon-text-4)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)]" aria-label="Fermer">
628
+ <div class="rte-findbar">
629
+ <input type="text" bind:value={findText} placeholder="Rechercher..." class="rte-bar-input w-40" onkeydown={(e) => { if (e.key === 'Enter') findNext() }} />
630
+ <input type="text" bind:value={replaceText} placeholder="Remplacer..." class="rte-bar-input w-40" />
631
+ <button type="button" onclick={findNext} class="rte-bar-btn">Suivant</button>
632
+ <button type="button" onclick={replaceNext} class="rte-bar-btn">Remplacer</button>
633
+ <button type="button" onclick={replaceAll} class="rte-bar-btn">Tout</button>
634
+ <button type="button" onclick={() => showFindReplace = false} class="rte-btn" aria-label="Fermer">
558
635
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
559
636
  </button>
560
637
  </div>
@@ -562,33 +639,18 @@
562
639
 
563
640
  <!-- ═══ EDITOR / SOURCE ═══ -->
564
641
  {#if sourceMode}
565
- <div class="relative flex overflow-hidden bg-[#0d1117] {fullscreen ? 'flex-1' : 'h-[500px]'}">
566
- <div bind:this={lineNumbers} class="shrink-0 w-12 py-4 text-right font-mono text-[0.8125rem] leading-[1.625] text-[#484f58] select-none overflow-hidden border-r border-[#21262d] [&>div]:pr-3"></div>
567
- <div class="absolute top-0 left-12 right-0 bottom-0 p-4 font-mono text-[0.8125rem] leading-[1.625] whitespace-pre overflow-hidden pointer-events-none text-[#c9d1d9]">{@html highlightHtml(sourceCode)}</div>
568
- <textarea bind:this={sourceEl} bind:value={sourceCode} oninput={handleSourceInput} onscroll={handleSourceScroll} class="absolute top-0 left-12 right-0 bottom-0 p-4 m-0 border-none outline-none resize-none font-mono text-[0.8125rem] leading-[1.625] text-transparent caret-[#c9d1d9] bg-transparent whitespace-pre overflow-auto [-webkit-text-fill-color:transparent]" spellcheck="false" wrap="off"></textarea>
642
+ <div class="rte-source-wrapper {fullscreen ? 'flex-1' : ''}" style="{fullscreen ? '' : 'height: 500px;'}">
643
+ <div bind:this={lineNumbers} class="rte-line-numbers"></div>
644
+ <div class="rte-highlight-layer" aria-hidden="true">{@html highlightHtml(sourceCode)}</div>
645
+ <textarea bind:this={sourceEl} bind:value={sourceCode} oninput={handleSourceInput} onscroll={handleSourceScroll} class="rte-source-textarea" spellcheck="false" wrap="off"></textarea>
569
646
  </div>
570
647
  {:else}
648
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
571
649
  <div
572
650
  bind:this={editor} contenteditable="true"
573
- class="outline-none px-5 py-5 text-[var(--karbon-text)] text-[0.9375rem] leading-relaxed
574
- [&:empty:before]:content-[attr(data-placeholder)] [&:empty:before]:text-[var(--karbon-text-4)] [&:empty:before]:pointer-events-none
575
- [&_h2]:text-[1.5em] [&_h2]:font-bold [&_h2]:mt-4 [&_h2]:mb-2
576
- [&_h3]:text-[1.25em] [&_h3]:font-semibold [&_h3]:mt-3 [&_h3]:mb-1.5
577
- [&_h4]:text-[1.1em] [&_h4]:font-semibold [&_h4]:mt-2.5 [&_h4]:mb-1
578
- [&_p]:my-2 [&_a]:text-violet-500 [&_a]:underline
579
- [&_blockquote]:border-l-[3px] [&_blockquote]:border-violet-500 [&_blockquote]:pl-4 [&_blockquote]:py-2 [&_blockquote]:my-4 [&_blockquote]:bg-violet-500/5 [&_blockquote]:rounded-r-lg [&_blockquote]:italic
580
- [&_pre]:bg-[var(--karbon-bg-2)] [&_pre]:border [&_pre]:border-[var(--karbon-border)] [&_pre]:rounded-lg [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-[0.875em] [&_pre]:overflow-x-auto [&_pre]:my-4
581
- [&_code]:bg-[var(--karbon-bg-2)] [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-[0.875em]
582
- [&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-lg [&_img]:my-4 [&_img]:cursor-pointer [&_img:hover]:outline [&_img:hover]:outline-2 [&_img:hover]:outline-violet-500/50 [&_img:hover]:outline-offset-2
583
- [&_hr]:border-none [&_hr]:border-t [&_hr]:border-[var(--karbon-border)] [&_hr]:my-6
584
- [&_ul]:pl-6 [&_ul]:my-2 [&_ol]:pl-6 [&_ol]:my-2 [&_li]:my-1
585
- [&_table]:w-full [&_table]:border-collapse [&_table]:my-4
586
- [&_th]:border [&_th]:border-[var(--karbon-border)] [&_th]:px-3 [&_th]:py-2 [&_th]:bg-[var(--karbon-bg-2)] [&_th]:font-semibold [&_th]:text-[0.875em]
587
- [&_td]:border [&_td]:border-[var(--karbon-border)] [&_td]:px-3 [&_td]:py-2
588
- [&_.embed-responsive]:relative [&_.embed-responsive]:pb-[56.25%] [&_.embed-responsive]:h-0 [&_.embed-responsive]:overflow-hidden [&_.embed-responsive]:my-4 [&_.embed-responsive]:rounded-lg
589
- [&_.embed-responsive_iframe]:absolute [&_.embed-responsive_iframe]:inset-0 [&_.embed-responsive_iframe]:w-full [&_.embed-responsive_iframe]:h-full [&_.embed-responsive_iframe]:border-0
590
- {fullscreen ? 'flex-1 overflow-y-auto' : 'min-h-[500px] max-h-[800px] overflow-y-auto'}"
591
- data-placeholder={placeholder} role="textbox" tabindex="0" aria-multiline="true"
651
+ class="rte-content rte-{theme} outline-none {fullscreen ? 'flex-1 overflow-y-auto' : ''}"
652
+ style="{fullscreen ? '' : 'min-height: 500px; max-height: 800px; overflow-y: auto;'}"
653
+ data-placeholder={placeholder} role="textbox" aria-multiline="true"
592
654
  oninput={handleInput} onkeydown={handleKeydown} onkeyup={updateActiveFormats}
593
655
  onmouseup={updateActiveFormats} onpaste={handlePaste}
594
656
  oncontextmenu={handleContextMenu} ondblclick={handleDblClick}
@@ -596,7 +658,7 @@
596
658
  {/if}
597
659
 
598
660
  <!-- ═══ STATUS BAR ═══ -->
599
- <div class="flex items-center gap-4 border-t border-[var(--karbon-border)] bg-[var(--karbon-bg-2)]/30 px-4 py-1.5 text-[11px] text-[var(--karbon-text-4)]">
661
+ <div class="rte-statusbar">
600
662
  <span>{wordCount} mot{wordCount !== 1 ? 's' : ''}</span>
601
663
  <span>{charCount} caractère{charCount !== 1 ? 's' : ''}</span>
602
664
  {#if sourceMode}<span class="text-violet-400">Mode source</span>{/if}
@@ -607,71 +669,72 @@
607
669
 
608
670
  <!-- ═══ CONTEXT MENU ═══ -->
609
671
  {#if showContextMenu}
672
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
610
673
  <div class="fixed inset-0 z-[60]" role="presentation" onclick={() => showContextMenu = false} onkeydown={(e) => { if (e.key === "Escape") showContextMenu = false }}></div>
611
- <div class="fixed z-[61] w-56 rounded-xl border border-[var(--karbon-border)] bg-[var(--karbon-bg-card)] shadow-2xl shadow-black/30 backdrop-blur-xl overflow-hidden" style="left: {contextPos.x}px; top: {contextPos.y}px;">
612
- <div class="px-3 py-2 border-b border-[var(--karbon-border)] bg-[var(--karbon-bg-2)]/50">
613
- <span class="text-[11px] font-semibold uppercase tracking-wider text-[var(--karbon-text-4)]">
614
- {contextType === 'image' ? 'Image' : contextType === 'link' ? 'Lien' : contextType === 'table' ? 'Tableau' : 'Élément'}
615
- </span>
674
+ <div class="ctx-menu" style="left: {contextPos.x}px; top: {contextPos.y}px;">
675
+ <div class="ctx-header">
676
+ <span>{contextType === 'image' ? 'Image' : contextType === 'link' ? 'Lien' : contextType === 'table' ? 'Tableau' : 'Élément'}</span>
616
677
  </div>
617
678
  {#if contextType === 'image'}
618
679
  <div class="py-1">
619
- <button type="button" onclick={() => { if (targetImage) openImageProps(targetImage) }} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">Propriétés de l'image</button>
680
+ <button type="button" onclick={() => { if (targetImage) openImageProps(targetImage) }} class="ctx-item">Propriétés de l'image</button>
681
+ <button type="button" onclick={() => { showContextMenu = false; if (targetImage) { saveSelection(); linkUrl = ''; linkText = ''; showLinkModal = true } }} class="ctx-item">Ajouter un lien</button>
620
682
  </div>
621
- <div class="border-t border-[var(--karbon-border)] py-1">
622
- <button type="button" onclick={deleteTargetImage} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-red-400 bg-transparent border-none cursor-pointer transition-all hover:bg-red-500/10 hover:text-red-500">Supprimer l'image</button>
683
+ <div class="ctx-divider">
684
+ <button type="button" onclick={deleteTargetImage} class="ctx-item ctx-danger">Supprimer l'image</button>
623
685
  </div>
624
686
  {:else if contextType === 'link'}
625
687
  <div class="py-1">
626
- <button type="button" onclick={() => { const a = contextTarget?.closest('a'); if (a) openLinkModalFromElement(a) }} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">Modifier le lien</button>
688
+ <button type="button" onclick={() => { const a = contextTarget?.closest('a'); if (a) openLinkModalFromElement(a) }} class="ctx-item">Modifier le lien</button>
627
689
  </div>
628
- <div class="border-t border-[var(--karbon-border)] py-1">
629
- <button type="button" onclick={() => { showContextMenu = false; exec('unlink') }} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-red-400 bg-transparent border-none cursor-pointer transition-all hover:bg-red-500/10 hover:text-red-500">Supprimer le lien</button>
690
+ <div class="ctx-divider">
691
+ <button type="button" onclick={() => { showContextMenu = false; exec('unlink') }} class="ctx-item ctx-danger">Supprimer le lien</button>
630
692
  </div>
631
693
  {:else if contextType === 'table'}
632
694
  <div class="py-1">
633
- <div class="px-3 py-1"><span class="text-[10px] font-semibold uppercase tracking-wider text-[var(--karbon-text-4)]">Lignes</span></div>
634
- <button type="button" onclick={() => tableAction('addRowAbove')} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">↑ Insérer au-dessus</button>
635
- <button type="button" onclick={() => tableAction('addRowBelow')} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">↓ Insérer en-dessous</button>
695
+ <div class="ctx-section-title">Lignes</div>
696
+ <button type="button" onclick={() => tableAction('addRowAbove')} class="ctx-item">↑ Insérer au-dessus</button>
697
+ <button type="button" onclick={() => tableAction('addRowBelow')} class="ctx-item">↓ Insérer en-dessous</button>
636
698
  </div>
637
- <div class="border-t border-[var(--karbon-border)] py-1">
638
- <div class="px-3 py-1"><span class="text-[10px] font-semibold uppercase tracking-wider text-[var(--karbon-text-4)]">Colonnes</span></div>
639
- <button type="button" onclick={() => tableAction('addColLeft')} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">← Insérer à gauche</button>
640
- <button type="button" onclick={() => tableAction('addColRight')} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">→ Insérer à droite</button>
699
+ <div class="ctx-divider">
700
+ <div class="ctx-section-title">Colonnes</div>
701
+ <button type="button" onclick={() => tableAction('addColLeft')} class="ctx-item">← Insérer à gauche</button>
702
+ <button type="button" onclick={() => tableAction('addColRight')} class="ctx-item">→ Insérer à droite</button>
641
703
  </div>
642
- <div class="border-t border-[var(--karbon-border)] py-1">
643
- <button type="button" onclick={() => tableAction('deleteRow')} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-amber-400 bg-transparent border-none cursor-pointer transition-all hover:bg-amber-500/10 hover:text-amber-500">Supprimer la ligne</button>
644
- <button type="button" onclick={() => tableAction('deleteCol')} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-amber-400 bg-transparent border-none cursor-pointer transition-all hover:bg-amber-500/10 hover:text-amber-500">Supprimer la colonne</button>
645
- <button type="button" onclick={() => tableAction('deleteTable')} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-red-400 bg-transparent border-none cursor-pointer transition-all hover:bg-red-500/10 hover:text-red-500">Supprimer le tableau</button>
704
+ <div class="ctx-divider">
705
+ <button type="button" onclick={() => tableAction('deleteRow')} class="ctx-item ctx-warn">Supprimer la ligne</button>
706
+ <button type="button" onclick={() => tableAction('deleteCol')} class="ctx-item ctx-warn">Supprimer la colonne</button>
707
+ <button type="button" onclick={() => tableAction('deleteTable')} class="ctx-item ctx-danger">Supprimer le tableau</button>
646
708
  </div>
647
709
  {/if}
648
- <div class="border-t border-[var(--karbon-border)] py-1">
649
- <button type="button" onclick={() => openElementProps()} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">Propriétés HTML</button>
650
- <button type="button" onclick={() => { showContextMenu = false; exec('removeFormat') }} class="flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs text-[var(--karbon-text-2)] bg-transparent border-none cursor-pointer transition-all hover:bg-violet-500/8 hover:text-[var(--karbon-text)]">Effacer le formatage</button>
710
+ <div class="ctx-divider">
711
+ <button type="button" onclick={() => openElementProps()} class="ctx-item">Propriétés HTML</button>
712
+ <button type="button" onclick={() => { showContextMenu = false; exec('removeFormat') }} class="ctx-item">Effacer le formatage</button>
651
713
  </div>
652
714
  </div>
653
715
  {/if}
654
716
 
655
717
  <!-- ═══ LINK MODAL ═══ -->
656
718
  {#if showLinkModal}
657
- <div class={modalOverlay} role="presentation" onclick={() => showLinkModal = false} onkeydown={(e) => { if (e.key === "Escape") showLinkModal = false }}>
658
- <div class="{modalBox} max-w-md" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
659
- <div class="flex items-center justify-between mb-4">
660
- <h3 class="text-sm font-semibold text-[var(--karbon-text)]">Insérer / Modifier un lien</h3>
661
- <button type="button" onclick={() => showLinkModal = false} class="text-[var(--karbon-text-4)] hover:text-[var(--karbon-text)] cursor-pointer" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
719
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
720
+ <div class="rte-overlay" role="presentation" onclick={() => showLinkModal = false} onkeydown={(e) => { if (e.key === "Escape") showLinkModal = false }}>
721
+ <div class="rte-modal max-w-md" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
722
+ <div class="rte-modal-header">
723
+ <h3>Insérer / Modifier un lien</h3>
724
+ <button type="button" onclick={() => showLinkModal = false} class="rte-modal-close" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
662
725
  </div>
663
726
  <div class="space-y-3">
664
- <div class="space-y-1"><span class={modalLabel}>URL</span><input type="url" bind:value={linkUrl} placeholder="https://..." class={modalInput} /></div>
665
- <div class="space-y-1"><span class={modalLabel}>Texte</span><input type="text" bind:value={linkText} placeholder="Texte affiché" class={modalInput} /></div>
666
- <div class="space-y-1"><span class={modalLabel}>Title</span><input type="text" bind:value={linkTitle} placeholder="Info-bulle au survol" class={modalInput} /></div>
667
- <div class="space-y-1"><span class={modalLabel}>Classes CSS</span><input type="text" bind:value={linkClass} placeholder="ex: btn btn-primary" class="{modalInput} font-mono text-xs" /></div>
668
- <label class="flex items-center gap-2 cursor-pointer"><input type="checkbox" bind:checked={linkTarget} class="h-4 w-4 rounded" /><span class="text-sm text-[var(--karbon-text-2)]">Ouvrir dans un nouvel onglet</span></label>
727
+ <div class="space-y-1"><span class="rte-label">URL</span><input type="url" bind:value={linkUrl} placeholder="https://..." class="rte-input" /></div>
728
+ <div class="space-y-1"><span class="rte-label">Texte</span><input type="text" bind:value={linkText} placeholder="Texte affiché" class="rte-input" /></div>
729
+ <div class="space-y-1"><span class="rte-label">Title</span><input type="text" bind:value={linkTitle} placeholder="Info-bulle au survol" class="rte-input" /></div>
730
+ <div class="space-y-1"><span class="rte-label">Classes CSS</span><input type="text" bind:value={linkClass} placeholder="ex: btn btn-primary" class="rte-input font-mono text-xs" /></div>
731
+ <label class="flex items-center gap-2 cursor-pointer"><input type="checkbox" bind:checked={linkTarget} class="h-4 w-4 rounded" /><span class="rte-label-inline">Ouvrir dans un nouvel onglet</span></label>
669
732
  </div>
670
- <div class="mt-5 flex items-center justify-between">
671
- <button type="button" onclick={() => { showLinkModal = false; restoreSelection(); exec('unlink') }} class="text-xs text-red-400 hover:text-red-300 cursor-pointer">Supprimer</button>
733
+ <div class="rte-modal-footer justify-between">
734
+ <button type="button" onclick={() => { showLinkModal = false; restoreSelection(); exec('unlink') }} class="rte-btn-danger-text">Supprimer</button>
672
735
  <div class="flex gap-2">
673
- <button type="button" onclick={() => showLinkModal = false} class="{modalBtnCancel} cursor-pointer">Annuler</button>
674
- <button type="button" onclick={insertLink} class="{modalBtnPrimary} cursor-pointer">Appliquer</button>
736
+ <button type="button" onclick={() => showLinkModal = false} class="rte-btn-cancel">Annuler</button>
737
+ <button type="button" onclick={insertLink} class="rte-btn-primary">Appliquer</button>
675
738
  </div>
676
739
  </div>
677
740
  </div>
@@ -680,46 +743,47 @@
680
743
 
681
744
  <!-- ═══ IMAGE INSERT MODAL ═══ -->
682
745
  {#if showImageModal}
683
- <div class={modalOverlay} role="presentation" onclick={() => showImageModal = false} onkeydown={(e) => { if (e.key === "Escape") showImageModal = false }}>
684
- <div class="{modalBox} max-w-lg" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
685
- <div class="flex items-center justify-between mb-4">
686
- <h3 class="text-sm font-semibold text-[var(--karbon-text)]">Insérer une image</h3>
687
- <button type="button" onclick={() => showImageModal = false} class="text-[var(--karbon-text-4)] hover:text-[var(--karbon-text)] cursor-pointer" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
746
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
747
+ <div class="rte-overlay" role="presentation" onclick={() => showImageModal = false} onkeydown={(e) => { if (e.key === "Escape") showImageModal = false }}>
748
+ <div class="rte-modal max-w-lg" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
749
+ <div class="rte-modal-header">
750
+ <h3>Insérer une image</h3>
751
+ <button type="button" onclick={() => showImageModal = false} class="rte-modal-close" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
688
752
  </div>
689
753
  <div class="space-y-4">
690
754
  <div class="grid grid-cols-2 gap-3">
691
755
  {#if media?.upload}
692
- <div class="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-[var(--karbon-border)] p-4 hover:border-[var(--karbon-border-input)] transition-colors">
756
+ <div class="rte-upload-zone">
693
757
  {#if imageUploading}
694
758
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin text-violet-400 mb-1"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
695
- <p class="text-[11px] text-[var(--karbon-text-3)]">Upload...</p>
759
+ <p class="text-[11px] opacity-60">Upload...</p>
696
760
  {:else}
697
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--karbon-text-4)] mb-1"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
761
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-40 mb-1"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
698
762
  <label class="cursor-pointer text-xs font-medium text-violet-400 hover:text-violet-300">Uploader<input type="file" accept="image/*" class="hidden" onchange={(e) => handleImageUpload(e.currentTarget.files)} /></label>
699
763
  {/if}
700
764
  </div>
701
765
  {/if}
702
766
  {#if media}
703
- <button type="button" onclick={openMediaExplorerForImage} class="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-[var(--karbon-border)] p-4 hover:border-violet-500/40 hover:bg-violet-500/5 transition-colors cursor-pointer">
704
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--karbon-text-4)] mb-1"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>
767
+ <button type="button" onclick={openMediaExplorerForImage} class="rte-upload-zone hover:border-violet-500/40 hover:bg-violet-500/5">
768
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-40 mb-1"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>
705
769
  <span class="text-xs font-medium text-violet-400">Parcourir les médias</span>
706
770
  </button>
707
771
  {/if}
708
772
  </div>
709
- <div class="space-y-1"><span class={modalLabel}>URL</span><input type="url" bind:value={imageUrl} placeholder="https://..." class={modalInput} /></div>
773
+ <div class="space-y-1"><span class="rte-label">URL</span><input type="url" bind:value={imageUrl} placeholder="https://..." class="rte-input" /></div>
710
774
  <div class="grid grid-cols-2 gap-3">
711
- <div class="space-y-1"><span class={modalLabel}>Alt</span><input type="text" bind:value={imageAlt} class={modalInput} /></div>
712
- <div class="space-y-1"><span class={modalLabel}>Title</span><input type="text" bind:value={imageTitle} class={modalInput} /></div>
775
+ <div class="space-y-1"><span class="rte-label">Alt</span><input type="text" bind:value={imageAlt} class="rte-input" /></div>
776
+ <div class="space-y-1"><span class="rte-label">Title</span><input type="text" bind:value={imageTitle} class="rte-input" /></div>
713
777
  </div>
714
778
  <div class="grid grid-cols-2 gap-3">
715
- <div class="space-y-1"><span class={modalLabel}>Largeur</span><input type="text" bind:value={imageWidth} placeholder="auto" class={modalInput} /></div>
716
- <div class="space-y-1"><span class={modalLabel}>Classes CSS</span><input type="text" bind:value={imageClass} placeholder="rounded shadow" class="{modalInput} font-mono text-xs" /></div>
779
+ <div class="space-y-1"><span class="rte-label">Largeur</span><input type="text" bind:value={imageWidth} placeholder="auto" class="rte-input" /></div>
780
+ <div class="space-y-1"><span class="rte-label">Classes CSS</span><input type="text" bind:value={imageClass} placeholder="rounded shadow" class="rte-input font-mono text-xs" /></div>
717
781
  </div>
718
- {#if imageUrl}<div class="rounded-lg border border-[var(--karbon-border)] overflow-hidden"><img src={imageUrl} alt={imageAlt} class="w-full max-h-40 object-contain bg-[var(--karbon-bg-2)]" /></div>{/if}
782
+ {#if imageUrl}<div class="rte-preview-box"><img src={imageUrl} alt={imageAlt} class="w-full max-h-40 object-contain" /></div>{/if}
719
783
  </div>
720
- <div class="mt-5 flex justify-end gap-2">
721
- <button type="button" onclick={() => showImageModal = false} class="{modalBtnCancel} cursor-pointer">Annuler</button>
722
- <button type="button" onclick={insertImage} disabled={!imageUrl} class="{modalBtnPrimary} cursor-pointer">Insérer</button>
784
+ <div class="rte-modal-footer justify-end">
785
+ <button type="button" onclick={() => showImageModal = false} class="rte-btn-cancel">Annuler</button>
786
+ <button type="button" onclick={insertImage} disabled={!imageUrl} class="rte-btn-primary">Insérer</button>
723
787
  </div>
724
788
  </div>
725
789
  </div>
@@ -727,36 +791,37 @@
727
791
 
728
792
  <!-- ═══ IMAGE PROPS MODAL ═══ -->
729
793
  {#if showImagePropsModal}
730
- <div class={modalOverlay} role="presentation" onclick={() => showImagePropsModal = false} onkeydown={(e) => { if (e.key === "Escape") showImagePropsModal = false }}>
731
- <div class="{modalBox} max-w-lg" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
732
- <div class="flex items-center justify-between mb-4">
733
- <h3 class="text-sm font-semibold text-[var(--karbon-text)]">Propriétés de l'image</h3>
734
- <button type="button" onclick={() => showImagePropsModal = false} class="text-[var(--karbon-text-4)] hover:text-[var(--karbon-text)] cursor-pointer" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
794
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
795
+ <div class="rte-overlay" role="presentation" onclick={() => showImagePropsModal = false} onkeydown={(e) => { if (e.key === "Escape") showImagePropsModal = false }}>
796
+ <div class="rte-modal max-w-lg" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
797
+ <div class="rte-modal-header">
798
+ <h3>Propriétés de l'image</h3>
799
+ <button type="button" onclick={() => showImagePropsModal = false} class="rte-modal-close" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
735
800
  </div>
736
- {#if imageUrl}<div class="mb-4 rounded-lg border border-[var(--karbon-border)] overflow-hidden bg-[var(--karbon-bg-2)]"><img src={imageUrl} alt="" class="mx-auto max-h-32 object-contain" /></div>{/if}
801
+ {#if imageUrl}<div class="rte-preview-box mb-4"><img src={imageUrl} alt="" class="mx-auto max-h-32 object-contain" /></div>{/if}
737
802
  <div class="space-y-3">
738
803
  <div class="grid grid-cols-2 gap-3">
739
- <div class="space-y-1"><span class={modalLabel}>Largeur</span><input type="text" bind:value={imageWidth} placeholder="auto" class={modalInput} /></div>
740
- <div class="space-y-1"><span class={modalLabel}>Hauteur</span><input type="text" bind:value={imageHeight} placeholder="auto" class={modalInput} /></div>
804
+ <div class="space-y-1"><span class="rte-label">Largeur</span><input type="text" bind:value={imageWidth} placeholder="auto" class="rte-input" /></div>
805
+ <div class="space-y-1"><span class="rte-label">Hauteur</span><input type="text" bind:value={imageHeight} placeholder="auto" class="rte-input" /></div>
741
806
  </div>
742
- <div class="space-y-1"><span class={modalLabel}>Texte alternatif</span><input type="text" bind:value={imageAlt} class={modalInput} /></div>
743
- <div class="space-y-1"><span class={modalLabel}>Title</span><input type="text" bind:value={imageTitle} class={modalInput} /></div>
807
+ <div class="space-y-1"><span class="rte-label">Texte alternatif</span><input type="text" bind:value={imageAlt} class="rte-input" /></div>
808
+ <div class="space-y-1"><span class="rte-label">Title</span><input type="text" bind:value={imageTitle} class="rte-input" /></div>
744
809
  <div class="space-y-1">
745
- <span class={modalLabel}>Alignement</span>
810
+ <span class="rte-label">Alignement</span>
746
811
  <div class="flex gap-1">
747
812
  {#each [{ v: '', l: 'Aucun' }, { v: 'left', l: 'Gauche' }, { v: 'center', l: 'Centre' }, { v: 'right', l: 'Droite' }] as opt}
748
- <button type="button" onclick={() => imageAlign = opt.v} class="rounded-md px-3 py-1.5 text-xs transition-colors cursor-pointer {imageAlign === opt.v ? 'bg-violet-500/15 text-violet-400 ring-1 ring-violet-500/30' : 'text-[var(--karbon-text-3)] hover:bg-[var(--karbon-bg-2)]'}">{opt.l}</button>
813
+ <button type="button" onclick={() => imageAlign = opt.v} class="rte-align-btn {imageAlign === opt.v ? 'rte-align-btn-active' : ''}">{opt.l}</button>
749
814
  {/each}
750
815
  </div>
751
816
  </div>
752
- <div class="space-y-1"><span class={modalLabel}>Classes CSS</span><input type="text" bind:value={imageClass} placeholder="ex: rounded shadow-lg" class={modalInput} /></div>
753
- <div class="space-y-1"><span class={modalLabel}>Style inline</span><input type="text" bind:value={imageStyle} placeholder="ex: border-radius: 8px" class="{modalInput} font-mono text-xs" /></div>
817
+ <div class="space-y-1"><span class="rte-label">Classes CSS</span><input type="text" bind:value={imageClass} placeholder="ex: rounded shadow-lg" class="rte-input" /></div>
818
+ <div class="space-y-1"><span class="rte-label">Style inline</span><input type="text" bind:value={imageStyle} placeholder="ex: border-radius: 8px" class="rte-input font-mono text-xs" /></div>
754
819
  </div>
755
- <div class="mt-5 flex items-center justify-between">
756
- <button type="button" onclick={deleteTargetImage} class="text-xs text-red-400 hover:text-red-300 cursor-pointer">Supprimer</button>
820
+ <div class="rte-modal-footer justify-between">
821
+ <button type="button" onclick={deleteTargetImage} class="rte-btn-danger-text">Supprimer</button>
757
822
  <div class="flex gap-2">
758
- <button type="button" onclick={() => showImagePropsModal = false} class="{modalBtnCancel} cursor-pointer">Annuler</button>
759
- <button type="button" onclick={applyImageProps} class="{modalBtnPrimary} cursor-pointer">Appliquer</button>
823
+ <button type="button" onclick={() => showImagePropsModal = false} class="rte-btn-cancel">Annuler</button>
824
+ <button type="button" onclick={applyImageProps} class="rte-btn-primary">Appliquer</button>
760
825
  </div>
761
826
  </div>
762
827
  </div>
@@ -765,20 +830,21 @@
765
830
 
766
831
  <!-- ═══ ELEMENT PROPS MODAL ═══ -->
767
832
  {#if showElementProps}
768
- <div class={modalOverlay} role="presentation" onclick={() => showElementProps = false} onkeydown={(e) => { if (e.key === "Escape") showElementProps = false }}>
769
- <div class="{modalBox} max-w-md" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
770
- <div class="flex items-center justify-between mb-4">
771
- <h3 class="text-sm font-semibold text-[var(--karbon-text)]">Propriétés : <code class="text-violet-400">&lt;{elPropsTag}&gt;</code></h3>
772
- <button type="button" onclick={() => showElementProps = false} class="text-[var(--karbon-text-4)] hover:text-[var(--karbon-text)] cursor-pointer" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
833
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
834
+ <div class="rte-overlay" role="presentation" onclick={() => showElementProps = false} onkeydown={(e) => { if (e.key === "Escape") showElementProps = false }}>
835
+ <div class="rte-modal max-w-md" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
836
+ <div class="rte-modal-header">
837
+ <h3>Propriétés : <code class="text-violet-400">&lt;{elPropsTag}&gt;</code></h3>
838
+ <button type="button" onclick={() => showElementProps = false} class="rte-modal-close" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
773
839
  </div>
774
840
  <div class="space-y-3">
775
- <div class="space-y-1"><span class={modalLabel}>ID</span><input type="text" bind:value={elPropsId} placeholder="identifiant" class="{modalInput} font-mono text-xs" /></div>
776
- <div class="space-y-1"><span class={modalLabel}>Classes CSS</span><input type="text" bind:value={elPropsClass} placeholder="class1 class2" class="{modalInput} font-mono text-xs" /></div>
777
- <div class="space-y-1"><span class={modalLabel}>Style inline</span><textarea bind:value={elPropsStyle} rows="3" placeholder="color: red; font-size: 16px;" class="{modalInput} font-mono text-xs"></textarea></div>
841
+ <div class="space-y-1"><span class="rte-label">ID</span><input type="text" bind:value={elPropsId} placeholder="identifiant" class="rte-input font-mono text-xs" /></div>
842
+ <div class="space-y-1"><span class="rte-label">Classes CSS</span><input type="text" bind:value={elPropsClass} placeholder="class1 class2" class="rte-input font-mono text-xs" /></div>
843
+ <div class="space-y-1"><span class="rte-label">Style inline</span><textarea bind:value={elPropsStyle} rows="3" placeholder="color: red; font-size: 16px;" class="rte-input font-mono text-xs"></textarea></div>
778
844
  </div>
779
- <div class="mt-5 flex justify-end gap-2">
780
- <button type="button" onclick={() => showElementProps = false} class="{modalBtnCancel} cursor-pointer">Annuler</button>
781
- <button type="button" onclick={applyElementProps} class="{modalBtnPrimary} cursor-pointer">Appliquer</button>
845
+ <div class="rte-modal-footer justify-end">
846
+ <button type="button" onclick={() => showElementProps = false} class="rte-btn-cancel">Annuler</button>
847
+ <button type="button" onclick={applyElementProps} class="rte-btn-primary">Appliquer</button>
782
848
  </div>
783
849
  </div>
784
850
  </div>
@@ -786,19 +852,20 @@
786
852
 
787
853
  <!-- ═══ TABLE MODAL ═══ -->
788
854
  {#if showTableModal}
789
- <div class={modalOverlay} role="presentation" onclick={() => showTableModal = false} onkeydown={(e) => { if (e.key === "Escape") showTableModal = false }}>
790
- <div class="{modalBox} max-w-sm" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
791
- <div class="flex items-center justify-between mb-4">
792
- <h3 class="text-sm font-semibold text-[var(--karbon-text)]">Insérer un tableau</h3>
793
- <button type="button" onclick={() => showTableModal = false} class="text-[var(--karbon-text-4)] hover:text-[var(--karbon-text)] cursor-pointer" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
855
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
856
+ <div class="rte-overlay" role="presentation" onclick={() => showTableModal = false} onkeydown={(e) => { if (e.key === "Escape") showTableModal = false }}>
857
+ <div class="rte-modal max-w-sm" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
858
+ <div class="rte-modal-header">
859
+ <h3>Insérer un tableau</h3>
860
+ <button type="button" onclick={() => showTableModal = false} class="rte-modal-close" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
794
861
  </div>
795
862
  <div class="grid grid-cols-2 gap-3">
796
- <div class="space-y-1"><span class={modalLabel}>Lignes</span><input type="number" bind:value={tableRows} min="1" max="20" class={modalInput} /></div>
797
- <div class="space-y-1"><span class={modalLabel}>Colonnes</span><input type="number" bind:value={tableCols} min="1" max="10" class={modalInput} /></div>
863
+ <div class="space-y-1"><span class="rte-label">Lignes</span><input type="number" bind:value={tableRows} min="1" max="20" class="rte-input" /></div>
864
+ <div class="space-y-1"><span class="rte-label">Colonnes</span><input type="number" bind:value={tableCols} min="1" max="10" class="rte-input" /></div>
798
865
  </div>
799
- <div class="mt-5 flex justify-end gap-2">
800
- <button type="button" onclick={() => showTableModal = false} class="{modalBtnCancel} cursor-pointer">Annuler</button>
801
- <button type="button" onclick={insertTable} class="{modalBtnPrimary} cursor-pointer">Insérer</button>
866
+ <div class="rte-modal-footer justify-end">
867
+ <button type="button" onclick={() => showTableModal = false} class="rte-btn-cancel">Annuler</button>
868
+ <button type="button" onclick={insertTable} class="rte-btn-primary">Insérer</button>
802
869
  </div>
803
870
  </div>
804
871
  </div>
@@ -806,77 +873,733 @@
806
873
 
807
874
  <!-- ═══ EMBED MODAL ═══ -->
808
875
  {#if showEmbedModal}
809
- <div class={modalOverlay} role="presentation" onclick={() => showEmbedModal = false} onkeydown={(e) => { if (e.key === "Escape") showEmbedModal = false }}>
810
- <div class="{modalBox} max-w-md" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
811
- <div class="flex items-center justify-between mb-4">
812
- <h3 class="text-sm font-semibold text-[var(--karbon-text)]">Embed vidéo</h3>
813
- <button type="button" onclick={() => showEmbedModal = false} class="text-[var(--karbon-text-4)] hover:text-[var(--karbon-text)] cursor-pointer" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
876
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
877
+ <div class="rte-overlay" role="presentation" onclick={() => showEmbedModal = false} onkeydown={(e) => { if (e.key === "Escape") showEmbedModal = false }}>
878
+ <div class="rte-modal max-w-md" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
879
+ <div class="rte-modal-header">
880
+ <h3>Embed vidéo</h3>
881
+ <button type="button" onclick={() => showEmbedModal = false} class="rte-modal-close" aria-label="Fermer"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
814
882
  </div>
815
- <p class="text-xs text-[var(--karbon-text-3)] mb-3">Collez une URL YouTube, Vimeo ou un lien embed.</p>
816
- <input type="url" bind:value={embedUrl} placeholder="https://www.youtube.com/watch?v=..." class={modalInput} onkeydown={(e) => { if (e.key === 'Enter') insertEmbed() }} />
817
- <div class="mt-5 flex justify-end gap-2">
818
- <button type="button" onclick={() => showEmbedModal = false} class="{modalBtnCancel} cursor-pointer">Annuler</button>
819
- <button type="button" onclick={insertEmbed} disabled={!embedUrl} class="{modalBtnPrimary} cursor-pointer">Insérer</button>
883
+ <p class="text-xs opacity-50 mb-3">Collez une URL YouTube, Vimeo ou un lien embed.</p>
884
+ <input type="url" bind:value={embedUrl} placeholder="https://www.youtube.com/watch?v=..." class="rte-input" onkeydown={(e) => { if (e.key === 'Enter') insertEmbed() }} />
885
+ <div class="rte-modal-footer justify-end">
886
+ <button type="button" onclick={() => showEmbedModal = false} class="rte-btn-cancel">Annuler</button>
887
+ <button type="button" onclick={insertEmbed} disabled={!embedUrl} class="rte-btn-primary">Insérer</button>
820
888
  </div>
821
889
  </div>
822
890
  </div>
823
891
  {/if}
824
892
 
825
- <!-- ═══ MEDIA EXPLORER (simple) ═══ -->
893
+ <!-- ═══ MEDIA EXPLORER ═══ -->
826
894
  {#if showMediaExplorer && media}
827
- <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" role="presentation" onclick={() => showMediaExplorer = false} onkeydown={(e) => { if (e.key === "Escape") showMediaExplorer = false }}>
828
- <div class="flex h-[70vh] w-[80vw] max-w-4xl flex-col rounded-2xl border border-[var(--karbon-border)] bg-[var(--karbon-bg-card)] shadow-2xl overflow-hidden" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
829
- <div class="flex items-center gap-3 border-b border-[var(--karbon-border)] px-5 py-3">
830
- <h2 class="text-sm font-semibold text-[var(--karbon-text)]">Explorateur de médias</h2>
895
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
896
+ <div class="rte-overlay" role="presentation" onclick={() => showMediaExplorer = false} onkeydown={(e) => { if (e.key === "Escape") showMediaExplorer = false }}>
897
+ <div class="rte-media-explorer" role="dialog" tabindex="-1"
898
+ onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}
899
+ ondragover={(e) => { e.preventDefault(); mediaDragOver = true }}
900
+ ondragleave={() => mediaDragOver = false}
901
+ ondrop={mediaHandleDrop}
902
+ >
903
+ <!-- Header -->
904
+ <div class="rte-media-header">
905
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-violet-400"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
906
+ <h2 class="text-sm font-semibold">Explorateur de médias</h2>
831
907
  <div class="flex-1"></div>
832
- <input type="text" bind:value={mediaSearch} placeholder="Rechercher..." class="{modalInput} w-48 !py-1.5 !text-xs" onkeydown={(e) => { if (e.key === 'Enter') browseMedia() }} />
833
- <button type="button" onclick={() => showMediaExplorer = false} class="rounded-lg p-1.5 text-[var(--karbon-text-4)] hover:bg-[var(--karbon-bg-2)] hover:text-[var(--karbon-text)] cursor-pointer" aria-label="Fermer">
908
+ <div class="relative">
909
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-2.5 top-1/2 -translate-y-1/2 opacity-40"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
910
+ <input type="text" bind:value={mediaSearch} placeholder="Filtrer..." class="rte-input w-48 !py-1.5 !pl-8 !text-xs" onkeydown={(e) => { if (e.key === 'Enter') browseMedia(mediaCurrentPath) }} />
911
+ </div>
912
+ <!-- View toggle -->
913
+ <div class="rte-media-view-toggle">
914
+ <button type="button" onclick={() => mediaViewMode = 'grid'} class="p-1.5 {mediaViewMode === 'grid' ? 'bg-violet-500/15 text-violet-400' : 'opacity-40 hover:opacity-70'}">
915
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></svg>
916
+ </button>
917
+ <button type="button" onclick={() => mediaViewMode = 'list'} class="p-1.5 {mediaViewMode === 'list' ? 'bg-violet-500/15 text-violet-400' : 'opacity-40 hover:opacity-70'}">
918
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
919
+ </button>
920
+ </div>
921
+ <button type="button" onclick={() => showMediaExplorer = false} class="rte-modal-close" aria-label="Fermer">
834
922
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
835
923
  </button>
836
924
  </div>
837
925
 
838
- <div class="flex-1 overflow-y-auto p-4">
926
+ <!-- Toolbar (nav + breadcrumbs + actions) -->
927
+ <div class="rte-media-toolbar">
928
+ <button type="button" onclick={mediaGoUp} disabled={!mediaCurrentPath} class="rte-btn disabled:opacity-30" aria-label="Remonter">
929
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
930
+ </button>
931
+ <button type="button" onclick={() => mediaNavigateTo('')} class="rte-btn" aria-label="Racine">
932
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
933
+ </button>
934
+ <button type="button" onclick={() => browseMedia(mediaCurrentPath)} class="rte-btn" aria-label="Rafraîchir">
935
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>
936
+ </button>
937
+ <!-- Breadcrumb -->
938
+ <div class="rte-media-breadcrumb">
939
+ <button type="button" onclick={() => mediaNavigateTo('')} class="hover:text-violet-400 transition-colors">uploads</button>
940
+ {#each mediaBreadcrumbs as crumb}
941
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
942
+ <button type="button" onclick={() => mediaNavigateTo(crumb.path)} class="hover:text-violet-400 transition-colors">{crumb.label}</button>
943
+ {/each}
944
+ </div>
945
+ <div class="flex-1"></div>
946
+ {#if media.createFolder}
947
+ <button type="button" onclick={() => { mediaShowNewFolder = true; mediaNewFolderName = '' }} class="rte-media-action-btn">
948
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 10v6"/><path d="M9 13h6"/><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
949
+ Nouveau dossier
950
+ </button>
951
+ {/if}
952
+ {#if media.upload}
953
+ <label class="rte-media-upload-btn">
954
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
955
+ Uploader
956
+ <input type="file" accept="image/*" multiple class="hidden" onchange={(e) => mediaHandleUpload(e.currentTarget.files)} />
957
+ </label>
958
+ {/if}
959
+ </div>
960
+
961
+ <!-- New folder inline -->
962
+ {#if mediaShowNewFolder}
963
+ <div class="rte-media-newfolder">
964
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-violet-400"><path d="M12 10v6"/><path d="M9 13h6"/><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
965
+ <input type="text" bind:value={mediaNewFolderName} placeholder="Nom du dossier..." class="rte-input flex-1 !py-1.5 !text-xs"
966
+ onkeydown={(e) => { if (e.key === 'Enter') mediaCreateFolder(); if (e.key === 'Escape') mediaShowNewFolder = false }} />
967
+ <button type="button" onclick={mediaCreateFolder} class="rte-btn-primary !py-1.5 !px-3">
968
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
969
+ </button>
970
+ <button type="button" onclick={() => mediaShowNewFolder = false} class="rte-modal-close">
971
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
972
+ </button>
973
+ </div>
974
+ {/if}
975
+
976
+ <!-- Content -->
977
+ <div class="rte-media-content {mediaDragOver ? 'ring-2 ring-inset ring-violet-500/50 bg-violet-500/5' : ''}">
978
+ {#if mediaUploading}
979
+ <div class="rte-media-overlay">
980
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin text-violet-400"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
981
+ <p class="text-sm opacity-60 mt-2">Upload en cours...</p>
982
+ </div>
983
+ {/if}
984
+
839
985
  {#if mediaLoading}
840
986
  <div class="flex items-center justify-center py-20">
841
987
  <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin text-violet-400"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
842
988
  </div>
843
- {:else if mediaFiles.length === 0}
844
- <div class="flex flex-col items-center justify-center py-20 text-[var(--karbon-text-4)]">
845
- <p class="text-sm">Aucun média trouvé</p>
989
+ {:else if mediaFiltered.length === 0}
990
+ <div class="flex flex-col items-center justify-center py-20 opacity-40">
991
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="mb-3 opacity-30"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
992
+ <p class="text-sm">Dossier vide</p>
993
+ <p class="mt-1 text-xs">Glissez des fichiers ici ou cliquez sur Uploader</p>
846
994
  </div>
847
- {:else}
848
- <div class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-3">
849
- {#each mediaFiles as file}
850
- <div
851
- class="group flex flex-col items-center rounded-lg p-2 cursor-pointer transition-all {mediaSelected?.id === file.id ? 'bg-violet-500/15 ring-1 ring-violet-500/40' : 'hover:bg-[var(--karbon-bg-2)]'}"
852
- role="option" tabindex="0" aria-selected={mediaSelected?.id === file.id} onclick={() => mediaSelected = mediaSelected?.id === file.id ? null : file} onkeydown={(e) => { if (e.key === "Enter") { mediaSelected = mediaSelected?.id === file.id ? null : file } }}
853
- ondblclick={() => { if (file.url) handleMediaSelect(file.url) }}
995
+ {:else if mediaViewMode === 'grid'}
996
+ <div class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 gap-3">
997
+ {#each mediaFiltered as entry}
998
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
999
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
1000
+ <div class="rte-media-item {mediaSelected?.path === entry.path ? 'rte-media-item-selected' : ''}"
1001
+ onclick={() => handleMediaEntryClick(entry)}
1002
+ ondblclick={() => handleMediaEntryDblClick(entry)}
854
1003
  >
855
- {#if file.type?.startsWith('image/')}
856
- <div class="h-16 w-16 overflow-hidden rounded-md bg-[var(--karbon-bg-2)]">
857
- <img src={file.url} alt={file.name} class="h-full w-full object-cover" loading="lazy" />
1004
+ {#if entry.is_dir}
1005
+ <div class="flex h-16 w-16 items-center justify-center">
1006
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-amber-400/80"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
858
1007
  </div>
1008
+ {:else if isImage(entry) && entry.url}
1009
+ <div class="rte-media-thumb"><img src={entry.url} alt={entry.name} class="h-full w-full object-cover" loading="lazy" /></div>
859
1010
  {:else}
860
1011
  <div class="flex h-16 w-16 items-center justify-center">
861
- <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--karbon-text-4)]"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>
1012
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="opacity-40"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>
862
1013
  </div>
863
1014
  {/if}
864
- <p class="mt-1.5 w-full truncate text-center text-[11px] text-[var(--karbon-text-2)]">{file.name}</p>
1015
+ <p class="mt-1.5 w-full truncate text-center text-[11px]">{entry.name}</p>
1016
+ <p class="text-[10px] opacity-40">{entry.is_dir ? 'Dossier' : formatSize(entry.size)}</p>
1017
+ </div>
1018
+ {/each}
1019
+ </div>
1020
+ {:else}
1021
+ <div class="rte-media-list">
1022
+ {#each mediaFiltered as entry, i}
1023
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
1024
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
1025
+ <div class="rte-media-list-item {i > 0 ? 'rte-media-list-divider' : ''} {mediaSelected?.path === entry.path ? 'bg-violet-500/10' : ''}"
1026
+ onclick={() => handleMediaEntryClick(entry)}
1027
+ ondblclick={() => handleMediaEntryDblClick(entry)}
1028
+ >
1029
+ {#if entry.is_dir}
1030
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-amber-400/80"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
1031
+ {:else if isImage(entry) && entry.url}
1032
+ <img src={entry.url} alt="" class="rte-media-list-thumb" loading="lazy" />
1033
+ {:else}
1034
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 opacity-40"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>
1035
+ {/if}
1036
+ <span class="flex-1 truncate text-xs">{entry.name}</span>
1037
+ <span class="text-[11px] opacity-40">{entry.is_dir ? 'Dossier' : formatSize(entry.size)}</span>
1038
+ {#if entry.modified}<span class="hidden sm:block text-[11px] opacity-40">{entry.modified}</span>{/if}
865
1039
  </div>
866
1040
  {/each}
867
1041
  </div>
868
1042
  {/if}
869
1043
  </div>
870
1044
 
871
- <div class="flex items-center gap-3 border-t border-[var(--karbon-border)] bg-[var(--karbon-bg-2)]/50 px-5 py-3">
872
- <p class="flex-1 text-xs text-[var(--karbon-text-4)]">{mediaFiles.length} fichier{mediaFiles.length !== 1 ? 's' : ''}</p>
873
- <button type="button" onclick={() => showMediaExplorer = false} class="{modalBtnCancel} cursor-pointer">Annuler</button>
874
- <button type="button" onclick={() => { if (mediaSelected?.url) handleMediaSelect(mediaSelected.url) }} disabled={!mediaSelected?.url} class="{modalBtnPrimary} cursor-pointer">Sélectionner</button>
1045
+ <!-- Footer -->
1046
+ <div class="rte-media-footer">
1047
+ {#if mediaSelected}
1048
+ <div class="flex items-center gap-3 flex-1 min-w-0">
1049
+ {#if isImage(mediaSelected) && mediaSelected.url}
1050
+ <img src={mediaSelected.url} alt="" class="h-10 w-10 rounded object-cover" />
1051
+ {/if}
1052
+ <div class="min-w-0">
1053
+ <p class="truncate text-xs font-medium">{mediaSelected.name}</p>
1054
+ <p class="text-[11px] opacity-40">{formatSize(mediaSelected.size)} · {mediaSelected.mime ?? 'Inconnu'}</p>
1055
+ </div>
1056
+ </div>
1057
+ {:else}
1058
+ <p class="flex-1 text-xs opacity-40">{mediaFiltered.length} élément{mediaFiltered.length !== 1 ? 's' : ''}</p>
1059
+ {/if}
1060
+ <button type="button" onclick={() => showMediaExplorer = false} class="rte-btn-cancel">Annuler</button>
1061
+ <button type="button" onclick={() => { if (mediaSelected?.url) handleMediaSelect(mediaSelected.url) }} disabled={!mediaSelected?.url} class="rte-btn-primary">Sélectionner</button>
875
1062
  </div>
876
1063
  </div>
877
1064
  </div>
878
1065
  {/if}
879
1066
 
1067
+ <!-- Color picker backdrop -->
880
1068
  {#if showColorPicker}
1069
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
881
1070
  <div class="fixed inset-0 z-10" role="presentation" onclick={() => showColorPicker = false} onkeydown={(e) => { if (e.key === "Escape") showColorPicker = false }}></div>
882
1071
  {/if}
1072
+
1073
+ <style>
1074
+ /* ═══════════════════════════════════════════════
1075
+ WRAPPER
1076
+ ═══════════════════════════════════════════════ */
1077
+ .rte-wrapper {
1078
+ @apply rounded-xl shadow-sm overflow-hidden;
1079
+ border: 1px solid var(--karbon-border);
1080
+ background: var(--karbon-bg-card);
1081
+ }
1082
+
1083
+ /* ═══════════════════════════════════════════════
1084
+ TOOLBAR
1085
+ ═══════════════════════════════════════════════ */
1086
+ .rte-toolbar {
1087
+ @apply flex flex-wrap items-center gap-1 px-3 py-2 select-none;
1088
+ border-bottom: 1px solid var(--karbon-border);
1089
+ background: color-mix(in srgb, var(--karbon-bg-2) 50%, transparent);
1090
+ }
1091
+
1092
+ .rte-btn {
1093
+ @apply flex items-center justify-center rounded-md transition-all cursor-pointer border-none bg-transparent px-1;
1094
+ min-width: 32px;
1095
+ height: 32px;
1096
+ color: var(--karbon-text-3);
1097
+ }
1098
+
1099
+ .rte-btn:hover {
1100
+ background: var(--karbon-bg-2);
1101
+ color: var(--karbon-text);
1102
+ }
1103
+
1104
+ .rte-btn-active {
1105
+ @apply bg-violet-500/15 text-violet-400;
1106
+ }
1107
+
1108
+ .rte-btn-active:hover {
1109
+ @apply bg-violet-500/20 text-violet-400;
1110
+ }
1111
+
1112
+ .rte-sep {
1113
+ @apply mx-0.5 shrink-0;
1114
+ width: 1px;
1115
+ height: 22px;
1116
+ background: var(--karbon-border);
1117
+ }
1118
+
1119
+ .rte-select {
1120
+ @apply rounded-[5px] px-1 py-0.5 text-[0.6875rem] outline-none cursor-pointer;
1121
+ background: var(--karbon-bg-input);
1122
+ border: 1px solid var(--karbon-border-input);
1123
+ color: var(--karbon-text-3);
1124
+ }
1125
+
1126
+ /* ═══════════════════════════════════════════════
1127
+ FIND/REPLACE BAR
1128
+ ═══════════════════════════════════════════════ */
1129
+ .rte-findbar {
1130
+ @apply flex items-center gap-2 px-4 py-2;
1131
+ border-bottom: 1px solid var(--karbon-border);
1132
+ background: color-mix(in srgb, var(--karbon-bg-2) 30%, transparent);
1133
+ }
1134
+
1135
+ .rte-bar-input {
1136
+ @apply rounded-md px-2 py-1 text-xs outline-none;
1137
+ background: var(--karbon-bg-input);
1138
+ border: 1px solid var(--karbon-border-input);
1139
+ color: var(--karbon-text);
1140
+ }
1141
+
1142
+ .rte-bar-input:focus {
1143
+ border-color: var(--karbon-border-input-focus);
1144
+ }
1145
+
1146
+ .rte-bar-btn {
1147
+ @apply rounded-md px-2.5 py-1 text-[0.6875rem] cursor-pointer transition-all;
1148
+ background: var(--karbon-bg-2);
1149
+ border: 1px solid var(--karbon-border);
1150
+ color: var(--karbon-text-3);
1151
+ }
1152
+
1153
+ .rte-bar-btn:hover {
1154
+ background: var(--karbon-bg-card);
1155
+ color: var(--karbon-text);
1156
+ }
1157
+
1158
+ /* ═══════════════════════════════════════════════
1159
+ STATUS BAR
1160
+ ═══════════════════════════════════════════════ */
1161
+ .rte-statusbar {
1162
+ @apply flex items-center gap-4 px-4 py-1.5 text-[11px];
1163
+ border-top: 1px solid var(--karbon-border);
1164
+ background: color-mix(in srgb, var(--karbon-bg-2) 30%, transparent);
1165
+ color: var(--karbon-text-4);
1166
+ }
1167
+
1168
+ /* ═══════════════════════════════════════════════
1169
+ CONTENT AREA — Default theme
1170
+ ═══════════════════════════════════════════════ */
1171
+ .rte-content {
1172
+ padding: 1.25rem 1.5rem;
1173
+ color: var(--karbon-text);
1174
+ font-size: 0.9375rem;
1175
+ line-height: 1.75;
1176
+ }
1177
+
1178
+ .rte-content:empty:before {
1179
+ content: attr(data-placeholder);
1180
+ color: var(--karbon-text-4);
1181
+ pointer-events: none;
1182
+ }
1183
+
1184
+ .rte-content :global(h2) { @apply text-2xl font-bold mt-4 mb-2; }
1185
+ .rte-content :global(h3) { @apply text-xl font-semibold mt-3 mb-1.5; }
1186
+ .rte-content :global(h4) { @apply text-lg font-semibold mt-2.5 mb-1; }
1187
+ .rte-content :global(p) { @apply my-2; }
1188
+ .rte-content :global(a) { @apply text-violet-500 underline; }
1189
+ .rte-content :global(blockquote) {
1190
+ @apply border-l-[3px] border-violet-500 pl-4 py-2 my-4 rounded-r-lg italic;
1191
+ background: rgba(139, 92, 246, 0.05);
1192
+ }
1193
+ .rte-content :global(pre) {
1194
+ @apply rounded-lg p-4 font-mono text-sm overflow-x-auto my-4;
1195
+ background: var(--karbon-bg-2);
1196
+ border: 1px solid var(--karbon-border);
1197
+ }
1198
+ .rte-content :global(code) {
1199
+ @apply px-1.5 py-0.5 rounded text-sm;
1200
+ background: var(--karbon-bg-2);
1201
+ }
1202
+ .rte-content :global(img) {
1203
+ @apply max-w-full h-auto rounded-lg my-4 cursor-pointer;
1204
+ }
1205
+ .rte-content :global(img):hover {
1206
+ @apply outline outline-2 outline-violet-500/50 outline-offset-2;
1207
+ }
1208
+ .rte-content :global(hr) {
1209
+ @apply border-none my-6;
1210
+ border-top: 1px solid var(--karbon-border);
1211
+ }
1212
+ .rte-content :global(ul), .rte-content :global(ol) { @apply pl-6 my-2; }
1213
+ .rte-content :global(li) { @apply my-1; }
1214
+ .rte-content :global(table) { @apply w-full border-collapse my-4; }
1215
+ .rte-content :global(th) {
1216
+ @apply px-3 py-2 font-semibold text-sm;
1217
+ border: 1px solid var(--karbon-border);
1218
+ background: var(--karbon-bg-2);
1219
+ }
1220
+ .rte-content :global(td) {
1221
+ @apply px-3 py-2;
1222
+ border: 1px solid var(--karbon-border);
1223
+ }
1224
+ .rte-content :global(.embed-responsive) {
1225
+ @apply relative h-0 overflow-hidden my-4 rounded-lg;
1226
+ padding-bottom: 56.25%;
1227
+ }
1228
+ .rte-content :global(.embed-responsive iframe) {
1229
+ @apply absolute inset-0 w-full h-full border-0;
1230
+ }
1231
+
1232
+ /* ═══════════════════════════════════════════════
1233
+ CONTENT — Prose theme (article/blog)
1234
+ ═══════════════════════════════════════════════ */
1235
+ .rte-prose {
1236
+ line-height: 1.9;
1237
+ padding: 2rem 2.5rem;
1238
+ }
1239
+
1240
+ .rte-prose :global(h2) { @apply text-3xl font-bold mt-8 mb-3; }
1241
+ .rte-prose :global(h3) { @apply text-2xl font-semibold mt-6 mb-2; }
1242
+ .rte-prose :global(h4) { @apply text-xl font-semibold mt-4 mb-1.5; }
1243
+ .rte-prose :global(p) { @apply my-4; }
1244
+ .rte-prose :global(blockquote) { @apply pl-6 py-3 my-6; }
1245
+ .rte-prose :global(img) { @apply my-6; }
1246
+ .rte-prose :global(hr) { @apply my-10; }
1247
+
1248
+ /* ═══════════════════════════════════════════════
1249
+ CONTENT — Compact theme (admin/dashboard)
1250
+ ═══════════════════════════════════════════════ */
1251
+ .rte-compact {
1252
+ padding: 0.75rem 1rem;
1253
+ font-size: 0.8125rem;
1254
+ line-height: 1.6;
1255
+ }
1256
+
1257
+ .rte-compact :global(h2) { @apply text-lg font-semibold mt-2 mb-1; }
1258
+ .rte-compact :global(h3) { @apply text-base font-semibold mt-1.5 mb-0.5; }
1259
+ .rte-compact :global(h4) { @apply text-sm font-semibold mt-1 mb-0.5; }
1260
+ .rte-compact :global(p) { @apply my-1; }
1261
+ .rte-compact :global(blockquote) { @apply pl-3 py-1 my-2; }
1262
+ .rte-compact :global(img) { @apply my-2 rounded-md; }
1263
+ .rte-compact :global(hr) { @apply my-3; }
1264
+
1265
+ /* ═══════════════════════════════════════════════
1266
+ CONTENT — Minimal theme (neutral)
1267
+ ═══════════════════════════════════════════════ */
1268
+ .rte-minimal {
1269
+ padding: 1rem 1.25rem;
1270
+ font-size: 0.875rem;
1271
+ line-height: 1.7;
1272
+ }
1273
+
1274
+ .rte-minimal :global(h2) { @apply text-xl font-semibold mt-3 mb-1.5; }
1275
+ .rte-minimal :global(h3) { @apply text-lg font-medium mt-2 mb-1; }
1276
+ .rte-minimal :global(h4) { @apply text-base font-medium mt-1.5 mb-0.5; }
1277
+ .rte-minimal :global(a) { @apply text-blue-500 underline; }
1278
+ .rte-minimal :global(blockquote) {
1279
+ @apply border-gray-400 bg-transparent;
1280
+ }
1281
+
1282
+ /* ═══════════════════════════════════════════════
1283
+ SOURCE MODE
1284
+ ═══════════════════════════════════════════════ */
1285
+ .rte-source-wrapper {
1286
+ @apply relative flex overflow-hidden;
1287
+ background: #0d1117;
1288
+ }
1289
+
1290
+ .rte-line-numbers {
1291
+ @apply shrink-0 py-4 text-right select-none overflow-hidden;
1292
+ width: 48px;
1293
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
1294
+ font-size: 0.8125rem;
1295
+ line-height: 1.625;
1296
+ color: #484f58;
1297
+ border-right: 1px solid #21262d;
1298
+ }
1299
+
1300
+ .rte-line-numbers :global(div) { padding-right: 12px; }
1301
+
1302
+ .rte-highlight-layer {
1303
+ @apply absolute top-0 right-0 bottom-0 p-4 whitespace-pre overflow-hidden pointer-events-none;
1304
+ left: 48px;
1305
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
1306
+ font-size: 0.8125rem;
1307
+ line-height: 1.625;
1308
+ color: #c9d1d9;
1309
+ }
1310
+
1311
+ .rte-source-textarea {
1312
+ @apply absolute top-0 right-0 bottom-0 p-4 m-0 border-none outline-none resize-none whitespace-pre overflow-auto;
1313
+ left: 48px;
1314
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
1315
+ font-size: 0.8125rem;
1316
+ line-height: 1.625;
1317
+ color: transparent;
1318
+ caret-color: #c9d1d9;
1319
+ background: transparent;
1320
+ -webkit-text-fill-color: transparent;
1321
+ }
1322
+
1323
+ .rte-highlight-layer :global(.hl-tag) { color: #7ee787; }
1324
+ .rte-highlight-layer :global(.hl-bracket) { color: #8b949e; }
1325
+ .rte-highlight-layer :global(.hl-attr) { color: #79c0ff; }
1326
+ .rte-highlight-layer :global(.hl-eq) { color: #8b949e; }
1327
+ .rte-highlight-layer :global(.hl-val) { color: #a5d6ff; }
1328
+ .rte-highlight-layer :global(.hl-comment) { color: #484f58; font-style: italic; }
1329
+
1330
+ /* ═══════════════════════════════════════════════
1331
+ CONTEXT MENU
1332
+ ═══════════════════════════════════════════════ */
1333
+ .ctx-menu {
1334
+ @apply fixed z-[61] w-56 rounded-xl shadow-2xl shadow-black/30 backdrop-blur-xl overflow-hidden;
1335
+ border: 1px solid var(--karbon-border);
1336
+ background: var(--karbon-bg-card);
1337
+ }
1338
+
1339
+ .ctx-header {
1340
+ @apply px-3 py-2 text-[11px] font-semibold uppercase tracking-wider;
1341
+ border-bottom: 1px solid var(--karbon-border);
1342
+ background: color-mix(in srgb, var(--karbon-bg-2) 50%, transparent);
1343
+ color: var(--karbon-text-4);
1344
+ }
1345
+
1346
+ .ctx-item {
1347
+ @apply flex items-center gap-2 w-full text-left px-3 py-[7px] text-xs bg-transparent border-none cursor-pointer transition-all;
1348
+ color: var(--karbon-text-2);
1349
+ }
1350
+
1351
+ .ctx-item:hover {
1352
+ background: rgba(139, 92, 246, 0.08);
1353
+ color: var(--karbon-text);
1354
+ }
1355
+
1356
+ .ctx-danger { @apply text-red-400; }
1357
+ .ctx-danger:hover { @apply bg-red-500/10 text-red-500; }
1358
+ .ctx-warn { @apply text-amber-400; }
1359
+ .ctx-warn:hover { @apply bg-amber-500/10 text-amber-500; }
1360
+
1361
+ .ctx-divider {
1362
+ @apply py-1;
1363
+ border-top: 1px solid var(--karbon-border);
1364
+ }
1365
+
1366
+ .ctx-section-title {
1367
+ @apply px-3 py-1 text-[10px] font-semibold uppercase tracking-wider;
1368
+ color: var(--karbon-text-4);
1369
+ }
1370
+
1371
+ /* ═══════════════════════════════════════════════
1372
+ MODALS
1373
+ ═══════════════════════════════════════════════ */
1374
+ .rte-overlay {
1375
+ @apply fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm;
1376
+ }
1377
+
1378
+ .rte-modal {
1379
+ @apply w-full rounded-xl p-6 shadow-2xl;
1380
+ border: 1px solid var(--karbon-border);
1381
+ background: var(--karbon-bg-card);
1382
+ }
1383
+
1384
+ .rte-modal-header {
1385
+ @apply flex items-center justify-between mb-4;
1386
+ }
1387
+
1388
+ .rte-modal-header h3 {
1389
+ @apply text-sm font-semibold;
1390
+ color: var(--karbon-text);
1391
+ }
1392
+
1393
+ .rte-modal-close {
1394
+ @apply cursor-pointer bg-transparent border-none;
1395
+ color: var(--karbon-text-4);
1396
+ }
1397
+
1398
+ .rte-modal-close:hover {
1399
+ color: var(--karbon-text);
1400
+ }
1401
+
1402
+ .rte-modal-footer {
1403
+ @apply mt-5 flex items-center gap-2;
1404
+ }
1405
+
1406
+ .rte-input {
1407
+ @apply block w-full rounded-lg px-3 py-2 text-sm outline-none transition-colors;
1408
+ border: 1px solid var(--karbon-border-input);
1409
+ background: var(--karbon-bg-input);
1410
+ color: var(--karbon-text);
1411
+ }
1412
+
1413
+ .rte-input:focus {
1414
+ border-color: var(--karbon-border-input-focus);
1415
+ }
1416
+
1417
+ .rte-label {
1418
+ @apply block text-xs font-medium;
1419
+ color: var(--karbon-text-4);
1420
+ }
1421
+
1422
+ .rte-label-inline {
1423
+ @apply text-sm;
1424
+ color: var(--karbon-text-2);
1425
+ }
1426
+
1427
+ .rte-btn-primary {
1428
+ @apply rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-medium text-white cursor-pointer;
1429
+ }
1430
+
1431
+ .rte-btn-primary:hover { @apply bg-violet-700; }
1432
+ .rte-btn-primary:disabled { @apply opacity-40 cursor-not-allowed; }
1433
+
1434
+ .rte-btn-cancel {
1435
+ @apply rounded-lg px-3 py-1.5 text-xs cursor-pointer;
1436
+ border: 1px solid var(--karbon-border);
1437
+ color: var(--karbon-text-3);
1438
+ }
1439
+
1440
+ .rte-btn-cancel:hover {
1441
+ background: var(--karbon-bg-2);
1442
+ }
1443
+
1444
+ .rte-btn-danger-text {
1445
+ @apply text-xs text-red-400 cursor-pointer bg-transparent border-none;
1446
+ }
1447
+
1448
+ .rte-btn-danger-text:hover { @apply text-red-300; }
1449
+
1450
+ .rte-upload-zone {
1451
+ @apply flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 transition-colors;
1452
+ border-color: var(--karbon-border);
1453
+ }
1454
+
1455
+ .rte-upload-zone:hover {
1456
+ border-color: var(--karbon-border-input);
1457
+ }
1458
+
1459
+ .rte-preview-box {
1460
+ @apply rounded-lg overflow-hidden;
1461
+ border: 1px solid var(--karbon-border);
1462
+ background: var(--karbon-bg-2);
1463
+ }
1464
+
1465
+ .rte-align-btn {
1466
+ @apply rounded-md px-3 py-1.5 text-xs transition-colors cursor-pointer;
1467
+ color: var(--karbon-text-3);
1468
+ }
1469
+
1470
+ .rte-align-btn:hover {
1471
+ background: var(--karbon-bg-2);
1472
+ }
1473
+
1474
+ .rte-align-btn-active {
1475
+ @apply bg-violet-500/15 text-violet-400 ring-1 ring-violet-500/30;
1476
+ }
1477
+
1478
+ /* ═══════════════════════════════════════════════
1479
+ COLOR PICKER
1480
+ ═══════════════════════════════════════════════ */
1481
+ .rte-color-grid {
1482
+ @apply absolute left-0 top-full z-20 mt-1 grid grid-cols-8 gap-1 rounded-lg p-2 shadow-xl;
1483
+ border: 1px solid var(--karbon-border);
1484
+ background: var(--karbon-bg-card);
1485
+ }
1486
+
1487
+ .rte-color-swatch {
1488
+ @apply h-5 w-5 rounded cursor-pointer transition-transform;
1489
+ border: 1px solid var(--karbon-border);
1490
+ }
1491
+
1492
+ .rte-color-swatch:hover { @apply scale-125; }
1493
+
1494
+ /* ═══════════════════════════════════════════════
1495
+ MEDIA EXPLORER
1496
+ ═══════════════════════════════════════════════ */
1497
+ .rte-media-explorer {
1498
+ @apply flex flex-col rounded-2xl shadow-2xl overflow-hidden;
1499
+ height: 85vh;
1500
+ width: 90vw;
1501
+ max-width: 64rem;
1502
+ border: 1px solid var(--karbon-border);
1503
+ background: var(--karbon-bg-card);
1504
+ }
1505
+
1506
+ .rte-media-header {
1507
+ @apply flex items-center gap-3 px-5 py-3;
1508
+ border-bottom: 1px solid var(--karbon-border);
1509
+ color: var(--karbon-text);
1510
+ }
1511
+
1512
+ .rte-media-toolbar {
1513
+ @apply flex items-center gap-2 px-5 py-2;
1514
+ border-bottom: 1px solid var(--karbon-border);
1515
+ background: color-mix(in srgb, var(--karbon-bg-2) 50%, transparent);
1516
+ }
1517
+
1518
+ .rte-media-breadcrumb {
1519
+ @apply flex items-center gap-1 text-xs ml-2;
1520
+ color: var(--karbon-text-4);
1521
+ }
1522
+
1523
+ .rte-media-action-btn {
1524
+ @apply flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium cursor-pointer bg-transparent border-none transition-colors;
1525
+ color: var(--karbon-text-3);
1526
+ }
1527
+
1528
+ .rte-media-action-btn:hover {
1529
+ background: var(--karbon-bg-2);
1530
+ }
1531
+
1532
+ .rte-media-upload-btn {
1533
+ @apply flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium text-violet-400 cursor-pointer transition-colors;
1534
+ background: rgba(139, 92, 246, 0.1);
1535
+ }
1536
+
1537
+ .rte-media-upload-btn:hover {
1538
+ background: rgba(139, 92, 246, 0.2);
1539
+ }
1540
+
1541
+ .rte-media-view-toggle {
1542
+ @apply flex items-center rounded-lg overflow-hidden;
1543
+ border: 1px solid var(--karbon-border-input);
1544
+ }
1545
+
1546
+ .rte-media-newfolder {
1547
+ @apply flex items-center gap-2 px-5 py-2;
1548
+ border-bottom: 1px solid var(--karbon-border);
1549
+ background: rgba(139, 92, 246, 0.05);
1550
+ }
1551
+
1552
+ .rte-media-content {
1553
+ @apply relative flex-1 overflow-y-auto p-4;
1554
+ }
1555
+
1556
+ .rte-media-overlay {
1557
+ @apply absolute inset-0 z-10 flex flex-col items-center justify-center backdrop-blur-sm;
1558
+ background: color-mix(in srgb, var(--karbon-bg-card) 80%, transparent);
1559
+ }
1560
+
1561
+ .rte-media-footer {
1562
+ @apply flex items-center gap-3 px-5 py-3;
1563
+ border-top: 1px solid var(--karbon-border);
1564
+ background: color-mix(in srgb, var(--karbon-bg-2) 50%, transparent);
1565
+ }
1566
+
1567
+ .rte-media-item {
1568
+ @apply flex flex-col items-center rounded-lg p-2 cursor-pointer transition-all;
1569
+ }
1570
+
1571
+ .rte-media-item:hover {
1572
+ background: var(--karbon-bg-2);
1573
+ }
1574
+
1575
+ .rte-media-item-selected {
1576
+ @apply bg-violet-500/15 ring-1 ring-violet-500/40;
1577
+ }
1578
+
1579
+ .rte-media-thumb {
1580
+ @apply h-16 w-16 overflow-hidden rounded-md;
1581
+ background: var(--karbon-bg-2);
1582
+ }
1583
+
1584
+ .rte-media-list {
1585
+ @apply rounded-lg overflow-hidden;
1586
+ border: 1px solid var(--karbon-border);
1587
+ }
1588
+
1589
+ .rte-media-list-item {
1590
+ @apply flex items-center gap-3 px-4 py-2 cursor-pointer transition-colors;
1591
+ }
1592
+
1593
+ .rte-media-list-item:hover {
1594
+ background: var(--karbon-bg-2);
1595
+ }
1596
+
1597
+ .rte-media-list-divider {
1598
+ border-top: 1px solid var(--karbon-border);
1599
+ }
1600
+
1601
+ .rte-media-list-thumb {
1602
+ @apply h-8 w-8 shrink-0 rounded object-cover;
1603
+ background: var(--karbon-bg-2);
1604
+ }
1605
+ </style>