@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.37",
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.37",
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: grid;
365
- gap: 2px;
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 + icono + etiqueta + badges + acciones),
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: el primer texto traducible con valor. */
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
- for (const field of type?.fields ?? []) {
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
- <hr class="manager-panel__divider" />
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
- <hr v-if="selectedFields.length" class="manager-panel__divider" />
791
- <div v-if="selectedFields.length" class="manager-detail">
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
- </div>
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'