@edc-motor/ui 0.4.31 → 4.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edc-motor/ui",
3
- "version": "0.4.31",
3
+ "version": "4.0.29",
4
4
  "description": "EdC Motor — componentes públicos Vue 3 + tokens SCSS para webs de juegos de mesa (paquete fuente: lo compila el consumidor con Vite)",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
@@ -39,17 +39,14 @@
39
39
  &--quote.block--w-narrow .block__inner { max-width: min(780px, 90%); }
40
40
 
41
41
  // Tipografía de bloque: texto BASE $fs-16 (el mismo cuerpo que el wysiwyg
42
- // por defecto — sin override aquí); título y subtítulo calcan la cabecera
43
- // clásica de índices y fichas de CDL: título $fs-28 compacto (line-height
44
- // 1.1, el h1 de base) y subtítulo atenuado AL TAMAÑO del cuerpo — la
45
- // jerarquía la da el color, no otro escalón de tamaño. Vale para TODOS
46
- // los bloques (la cabecera ya no sube a 40/28). Los títulos nunca se
47
- // justifican.
42
+ // por defecto — sin override aquí), subtítulo $fs-24 (entre el título y
43
+ // la base), título $fs-32 (la cabecera sube a 40/28). Los títulos nunca
44
+ // se justifican.
48
45
  // width 100%: en grids con justify-items start (el cuerpo del CTA) el
49
46
  // elemento encoge a su contenido y el text-align no se vería.
50
- &__title { margin: 0; font-size: $fs-28; line-height: 1.1; width: 100%; }
47
+ &__title { margin: 0; font-size: $fs-32; width: 100%; }
51
48
  // pre-line: el subtítulo es un textarea — sus saltos de línea cuentan.
52
- &__subtitle { margin: 0; color: $text-2; font-size: $fs-16; white-space: pre-line; width: 100%; }
49
+ &__subtitle { margin: 0; color: $text-2; font-size: $fs-24; white-space: pre-line; width: 100%; }
53
50
  &__text { color: $text-1; font-size: $fs-16; }
54
51
 
55
52
  // Alineación del bloque (campo común `align`): por CLASE (BlockShell ya
@@ -121,6 +118,9 @@
121
118
  .edc-button { justify-self: start; text-decoration: none; }
122
119
  }
123
120
 
121
+ &--header .block__title { font-size: $fs-40; }
122
+ &--header .block__subtitle { font-size: $fs-28; }
123
+
124
124
  &__media-layout { display: grid; gap: $space-4; }
