@edc-motor/ui 0.4.29 → 0.4.31

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.29",
3
+ "version": "0.4.31",
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,14 +39,17 @@
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í), 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.
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.
45
48
  // width 100%: en grids con justify-items start (el cuerpo del CTA) el
46
49
  // elemento encoge a su contenido y el text-align no se vería.
47
- &__title { margin: 0; font-size: $fs-32; width: 100%; }
50
+ &__title { margin: 0; font-size: $fs-28; line-height: 1.1; width: 100%; }
48
51
  // pre-line: el subtítulo es un textarea — sus saltos de línea cuentan.
49
- &__subtitle { margin: 0; color: $text-2; font-size: $fs-24; white-space: pre-line; width: 100%; }
52
+ &__subtitle { margin: 0; color: $text-2; font-size: $fs-16; white-space: pre-line; width: 100%; }
50
53
  &__text { color: $text-1; font-size: $fs-16; }
51
54
 
52
55
  // Alineación del bloque (campo común `align`): por CLASE (BlockShell ya
@@ -118,9 +121,6 @@
118
121
  .edc-button { justify-self: start; text-decoration: none; }
119
122
  }
120
123
 
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,7 +17,12 @@
17
17
  &__zone {
18
18
  position: relative;
19
19
  display: flex;
20
- align-self: flex-start;
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;
21
26
  align-items: center;
22
27
  justify-content: center;
23
28
  width: 160px;
@@ -81,8 +86,10 @@
81
86
  &__icon { width: 32px; height: 32px; color: $text-3; }
82
87
  &__text { font-size: $fs-12; color: $text-2; }
83
88
 
84
- // Nombre del fichero (pendiente o guardado) bajo la miniatura.
89
+ // Nombre del fichero (pendiente o guardado) bajo la miniatura, centrado
90
+ // con ella.
85
91
  &__name {
92
+ align-self: center;
86
93
  max-width: 160px;
87
94
  overflow: hidden;
88
95
  font-size: $fs-12;
@@ -34,13 +34,14 @@
34
34
  > :first-child { margin-block-start: 0; }
35
35
  > :last-child { margin-block-end: 0; }
36
36
 
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; }
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; }
44
45
 
45
46
  ul,
46
47
  ol {
@@ -69,34 +69,21 @@
69
69
  outline: none;
70
70
  }
71
71
 
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;
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 {
83
76
  display: flex;
84
77
  align-items: center;
85
78
  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;
91
79
  padding: $space-1;
92
- background: $surface;
93
- border: 1px solid $border;
94
- border-radius: $radius-md;
95
- box-shadow: $shadow-md;
80
+ border-bottom: 1px solid $border;
81
+ background: $surface-2;
96
82
  }
97
83
 
98
84
  &__link-input {
99
- width: 220px;
85
+ flex: 1;
86
+ min-width: 0;
100
87
  height: 30px;
101
88
  padding: 0 $space-2;
102
89
  border: 1px solid $input-border;
@@ -113,21 +100,16 @@
113
100
  }
114
101
 
115
102
  &__icons {
116
- position: absolute;
117
- top: calc(100% + 4px);
118
- left: 0;
119
- z-index: 100;
120
103
  display: grid;
121
- grid-template-columns: repeat(5, 1fr);
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));
122
107
  gap: 2px;
123
- max-width: 220px;
124
108
  max-height: 200px;
125
109
  overflow-y: auto;
126
110
  padding: $space-1;
127
- background: $surface;
128
- border: 1px solid $border;
129
- border-radius: $radius-md;
130
- box-shadow: $shadow-md;
111
+ border-bottom: 1px solid $border;
112
+ background: $surface-2;
131
113
  }
132
114
 
133
115
  &__icon {
@@ -3,7 +3,6 @@ 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'
7
6
  import UnderlineExtension from '@tiptap/extension-underline'
8
7
  import LinkExtension from '@tiptap/extension-link'
9
8
  import TableExtension from '@tiptap/extension-table'
@@ -331,25 +330,23 @@ function insertTable() {
331
330
  }
332
331
 
333
332
  // --- 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).
334
336
  const pickerOpen = ref(false)
335
337
  const pickerRef = ref<HTMLElement | null>(null)
336
338
  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 })
341
339
 
342
340
  function insertIcon(icon: RichIcon) {
343
341
  editor.value?.chain().focus().setImage({ src: icon.url, alt: icon.name, title: icon.name }).run()
344
342
  pickerOpen.value = false
345
343
  }
346
344
 
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) ---
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) ---
349
347
  const linkPopoverOpen = ref(false)
350
348
  const linkPickerRef = ref<HTMLElement | null>(null)
351
349
  const linkPanelRef = ref<HTMLElement | null>(null)
352
- useDropdownPanel(linkPickerRef, linkPanelRef, linkPopoverOpen, { matchWidth: false })
353
350
  const linkInputRef = ref<HTMLInputElement>()
354
351
  const linkUrlInput = ref('')
355
352
 
@@ -377,8 +374,12 @@ function removeLink() {
377
374
 
378
375
  function onClickOutside(e: MouseEvent) {
379
376
  const target = e.target as Node
380
- if (pickerRef.value && !pickerRef.value.contains(target)) pickerOpen.value = false
381
- if (linkPickerRef.value && !linkPickerRef.value.contains(target)) linkPopoverOpen.value = false
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
382
383
  }
383
384
  onMounted(() => document.addEventListener('click', onClickOutside))
384
385
  onBeforeUnmount(() => {
@@ -447,38 +448,18 @@ function onSourceInput(value: string) {
447
448
 
448
449
  <span class="rich-text__sep" />
449
450
 
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>
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>
482
463
  <button
483
464
  v-if="editor?.isActive('link')"
484
465
  type="button"
@@ -537,33 +518,20 @@ function onSourceInput(value: string) {
537
518
  <Redo2 :size="16" />
538
519
  </button>
539
520
 
540
- <!-- Selector de iconos del juego -->
521
+ <!-- Selector de iconos del juego: abre la franja bajo la toolbar -->
541
522
  <template v-if="icons.length">
542
523
  <span class="rich-text__sep" />
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>
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>
567
535
  </template>
568
536
  </template>
569
537
 
@@ -580,6 +548,38 @@ function onSourceInput(value: string) {
580
548
  </button>
581
549
  </div>
582
550
 
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,14 +20,7 @@ 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
- } = {},
29
23
  ) {
30
- const { matchWidth = true } = options
31
24
  const GAP = 4 // $space-1: aire entre trigger y panel, como el CSS absolute
32
25
 
33
26
  /** Ancla el panel bajo el trigger (o encima, si abajo no cabe y arriba sí). */
@@ -36,11 +29,8 @@ export function useDropdownPanel(
36
29
  const p = panel.value
37
30
  if (!a || !p) return
38
31
  const r = a.getBoundingClientRect()
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`
32
+ p.style.width = `${r.width}px`
33
+ p.style.left = `${r.left}px`
44
34
  const height = p.offsetHeight
45
35
  const below = window.innerHeight - r.bottom - GAP
46
36
  const openUp = height > below && r.top > window.innerHeight - r.bottom