@edc-motor/admin-kit 0.4.37 → 0.4.39
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/admin-kit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.39",
|
|
4
4
|
"description": "EdC Motor — kit del panel de administración: layout, gestores CRUD, imágenes, PDF, páginas, configuración, usuarios y copias (paquete fuente: lo compila el consumidor con Vite)",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@edc-motor/ui": "^0.4.
|
|
36
|
+
"@edc-motor/ui": "^0.4.39",
|
|
37
37
|
"axios": "^1.7.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -187,6 +187,14 @@
|
|
|
187
187
|
width: 100%;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
// Sección del panel (divisoria + kicker + contenido) con el aire justo
|
|
191
|
+
// entre el kicker y lo suyo: reutilizable por cualquier panel derecho
|
|
192
|
+
// (la usa el panel de bloque de PageBlocks).
|
|
193
|
+
&__section {
|
|
194
|
+
display: grid;
|
|
195
|
+
gap: $space-2;
|
|
196
|
+
}
|
|
197
|
+
|
|
190
198
|
// Botón-volver del panel (texto con flecha): del detalle de la card
|
|
191
199
|
// seleccionada al estado sin selección (los filtros del listado).
|
|
192
200
|
&__back {
|
|
@@ -360,21 +368,28 @@
|
|
|
360
368
|
|
|
361
369
|
// Campos del bloque seleccionado: etiqueta + valor; los valores largos se
|
|
362
370
|
// recortan a unas pocas líneas.
|
|
371
|
+
// Campo del panel de bloque: «Label: valor» EN LÍNEA; las imágenes van
|
|
372
|
+
// apiladas (is-stacked), con la miniatura debajo del label.
|
|
363
373
|
&__field {
|
|
364
|
-
display:
|
|
365
|
-
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: baseline;
|
|
376
|
+
gap: $space-1;
|
|
366
377
|
min-width: 0;
|
|
367
378
|
font-size: $fs-13;
|
|
368
379
|
|
|
369
380
|
dt {
|
|
381
|
+
flex: none;
|
|
370
382
|
color: $text-3;
|
|
371
383
|
font-size: $fs-12;
|
|
372
384
|
text-transform: uppercase;
|
|
373
385
|
letter-spacing: 0.04em;
|
|
386
|
+
|
|
387
|
+
&::after { content: ':'; }
|
|
374
388
|
}
|
|
375
389
|
|
|
376
390
|
dd {
|
|
377
391
|
margin: 0;
|
|
392
|
+
min-width: 0;
|
|
378
393
|
color: $text-2;
|
|
379
394
|
overflow-wrap: anywhere;
|
|
380
395
|
display: -webkit-box;
|
|
@@ -383,6 +398,13 @@
|
|
|
383
398
|
line-clamp: 4;
|
|
384
399
|
overflow: hidden;
|
|
385
400
|
}
|
|
401
|
+
|
|
402
|
+
&.is-stacked {
|
|
403
|
+
display: grid;
|
|
404
|
+
gap: 2px;
|
|
405
|
+
|
|
406
|
+
dt::after { content: none; }
|
|
407
|
+
}
|
|
386
408
|
}
|
|
387
409
|
|
|
388
410
|
&__fields {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@use "tokens" as *;
|
|
2
2
|
|
|
3
3
|
// Configurador del menú público (doc 10 ampliado, rediseño sin grupos):
|
|
4
|
-
// filas tipo PageBlocks (asa +
|
|
4
|
+
// filas tipo PageBlocks (asa + etiqueta + badges + acciones),
|
|
5
5
|
// arrastrables (drag & drop nativo) además de las flechas. Nada se persiste
|
|
6
6
|
// hasta "Guardar": la barra superior trae el aviso de cambios sin guardar y
|
|
7
7
|
// Descartar/Guardar.
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
gap: $space-2;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// SIN wrap: la fila entera cabe siempre en UNA línea — la etiqueta es lo
|
|
34
|
+
// único que encoge (min-width: 0 + ellipsis); en muy estrecho, además,
|
|
35
|
+
// baja de cuerpo (container query más abajo).
|
|
33
36
|
&__item {
|
|
34
37
|
display: flex;
|
|
35
38
|
align-items: center;
|
|
36
|
-
flex-wrap: wrap;
|
|
37
39
|
gap: $space-2;
|
|
38
40
|
border: 1px solid $border;
|
|
39
41
|
border-radius: $radius-md;
|
|
@@ -88,12 +90,6 @@
|
|
|
88
90
|
&:active { cursor: grabbing; }
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
&__icon {
|
|
92
|
-
display: inline-flex;
|
|
93
|
-
color: $text-3;
|
|
94
|
-
flex: 0 0 auto;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
93
|
&__label {
|
|
98
94
|
font-weight: $k-fw-semibold;
|
|
99
95
|
min-width: 0;
|
|
@@ -103,6 +99,12 @@
|
|
|
103
99
|
margin-right: auto;
|
|
104
100
|
}
|
|
105
101
|
|
|
102
|
+
// En muy estrecho el título baja de cuerpo (y la elipsis remata si aún
|
|
103
|
+
// no cabe): la fila sigue siendo UNA línea.
|
|
104
|
+
@container content (max-width: #{$bp-sm - 1px}) {
|
|
105
|
+
&__label { font-size: $fs-13; }
|
|
106
|
+
}
|
|
107
|
+
|
|
106
108
|
&__badges { display: flex; gap: $space-1; }
|
|
107
109
|
|
|
108
110
|
&__moves {
|
|
@@ -136,6 +138,7 @@
|
|
|
136
138
|
color: $success;
|
|
137
139
|
cursor: pointer;
|
|
138
140
|
padding: $space-1;
|
|
141
|
+
flex: 0 0 auto;
|
|
139
142
|
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
140
143
|
|
|
141
144
|
&.is-on {
|
|
@@ -143,5 +146,13 @@
|
|
|
143
146
|
border-color: $success;
|
|
144
147
|
@include contrast-text($success);
|
|
145
148
|
}
|
|
149
|
+
|
|
150
|
+
// Entrada OCULTA: el botón en ámbar ($warning) — sin chip, el estado
|
|
151
|
+
// lo cuenta el propio color del botón.
|
|
152
|
+
&.is-off {
|
|
153
|
+
background: $warning;
|
|
154
|
+
border-color: $warning;
|
|
155
|
+
@include contrast-text($warning);
|
|
156
|
+
}
|
|
146
157
|
}
|
|
147
158
|
}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
3
3
|
import type { AxiosInstance } from 'axios'
|
|
4
|
-
import {
|
|
5
|
-
ArrowDown,
|
|
6
|
-
ArrowUp,
|
|
7
|
-
Compass,
|
|
8
|
-
Eye,
|
|
9
|
-
EyeOff,
|
|
10
|
-
File,
|
|
11
|
-
GripVertical,
|
|
12
|
-
RotateCcw,
|
|
13
|
-
Save,
|
|
14
|
-
} from '@lucide/vue'
|
|
4
|
+
import { ArrowDown, ArrowUp, Eye, EyeOff, GripVertical, RotateCcw, Save } from '@lucide/vue'
|
|
15
5
|
import { BaseButton, useToast } from '@edc-motor/ui'
|
|
16
6
|
|
|
17
7
|
// Configurador del menú de la web pública (doc 10 ampliado, rediseño sin
|
|
@@ -27,7 +17,6 @@ import { BaseButton, useToast } from '@edc-motor/ui'
|
|
|
27
17
|
|
|
28
18
|
export interface MenuManagerLabels {
|
|
29
19
|
empty: string
|
|
30
|
-
hidden: string
|
|
31
20
|
draft: string
|
|
32
21
|
moveUp: string
|
|
33
22
|
moveDown: string
|
|
@@ -41,7 +30,6 @@ export interface MenuManagerLabels {
|
|
|
41
30
|
|
|
42
31
|
const defaultLabels: MenuManagerLabels = {
|
|
43
32
|
empty: 'El menú aún no tiene elementos.',
|
|
44
|
-
hidden: 'Oculto',
|
|
45
33
|
draft: 'Borrador',
|
|
46
34
|
moveUp: 'Subir',
|
|
47
35
|
moveDown: 'Bajar',
|
|
@@ -367,15 +355,11 @@ defineExpose({ isDirty })
|
|
|
367
355
|
>
|
|
368
356
|
<span class="menu-manager__grip"><GripVertical :size="15" /></span>
|
|
369
357
|
|
|
370
|
-
<span class="menu-manager__icon">
|
|
371
|
-
<File v-if="row.node.type === 'page'" :size="16" />
|
|
372
|
-
<Compass v-else :size="16" />
|
|
373
|
-
</span>
|
|
374
|
-
|
|
375
358
|
<span class="menu-manager__label">{{ labelOf(row.node) }}</span>
|
|
376
359
|
|
|
360
|
+
<!-- Sin chip «Oculto»: el estado lo cuenta el color del botón de
|
|
361
|
+
visibilidad (ámbar = oculta) -->
|
|
377
362
|
<span class="menu-manager__badges">
|
|
378
|
-
<span v-if="!row.node.is_visible" class="chip is-missing">{{ L.hidden }}</span>
|
|
379
363
|
<span
|
|
380
364
|
v-if="row.node.type === 'page' && row.node.page && !row.node.page.is_published"
|
|
381
365
|
class="chip is-missing"
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type RichTextLabels,
|
|
14
14
|
} from '@edc-motor/ui'
|
|
15
15
|
import SchemaFields, { type FieldSchema } from './SchemaFields.vue'
|
|
16
|
+
import { blockPreview } from './blockPreview'
|
|
16
17
|
import { collectImageUrls, deleteContentImage, uploadPendingImages } from './deferredImages'
|
|
17
18
|
import { useRightSidebar } from '../composables/useRightSidebar'
|
|
18
19
|
import { useCardDeselect } from '../composables/useCardDeselect'
|
|
@@ -244,18 +245,12 @@ function displayText(map: Record<string, string> | null | undefined): string {
|
|
|
244
245
|
return map[props.displayLocale] || Object.values(map).find(Boolean) || ''
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
/** Resumen del bloque en la lista:
|
|
248
|
+
/** Resumen del bloque en la lista: preview depurado (`blockPreview`) — la
|
|
249
|
+
* primera frase del primer campo con contenido (título > subtítulo >
|
|
250
|
+
* contenido), completa en el index; card y paneles la truncan por CSS. */
|
|
248
251
|
function summary(block: BlockRow): string {
|
|
249
252
|
const type = types.value.find((t) => t.key === block.type)
|
|
250
|
-
|
|
251
|
-
if (!['text', 'textarea', 'richtext'].includes(field.type)) continue
|
|
252
|
-
const value = block.settings?.[field.key]
|
|
253
|
-
if (field.translatable && value && typeof value === 'object') {
|
|
254
|
-
const text = displayText(value as Record<string, string>)
|
|
255
|
-
if (text) return text.replace(/<[^>]*>/g, '').slice(0, 80)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return ''
|
|
253
|
+
return blockPreview(block.settings, type?.fields ?? [], displayText)
|
|
259
254
|
}
|
|
260
255
|
|
|
261
256
|
/** URL de un campo imagen (traducible o no): la del locale actual. */
|
|
@@ -772,29 +767,20 @@ defineExpose({ reload: load })
|
|
|
772
767
|
</BaseButton>
|
|
773
768
|
</div>
|
|
774
769
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
<!-- El título del panel es el TIPO del bloque (el contenido va en
|
|
778
|
-
su sección, abajo) -->
|
|
770
|
+
<!-- El TIPO del bloque como título del panel (patrón de los
|
|
771
|
+
paneles de entidad), debajo del estado y fuera de secciones -->
|
|
779
772
|
<h3 class="manager-detail__title">{{ typeName(selected.type) }}</h3>
|
|
780
773
|
|
|
781
|
-
<!-- Estado de los interruptores, en texto -->
|
|
782
|
-
<p class="manager-detail__meta">
|
|
783
|
-
<strong>{{ L.printable }}</strong> {{ selected.is_printable ? L.yes : L.no }}
|
|
784
|
-
</p>
|
|
785
|
-
<p class="manager-detail__meta">
|
|
786
|
-
<strong>{{ L.indexable }}</strong> {{ selected.is_indexable ? L.yes : L.no }}
|
|
787
|
-
</p>
|
|
788
|
-
|
|
789
774
|
<!-- Contenido: cada campo del bloque con su valor (truncado) -->
|
|
790
|
-
<
|
|
791
|
-
|
|
775
|
+
<section v-if="selectedFields.length" class="manager-panel__section">
|
|
776
|
+
<hr class="manager-panel__divider" />
|
|
792
777
|
<p class="manager-panel__kicker">{{ L.panelContent }}</p>
|
|
793
778
|
<dl class="manager-detail__fields">
|
|
794
779
|
<div
|
|
795
780
|
v-for="entry in selectedFields"
|
|
796
781
|
:key="entry.field.key"
|
|
797
782
|
class="manager-detail__field"
|
|
783
|
+
:class="{ 'is-stacked': entry.field.type === 'image' }"
|
|
798
784
|
>
|
|
799
785
|
<dt>{{ fieldLabel(entry.field) }}</dt>
|
|
800
786
|
<dd v-if="entry.field.type === 'image'">
|
|
@@ -807,7 +793,7 @@ defineExpose({ reload: load })
|
|
|
807
793
|
<dd v-else>{{ entry.value }}</dd>
|
|
808
794
|
</div>
|
|
809
795
|
</dl>
|
|
810
|
-
</
|
|
796
|
+
</section>
|
|
811
797
|
</template>
|
|
812
798
|
</div>
|
|
813
799
|
</Teleport>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Texto de preview de un bloque, DEPURADO y transversal (index de bloques,
|
|
2
|
+
// card de bloque y paneles derechos): del PRIMER campo con contenido en el
|
|
3
|
+
// orden título > subtítulo > contenido (el primer campo de texto restante
|
|
4
|
+
// del esquema), SOLO la primera frase, sin HTML. Helpers puros: quien pinta
|
|
5
|
+
// decide si además trunca por CSS (una línea con ellipsis en card/panel).
|
|
6
|
+
|
|
7
|
+
/** Campo mínimo del esquema que el preview necesita conocer. */
|
|
8
|
+
export interface BlockPreviewField {
|
|
9
|
+
key: string
|
|
10
|
+
type: string
|
|
11
|
+
translatable: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Tipos de campo con texto legible (mismo criterio que SchemaFields).
|
|
15
|
+
const TEXT_TYPES = ['text', 'textarea', 'richtext']
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Primera frase de un texto: sin HTML, cortada en el primer signo de
|
|
19
|
+
* puntuación de cierre (. ! ? … : ;) — que se conserva — o salto de línea.
|
|
20
|
+
*/
|
|
21
|
+
export function firstSentence(text: string): string {
|
|
22
|
+
const plain = text.replace(/<[^>]*>/g, ' ')
|
|
23
|
+
const stop = /[.!?…:;\n]/.exec(plain)
|
|
24
|
+
const cut = stop ? plain.slice(0, stop.index + (stop[0] === '\n' ? 0 : 1)) : plain
|
|
25
|
+
return cut.replace(/\s+/g, ' ').trim()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Preview de un bloque: la primera frase (`firstSentence`) del primer campo
|
|
30
|
+
* de texto con contenido, priorizando `title`, luego `subtitle` y después el
|
|
31
|
+
* resto de campos de texto en el orden del esquema (el "contenido").
|
|
32
|
+
* `displayText` resuelve los traducibles (locale actual con fallback).
|
|
33
|
+
*/
|
|
34
|
+
export function blockPreview(
|
|
35
|
+
settings: Record<string, unknown> | null | undefined,
|
|
36
|
+
fields: BlockPreviewField[],
|
|
37
|
+
displayText: (map: Record<string, string> | null | undefined) => string,
|
|
38
|
+
): string {
|
|
39
|
+
const textual = fields.filter((field) => TEXT_TYPES.includes(field.type))
|
|
40
|
+
const ordered = [
|
|
41
|
+
...textual.filter((field) => field.key === 'title'),
|
|
42
|
+
...textual.filter((field) => field.key === 'subtitle'),
|
|
43
|
+
...textual.filter((field) => field.key !== 'title' && field.key !== 'subtitle'),
|
|
44
|
+
]
|
|
45
|
+
for (const field of ordered) {
|
|
46
|
+
const raw = settings?.[field.key]
|
|
47
|
+
const text =
|
|
48
|
+
field.translatable && raw && typeof raw === 'object'
|
|
49
|
+
? displayText(raw as Record<string, string>)
|
|
50
|
+
: typeof raw === 'string'
|
|
51
|
+
? raw
|
|
52
|
+
: ''
|
|
53
|
+
const sentence = firstSentence(text)
|
|
54
|
+
if (sentence) return sentence
|
|
55
|
+
}
|
|
56
|
+
return ''
|
|
57
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,10 @@ export { useResource, type ResourceMeta } from './crud/useResource'
|
|
|
24
24
|
// CRM de páginas y bloques (doc 03): editor dirigido por esquema.
|
|
25
25
|
export { default as SchemaFields, type FieldSchema } from './content/SchemaFields.vue'
|
|
26
26
|
export { default as PageBlocks, type PageBlocksLabels } from './content/PageBlocks.vue'
|
|
27
|
+
// Preview depurado de un bloque (primera frase del primer campo con
|
|
28
|
+
// contenido, título > subtítulo > contenido): lo usan PageBlocks y los
|
|
29
|
+
// paneles de los cascarones.
|
|
30
|
+
export { blockPreview, firstSentence, type BlockPreviewField } from './content/blockPreview'
|
|
27
31
|
// Configurador del menú público (doc 10 ampliado): páginas del CRM + rutas
|
|
28
32
|
// del juego, reordenables y agrupables. Agnóstico de i18n (DC-29).
|
|
29
33
|
export { default as MenuManager, type MenuManagerLabels } from './content/MenuManager.vue'
|