125
125
  &__image {
126
126
  max-width: 100%;
@@ -17,12 +17,7 @@
17
17
  &__zone {
18
18
  position: relative;
19
19
  display: flex;
20
- // Centrado EN EL CAMPO: horizontal siempre, y vertical cuando el
21
- // contenedor le da alto extra (p. ej. el grupo a dos columnas del form
22
- // de bloque) — los márgenes auto solo absorben espacio si lo hay.
23
- align-self: center;
24
- margin-block: auto;
25
- flex-shrink: 0;
20
+ align-self: flex-start;
26
21
  align-items: center;
27
22
  justify-content: center;
28
23
  width: 160px;
@@ -86,10 +81,8 @@
86
81
  &__icon { width: 32px; height: 32px; color: $text-3; }
87
82
  &__text { font-size: $fs-12; color: $text-2; }
88
83
 
89
- // Nombre del fichero (pendiente o guardado) bajo la miniatura, centrado
90
- // con ella.
84
+ // Nombre del fichero (pendiente o guardado) bajo la miniatura.
91
85
  &__name {
92
- align-self: center;
93
86
  max-width: 160px;
94
87
  overflow: hidden;
95
88
  font-size: $fs-12;
@@ -34,14 +34,13 @@
34
34
  > :first-child { margin-block-start: 0; }
35
35
  > :last-child { margin-block-end: 0; }
36
36
 
37
- // Escala del wysiwyg, POR DEBAJO del título de bloque ($fs-28 desde que
38
- // los bloques calcan la cabecera clásica): h2 24 · h3 20 · h4 18 · h5 16
39
- // (como el cuerpo, se distingue por el peso) · h6 14.
40
- h2 { font-size: $fs-24; }
41
- h3 { font-size: $fs-20; }
42
- h4 { font-size: $fs-18; }
43
- h5 { font-size: $fs-16; }
44
- h6 { font-size: $fs-14; }
37
+ // Escala del wysiwyg: h2 32 (el mismo tamaño que un título de bloque) ·
38
+ // h3 24 · h4 22 (no hay token entre 20 y 24) · h5 20 · h6 18.
39
+ h2 { font-size: $fs-32; }
40
+ h3 { font-size: $fs-24; }
41
+ h4 { font-size: 22px; }
42
+ h5 { font-size: $fs-20; }
43
+ h6 { font-size: $fs-18; }
45
44
 
46
45
  ul,
47
46
  ol {
@@ -69,21 +69,34 @@
69
69
  outline: none;
70
70
  }
71
71
 
72
- // Franjas del editor (URL del enlace, iconos): EN FLUJO entre la toolbar y
73
- // el contenido nada flota, así que dentro de un modal quedan siempre a la
74
- // vista enteras. Mismo fondo que la toolbar, de la que son una extensión.
75
- &__link-bar {
72
+ // Selector de iconos del juego (y el mismo contenedor sirve de anclaje
73
+ // para el mini-popover del enlace).
74
+ &__picker { position: relative; display: inline-flex; }
75
+
76
+ // Mini-popover del enlace: input de URL + botón de aplicar, misma
77
+ // posición/estética que el panel de iconos.
78
+ &__link-popover {
79
+ position: absolute;
80
+ top: calc(100% + 4px);
81
+ left: 0;
82
+ z-index: 100;
76
83
  display: flex;
77
84
  align-items: center;
78
85
  gap: 2px;
86
+ // `width: max-content` (no simplemente `auto`): dentro de un ancla tan
87
+ // angosta como el propio botón (`.rich-text__picker`), el algoritmo de
88
+ // shrink-to-fit de un absoluto con solo `left` fijado lo recorta al
89
+ // ancho del ancla — el input de 220px quedaba aplastado a ~35px.
90
+ width: max-content;
79
91
  padding: $space-1;
80
- border-bottom: 1px solid $border;
81
- background: $surface-2;
92
+ background: $surface;
93
+ border: 1px solid $border;
94
+ border-radius: $radius-md;
95
+ box-shadow: $shadow-md;
82
96
  }
83
97
 
84
98
  &__link-input {
85
- flex: 1;
86
- min-width: 0;
99
+ width: 220px;
87
100
  height: 30px;
88
101
  padding: 0 $space-2;
89
102
  border: 1px solid $input-border;
@@ -100,16 +113,21 @@
100
113
  }
101
114
 
102
115
  &__icons {
116
+ position: absolute;
117
+ top: calc(100% + 4px);
118
+ left: 0;
119
+ z-index: 100;
103
120
  display: grid;
104
- // A todo el ancho del editor: caben muchos más iconos por fila que en el
105
- // popover de 5 columnas de antes; si aun así no entran, scroll propio.
106
- grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
121
+ grid-template-columns: repeat(5, 1fr);
107
122
  gap: 2px;
123
+ max-width: 220px;
108
124
  max-height: 200px;
109
125
  overflow-y: auto;
110
126
  padding: $space-1;
111
- border-bottom: 1px solid $border;
112
- background: $surface-2;
127
+ background: $surface;
128
+ border: 1px solid $border;
129
+ border-radius: $radius-md;
130
+ box-shadow: $shadow-md;
113
131
  }
114
132
 
115
133
  &__icon {
@@ -3,6 +3,7 @@ import { computed, ref, watch, onBeforeUnmount, onMounted, nextTick } from 'vue'
3
3
  import { useEditor, EditorContent } from '@tiptap/vue-3'
4
4
  import StarterKit from '@tiptap/starter-kit'
5
5
  import Image from '@tiptap/extension-image'
6
+ import { useDropdownPanel } from '../composables/useDropdownPanel'
6
7
  import UnderlineExtension from '@tiptap/extension-underline'
7
8
  import LinkExtension from '@tiptap/extension-link'
8
9
  import TableExtension from '@tiptap/extension-table'
@@ -330,23 +331,25 @@ function insertTable() {
330
331
  }
331
332
 
332
333
  // --- Selector de iconos ---
333
- // Los paneles del editor (iconos y enlace) NO flotan: son franjas EN FLUJO
334
- // entre la toolbar y el contenido — dentro de un modal quedan siempre a la
335
- // vista (nada que recortar ni que anclar por coordenadas).
336
334
  const pickerOpen = ref(false)
337
335
  const pickerRef = ref<HTMLElement | null>(null)
338
336
  const iconsPanelRef = ref<HTMLElement | null>(null)
337
+ // Top layer (popover): dentro de un modal, el overflow del cuerpo recortaba
338
+ // el panel y no se podían elegir todos los iconos. Ancho propio, sin igualar
339
+ // al trigger.
340
+ useDropdownPanel(pickerRef, iconsPanelRef, pickerOpen, { matchWidth: false })
339
341
 
340
342
  function insertIcon(icon: RichIcon) {
341
343
  editor.value?.chain().focus().setImage({ src: icon.url, alt: icon.name, title: icon.name }).run()
342
344
  pickerOpen.value = false
343
345
  }
344
346
 
345
- // --- Enlaces: franja con la URL (mismo patrón que el selector de iconos:
346
- // botón + franja bajo la toolbar, cierra con Escape o click fuera) ---
347
+ // --- Enlaces: mini-popover con la URL (mismo patrón que el selector de
348
+ // iconos: botón + panel flotante, cierra con Escape o click fuera) ---
347
349
  const linkPopoverOpen = ref(false)
348
350
  const linkPickerRef = ref<HTMLElement | null>(null)
349
351
  const linkPanelRef = ref<HTMLElement | null>(null)
352
+ useDropdownPanel(linkPickerRef, linkPanelRef, linkPopoverOpen, { matchWidth: false })
350
353
  const linkInputRef = ref<HTMLInputElement>()
351
354
  const linkUrlInput = ref('')
352
355
 
@@ -374,12 +377,8 @@ function removeLink() {
374
377
 
375
378
  function onClickOutside(e: MouseEvent) {
376
379
  const target = e.target as Node
377
- // Botón y franja son elementos hermanos: un click en cualquiera de los dos
378
- // mantiene el panel abierto (insertar icono / escribir la URL).
379
- if (!pickerRef.value?.contains(target) && !iconsPanelRef.value?.contains(target))
380
- pickerOpen.value = false
381
- if (!linkPickerRef.value?.contains(target) && !linkPanelRef.value?.contains(target))
382
- linkPopoverOpen.value = false
380
+ if (pickerRef.value && !pickerRef.value.contains(target)) pickerOpen.value = false
381
+ if (linkPickerRef.value && !linkPickerRef.value.contains(target)) linkPopoverOpen.value = false
383
382
  }
384
383
  onMounted(() => document.addEventListener('click', onClickOutside))
385
384
  onBeforeUnmount(() => {
@@ -448,18 +447,38 @@ function onSourceInput(value: string) {
448
447
 
449
448
  <span class="rich-text__sep" />
450
449
 
451
- <!-- Enlace: botón que abre la franja de la URL bajo la toolbar -->
452
- <button
453
- ref="linkPickerRef"
454
- type="button"
455
- class="rich-text__tool"
456
- :class="{ 'rich-text__tool--active': editor?.isActive('link') || linkPopoverOpen }"
457
- :title="t.link"
458
- :disabled="disabled"
459
- @click="toggleLinkPopover"
460
- >
461
- <LinkIcon :size="16" />
462
- </button>
450
+ <!-- Enlace: botón + mini-popover con la URL -->
451
+ <div ref="linkPickerRef" class="rich-text__picker">
452
+ <button
453
+ type="button"
454
+ class="rich-text__tool"
455
+ :class="{ 'rich-text__tool--active': editor?.isActive('link') || linkPopoverOpen }"
456
+ :title="t.link"
457
+ :disabled="disabled"
458
+ @click="toggleLinkPopover"
459
+ >
460
+ <LinkIcon :size="16" />
461
+ </button>
462
+ <div
463
+ v-if="linkPopoverOpen"
464
+ ref="linkPanelRef"
465
+ class="rich-text__link-popover"
466
+ popover="manual"
467
+ >
468
+ <input
469
+ ref="linkInputRef"
470
+ v-model="linkUrlInput"
471
+ type="text"
472
+ class="rich-text__link-input"
473
+ :placeholder="t.linkUrl"
474
+ @keydown.enter.prevent="applyLink"
475
+ @keydown.escape.prevent="linkPopoverOpen = false"
476
+ />
477
+ <button type="button" class="rich-text__tool" :title="t.linkApply" @click="applyLink">
478
+ <LinkIcon :size="14" />
479
+ </button>
480
+ </div>
481
+ </div>
463
482
  <button
464
483
  v-if="editor?.isActive('link')"
465
484
  type="button"
@@ -518,20 +537,33 @@ function onSourceInput(value: string) {
518
537
  <Redo2 :size="16" />
519
538
  </button>
520
539
 
521
- <!-- Selector de iconos del juego: abre la franja bajo la toolbar -->
540
+ <!-- Selector de iconos del juego -->
522
541
  <template v-if="icons.length">
523
542
  <span class="rich-text__sep" />
524
- <button
525
- ref="pickerRef"
526
- type="button"
527
- class="rich-text__tool"
528
- :class="{ 'rich-text__tool--active': pickerOpen }"
529
- :title="t.insertIcon"
530
- :disabled="disabled"
531
- @click="pickerOpen = !pickerOpen"
532
- >
533
- <Smile :size="16" />
534
- </button>
543
+ <div ref="pickerRef" class="rich-text__picker">
544
+ <button
545
+ type="button"
546
+ class="rich-text__tool"
547
+ :class="{ 'rich-text__tool--active': pickerOpen }"
548
+ :title="t.insertIcon"
549
+ :disabled="disabled"
550
+ @click="pickerOpen = !pickerOpen"
551
+ >
552
+ <Smile :size="16" />
553
+ </button>
554
+ <div v-if="pickerOpen" ref="iconsPanelRef" class="rich-text__icons" popover="manual">
555
+ <button
556
+ v-for="icon in icons"
557
+ :key="icon.url"
558
+ type="button"
559
+ class="rich-text__icon"
560
+ :title="icon.name"
561
+ @click="insertIcon(icon)"
562
+ >
563
+ <img :src="icon.url" :alt="icon.name" />
564
+ </button>
565
+ </div>
566
+ </div>
535
567
  </template>
536
568
  </template>
537
569
 
@@ -548,38 +580,6 @@ function onSourceInput(value: string) {
548
580
  </button>
549
581
  </div>
550
582
 
551
- <!-- Franja del enlace: EN FLUJO bajo la toolbar (dentro de un modal queda
552
- siempre a la vista; los popovers flotantes se recortaban o quedaban
553
- descolgados del editor) -->
554
- <div v-if="linkPopoverOpen && !source" ref="linkPanelRef" class="rich-text__link-bar">
555
- <input
556
- ref="linkInputRef"
557
- v-model="linkUrlInput"
558
- type="text"
559
- class="rich-text__link-input"
560
- :placeholder="t.linkUrl"
561
- @keydown.enter.prevent="applyLink"
562
- @keydown.escape.prevent="linkPopoverOpen = false"
563
- />
564
- <button type="button" class="rich-text__tool" :title="t.linkApply" @click="applyLink">
565
- <LinkIcon :size="14" />
566
- </button>
567
- </div>
568
-
569
- <!-- Franja de iconos: misma idea, a todo el ancho del editor -->
570
- <div v-if="pickerOpen && !source" ref="iconsPanelRef" class="rich-text__icons">
571
- <button
572
- v-for="icon in icons"
573
- :key="icon.url"
574
- type="button"
575
- class="rich-text__icon"
576
- :title="icon.name"
577
- @click="insertIcon(icon)"
578
- >
579
- <img :src="icon.url" :alt="icon.name" />
580
- </button>
581
- </div>
582
-
583
583
  <textarea
584
584
  v-if="source"
585
585
  class="rich-text__source"
@@ -20,7 +20,14 @@ export function useDropdownPanel(
20
20
  anchor: Ref<HTMLElement | null>,
21
21
  panel: Ref<HTMLElement | null>,
22
22
  open: Ref<boolean>,
23
+ options: {
24
+ /** Igualar el ancho del panel al del trigger (los select). Los popovers
25
+ * con ancho propio (selector de iconos, popover del enlace) lo apagan:
26
+ * conservan su ancho y solo se CLAMPEA su left para no salirse. */
27
+ matchWidth?: boolean
28
+ } = {},
23
29
  ) {
30
+ const { matchWidth = true } = options
24
31
  const GAP = 4 // $space-1: aire entre trigger y panel, como el CSS absolute
25
32
 
26
33
  /** Ancla el panel bajo el trigger (o encima, si abajo no cabe y arriba sí). */
@@ -29,8 +36,11 @@ export function useDropdownPanel(
29
36
  const p = panel.value
30
37
  if (!a || !p) return
31
38
  const r = a.getBoundingClientRect()
32
- p.style.width = `${r.width}px`
33
- p.style.left = `${r.left}px`
39
+ if (matchWidth) p.style.width = `${r.width}px`
40
+ const left = matchWidth
41
+ ? r.left
42
+ : Math.max(GAP, Math.min(r.left, window.innerWidth - p.offsetWidth - GAP))
43
+ p.style.left = `${left}px`
34
44
  const height = p.offsetHeight
35
45
  const below = window.innerHeight - r.bottom - GAP
36
46
  const openUp = height > below && r.top > window.innerHeight - r.bottom