@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 +1 -1
- package/scss/components/_blocks.scss +8 -8
- package/scss/components/_image-upload.scss +2 -9
- package/scss/components/_rich-content.scss +7 -8
- package/scss/components/_rich-text.scss +31 -13
- package/src/components/RichTextInput.vue +67 -67
- package/src/composables/useDropdownPanel.ts +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edc-motor/ui",
|
|
3
|
-
"version": "0.
|
|
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í)
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
73
|
-
// el
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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:
|
|
346
|
-
// botón +
|
|
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
|
-
|
|
378
|
-
|
|
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
|
|
452
|
-
<
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
540
|
+
<!-- Selector de iconos del juego -->
|
|
522
541
|
<template v-if="icons.length">
|
|
523
542
|
<span class="rich-text__sep" />
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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
|