@edc-motor/admin-kit 0.4.37 → 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.37",
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.37",
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 + 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'
@@ -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: el primer texto traducible con valor. */
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
- 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 ''
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
- <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) -->
779
- <h3 class="manager-detail__title">{{ typeName(selected.type) }}</h3>
780
-
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>
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
- <hr v-if="selectedFields.length" class="manager-panel__divider" />
791
- <div v-if="selectedFields.length" class="manager-detail">
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
- </div>
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'