@edc-motor/admin-kit 0.4.36 → 0.4.38
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.38",
|
|
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.38",
|
|
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 {
|
|
@@ -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'
|
|
@@ -55,6 +56,8 @@ export interface PageBlocksLabels {
|
|
|
55
56
|
/** Título de la sección de interruptores (PDF/Índice) del panel, debajo
|
|
56
57
|
* de las acciones de verdad (editar, borrar…). */
|
|
57
58
|
stateKicker: string
|
|
59
|
+
/** Título de la sección de detalles del panel (el tipo del bloque). */
|
|
60
|
+
details: string
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
const defaultLabels: PageBlocksLabels = {
|
|
@@ -79,6 +82,7 @@ const defaultLabels: PageBlocksLabels = {
|
|
|
79
82
|
parent: 'Bloque padre (índices indentados)',
|
|
80
83
|
parentNone: '— Ninguno —',
|
|
81
84
|
stateKicker: 'Estado',
|
|
85
|
+
details: 'Detalles',
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
const props = withDefaults(
|
|
@@ -244,18 +248,12 @@ function displayText(map: Record<string, string> | null | undefined): string {
|
|
|
244
248
|
return map[props.displayLocale] || Object.values(map).find(Boolean) || ''
|
|
245
249
|
}
|
|
246
250
|
|
|
247
|
-
/** Resumen del bloque en la lista:
|
|
251
|
+
/** Resumen del bloque en la lista: preview depurado (`blockPreview`) — la
|
|
252
|
+
* primera frase del primer campo con contenido (título > subtítulo >
|
|
253
|
+
* contenido), completa en el index; card y paneles la truncan por CSS. */
|
|
248
254
|
function summary(block: BlockRow): string {
|
|
249
255
|
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 ''
|
|
256
|
+
return blockPreview(block.settings, type?.fields ?? [], displayText)
|
|
259
257
|
}
|
|
260
258
|
|
|
261
259
|
/** URL de un campo imagen (traducible o no): la del locale actual. */
|
|
@@ -772,23 +770,18 @@ defineExpose({ reload: load })
|
|
|
772
770
|
</BaseButton>
|
|
773
771
|
</div>
|
|
774
772
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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>
|
|
773
|
+
<!-- Detalles con el lenguaje de secciones del panel (divisoria +
|
|
774
|
+
kicker): el título es el TIPO del bloque. El estado de los
|
|
775
|
+
interruptores ya lo cuentan los botones de arriba. -->
|
|
776
|
+
<section class="manager-panel__section">
|
|
777
|
+
<hr class="manager-panel__divider" />
|
|
778
|
+
<p class="manager-panel__kicker">{{ L.details }}</p>
|
|
779
|
+
<h3 class="manager-detail__title">{{ typeName(selected.type) }}</h3>
|
|
780
|
+
</section>
|
|
788
781
|
|
|
789
782
|
<!-- Contenido: cada campo del bloque con su valor (truncado) -->
|
|
790
|
-
<
|
|
791
|
-
|
|
783
|
+
<section v-if="selectedFields.length" class="manager-panel__section">
|
|
784
|
+
<hr class="manager-panel__divider" />
|
|
792
785
|
<p class="manager-panel__kicker">{{ L.panelContent }}</p>
|
|
793
786
|
<dl class="manager-detail__fields">
|
|
794
787
|
<div
|
|
@@ -807,7 +800,7 @@ defineExpose({ reload: load })
|
|
|
807
800
|
<dd v-else>{{ entry.value }}</dd>
|
|
808
801
|
</div>
|
|
809
802
|
</dl>
|
|
810
|
-
</
|
|
803
|
+
</section>
|
|
811
804
|
</template>
|
|
812
805
|
</div>
|
|
813
806
|
</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'
|