@edc-motor/ui 0.4.7 → 0.4.8

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/ui",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
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",
package/scss/_forms.scss CHANGED
@@ -18,7 +18,8 @@ select,
18
18
  textarea {
19
19
  font: inherit;
20
20
  width: 100%;
21
- padding: 10px 12px;
21
+ height: $input-height; // controles de una línea a 36px (como .form-field)
22
+ padding: 0 $input-padding-x;
22
23
  background: $surface;
23
24
  border: 1px solid $border;
24
25
  border-radius: $radius-md;
@@ -26,7 +27,12 @@ textarea {
26
27
  &:hover { border-color: $border-strong; }
27
28
  &:focus { outline: none; border-color: $accent-500; }
28
29
  }
29
- textarea { resize: vertical; }
30
+ textarea {
31
+ height: auto;
32
+ min-height: $input-height;
33
+ padding: $input-padding-y $input-padding-x;
34
+ resize: vertical;
35
+ }
30
36
  input[type="color"] { width: 44px; height: 36px; padding: 2px; cursor: pointer; }
31
37
 
32
38
  .error { color: $danger; margin: 0; }
package/scss/_tokens.scss CHANGED
@@ -101,9 +101,9 @@ $shadow-lg: var(--shadow-lg);
101
101
  // Formularios (elementos estilo kontuan)
102
102
  $focus-ring: 0 0 0 3px color-mix(in srgb, #{$accent-500} 25%, transparent);
103
103
  $disabled-opacity: 0.45;
104
- $input-height: 40px;
105
- $input-padding-x: 12px;
106
- $input-padding-y: 10px;
104
+ $input-height: 36px;
105
+ $input-padding-x: 10px;
106
+ $input-padding-y: 8px;
107
107
  $input-radius: $radius-md;
108
108
  $input-bg: $surface-2;
109
109
  $input-text: $text-1;
@@ -5,7 +5,8 @@
5
5
  align-items: center;
6
6
  justify-content: center;
7
7
  gap: $space-2;
8
- padding: $space-2 $space-4;
8
+ min-height: $input-height; // misma altura que los inputs del motor (36px)
9
+ padding: $space-1 $space-4;
9
10
  border: 1px solid transparent;
10
11
  border-radius: $radius-md;
11
12
  font: inherit;
@@ -74,6 +75,7 @@
74
75
  .edc-button--text {
75
76
  background: transparent;
76
77
  border-color: transparent;
78
+ min-height: 0;
77
79
  padding: 0;
78
80
  color: $accent-500;
79
81
 
@@ -82,6 +84,7 @@
82
84
  .edc-button--text-danger {
83
85
  background: transparent;
84
86
  border-color: transparent;
87
+ min-height: 0;
85
88
  padding: 0;
86
89
  color: $danger;
87
90
 
@@ -11,6 +11,8 @@
11
11
  display: inline-flex;
12
12
  align-items: center;
13
13
  justify-content: center;
14
+ // 32px a propósito: control secundario, un punto por debajo de los 36px
15
+ // de los controles de formulario.
14
16
  width: 32px;
15
17
  height: 32px;
16
18
  background: none;
@@ -0,0 +1,21 @@
1
+ @use "tokens" as *;
2
+
3
+ // Modal de filtros (FiltersModal.vue): el wrapper es contenedor y el grid
4
+ // del cuerpo suma columnas según el ancho REAL del modal (1 → 2 → 3).
5
+ .filters-modal {
6
+ container-type: inline-size;
7
+
8
+ &__grid {
9
+ display: grid;
10
+ grid-template-columns: 1fr;
11
+ gap: $space-4;
12
+
13
+ > * { min-width: 0; } // cada campo a ancho completo de su celda
14
+
15
+ @container (min-width: 460px) { grid-template-columns: repeat(2, 1fr); }
16
+ @container (min-width: 700px) { grid-template-columns: repeat(3, 1fr); }
17
+ }
18
+
19
+ // "Quitar filtros" a la izquierda del pie (el pie alinea a la derecha).
20
+ &__clear { margin-right: auto; }
21
+ }
@@ -54,7 +54,7 @@
54
54
  &::after {
55
55
  content: '';
56
56
  position: absolute;
57
- right: 12px;
57
+ right: $input-padding-x;
58
58
  top: 50%;
59
59
  transform: translateY(-50%);
60
60
  width: 0;
@@ -0,0 +1,103 @@
1
+ @use "tokens" as *;
2
+
3
+ // Barra de los index (IndexToolbar.vue). El propio toolbar es el contenedor
4
+ // de las queries: por debajo de $bp-sm de ancho REAL de la barra, la búsqueda
5
+ // pasa a su fila y debajo toggles + botón se reparten el ancho.
6
+ .index-toolbar {
7
+ container-type: inline-size;
8
+ margin-bottom: $space-4;
9
+
10
+ &__inner {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: $space-3;
14
+ }
15
+
16
+ &__search {
17
+ position: relative;
18
+ flex: 1;
19
+ min-width: 0;
20
+ }
21
+
22
+ &__search-icon {
23
+ position: absolute;
24
+ right: $input-padding-x;
25
+ top: 50%;
26
+ transform: translateY(-50%);
27
+ color: $text-3;
28
+ pointer-events: none;
29
+ }
30
+
31
+ &__search-input {
32
+ width: 100%;
33
+ height: $input-height;
34
+ padding: 0 34px 0 $input-padding-x; // hueco a la derecha para la lupa
35
+ background: $input-bg;
36
+ color: $input-text;
37
+ border: 1px solid $input-border;
38
+ border-radius: $input-radius;
39
+ font-family: $k-font-sans;
40
+ font-size: $fs-14;
41
+ transition: border-color 0.15s, box-shadow 0.15s;
42
+
43
+ &::placeholder { color: $input-placeholder; }
44
+ &:hover:not(:focus) { border-color: $input-border-hover; }
45
+ &:focus {
46
+ outline: none;
47
+ border-color: $input-border-focus;
48
+ box-shadow: $input-focus-ring;
49
+ }
50
+ }
51
+
52
+ &__actions {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: $space-3;
56
+ flex-shrink: 0;
57
+ }
58
+
59
+ &__filters {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: $space-2;
64
+ height: $input-height;
65
+ padding: 0 $space-3;
66
+ background: $input-bg;
67
+ border: 1px solid $input-border;
68
+ border-radius: $input-radius;
69
+ color: $text-2;
70
+ font-family: $k-font-sans;
71
+ font-size: $fs-14;
72
+ cursor: pointer;
73
+ transition: border-color 0.15s ease, color 0.15s ease;
74
+
75
+ &:hover { border-color: $input-border-hover; color: $text-1; }
76
+ }
77
+
78
+ &__badge {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ min-width: 18px;
83
+ height: 18px;
84
+ padding: 0 5px;
85
+ border-radius: $radius-pill;
86
+ background: $accent-500;
87
+ color: #fff;
88
+ font-size: $fs-11;
89
+ font-weight: $k-fw-semibold;
90
+ line-height: 1;
91
+ }
92
+
93
+ // Estrecho: la búsqueda ocupa su fila; debajo, toggles y botón se
94
+ // reparten el ancho proporcionalmente.
95
+ @container (max-width: #{$bp-sm - 1px}) {
96
+ &__inner { flex-wrap: wrap; }
97
+ &__search { flex: 1 1 100%; }
98
+ &__actions {
99
+ flex: 1 1 100%;
100
+ > * { flex: 1 1 auto; }
101
+ }
102
+ }
103
+ }
@@ -6,6 +6,9 @@
6
6
  @use "base-select";
7
7
  @use "base-pagination";
8
8
  @use "search-select";
9
+ @use "sort-toggles";
10
+ @use "index-toolbar";
11
+ @use "filters-modal";
9
12
  @use "blocks";
10
13
  @use "block-related";
11
14
  @use "preview-grid";
@@ -16,7 +16,8 @@
16
16
  border-radius: $input-radius;
17
17
  color: $input-text;
18
18
  cursor: pointer;
19
- padding: $space-2 $space-3;
19
+ height: $input-height;
20
+ padding: 0 $input-padding-x;
20
21
  text-align: left;
21
22
  font: inherit;
22
23
 
@@ -65,7 +66,8 @@
65
66
  border: 0;
66
67
  border-bottom: 1px solid $border;
67
68
  color: $input-text;
68
- padding: $space-2 $space-3;
69
+ height: $input-height;
70
+ padding: 0 $input-padding-x;
69
71
  font: inherit;
70
72
  outline: none;
71
73
 
@@ -0,0 +1,35 @@
1
+ @use "tokens" as *;
2
+
3
+ // Toggles de ordenación (SortToggles.vue): grupo segmentado a la altura de
4
+ // los inputs (36px); el activo se tiñe de acento y su icono marca el sentido.
5
+ .sort-toggles {
6
+ display: inline-flex;
7
+ gap: 2px;
8
+ height: $input-height;
9
+ padding: 2px;
10
+ background: $input-bg;
11
+ border: 1px solid $input-border;
12
+ border-radius: $input-radius;
13
+
14
+ &__btn {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ flex: 1 1 auto;
19
+ min-width: 40px;
20
+ padding: 0 $space-2;
21
+ border: none;
22
+ border-radius: $radius-sm;
23
+ background: transparent;
24
+ color: $text-2;
25
+ cursor: pointer;
26
+ transition: background-color 0.15s ease, color 0.15s ease;
27
+
28
+ &:hover { color: $text-1; }
29
+
30
+ &.is-active {
31
+ background: color-mix(in srgb, $accent-500 14%, transparent);
32
+ color: $accent-500;
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import { FunnelX, X } from '@lucide/vue'
3
+ import BaseModal from './BaseModal.vue'
4
+ import BaseButton from './BaseButton.vue'
5
+
6
+ // Modal de filtros de los index: BaseModal (overlay, Escape, click fuera,
7
+ // aria-modal) SIN semántica de guardar — los campos del slot aplican en vivo
8
+ // (v-model directo desde el consumidor). El cuerpo es un grid que suma
9
+ // columnas según el ancho del propio modal (container queries). El pie trae
10
+ // "Quitar filtros" (emit `clear`, solo con activeCount > 0) y "Cerrar".
11
+ // Agnóstico de i18n (DC-29): textos por prop, defaults en castellano.
12
+
13
+ withDefaults(
14
+ defineProps<{
15
+ modelValue: boolean
16
+ title?: string
17
+ size?: 'sm' | 'md' | 'lg'
18
+ activeCount?: number
19
+ clearLabel?: string
20
+ closeLabel?: string
21
+ }>(),
22
+ {
23
+ title: 'Filtros',
24
+ size: 'md',
25
+ activeCount: 0,
26
+ clearLabel: 'Quitar filtros',
27
+ closeLabel: 'Cerrar',
28
+ },
29
+ )
30
+
31
+ const emit = defineEmits<{ 'update:modelValue': [boolean]; clear: [] }>()
32
+ </script>
33
+
34
+ <template>
35
+ <BaseModal
36
+ :model-value="modelValue"
37
+ :title="title"
38
+ :size="size"
39
+ :close-label="closeLabel"
40
+ @update:model-value="(v) => emit('update:modelValue', v)"
41
+ >
42
+ <div class="filters-modal">
43
+ <div class="filters-modal__grid"><slot /></div>
44
+ </div>
45
+
46
+ <template #footer>
47
+ <BaseButton
48
+ v-if="activeCount > 0"
49
+ variant="secondary"
50
+ type="button"
51
+ class="filters-modal__clear"
52
+ @click="emit('clear')"
53
+ >
54
+ <template #icon><FunnelX :size="16" /></template>
55
+ {{ clearLabel }}
56
+ </BaseButton>
57
+ <BaseButton variant="primary" type="button" @click="emit('update:modelValue', false)">
58
+ <template #icon><X :size="16" /></template>
59
+ {{ closeLabel }}
60
+ </BaseButton>
61
+ </template>
62
+ </BaseModal>
63
+ </template>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import { Funnel, Search } from '@lucide/vue'
3
+ import SortToggles, { type SortValue } from './SortToggles.vue'
4
+
5
+ // Barra unificada de los index (admin y web pública): búsqueda (lupa a la
6
+ // derecha, como el FilterBar del admin-kit), toggles de ordenación y botón
7
+ // "Filtros" con badge de filtros activos (emite `open-filters`; el modal lo
8
+ // pone el consumidor). En ancho es una fila [búsqueda][toggles][botón]; en
9
+ // estrecho (container query propia) la búsqueda ocupa su fila y debajo se
10
+ // reparten el ancho los toggles y el botón.
11
+ // La búsqueda emite INMEDIATO (como FilterBar): el debounce va fuera.
12
+ // Agnóstico de i18n (DC-29): textos por prop, defaults en castellano.
13
+
14
+ withDefaults(
15
+ defineProps<{
16
+ modelValue: string
17
+ sort?: SortValue
18
+ searchPlaceholder?: string
19
+ filtersLabel?: string
20
+ activeCount?: number
21
+ showFilters?: boolean
22
+ showSort?: boolean
23
+ latestLabel?: string
24
+ oldestLabel?: string
25
+ nameLabel?: string
26
+ nameDescLabel?: string
27
+ }>(),
28
+ {
29
+ sort: 'latest',
30
+ searchPlaceholder: 'Buscar…',
31
+ filtersLabel: 'Filtros',
32
+ activeCount: 0,
33
+ showFilters: false,
34
+ showSort: true,
35
+ latestLabel: undefined,
36
+ oldestLabel: undefined,
37
+ nameLabel: undefined,
38
+ nameDescLabel: undefined,
39
+ },
40
+ )
41
+
42
+ const emit = defineEmits<{
43
+ 'update:modelValue': [value: string]
44
+ 'update:sort': [value: SortValue]
45
+ 'open-filters': []
46
+ }>()
47
+ </script>
48
+
49
+ <template>
50
+ <div class="index-toolbar">
51
+ <div class="index-toolbar__inner">
52
+ <div class="index-toolbar__search">
53
+ <input
54
+ type="search"
55
+ :value="modelValue"
56
+ :placeholder="searchPlaceholder"
57
+ class="index-toolbar__search-input"
58
+ @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
59
+ />
60
+ <Search :size="16" class="index-toolbar__search-icon" />
61
+ </div>
62
+
63
+ <div class="index-toolbar__actions">
64
+ <SortToggles
65
+ v-if="showSort"
66
+ :model-value="sort"
67
+ class="index-toolbar__sort"
68
+ :latest-label="latestLabel"
69
+ :oldest-label="oldestLabel"
70
+ :name-label="nameLabel"
71
+ :name-desc-label="nameDescLabel"
72
+ @update:model-value="(v) => emit('update:sort', v)"
73
+ />
74
+ <button
75
+ v-if="showFilters"
76
+ type="button"
77
+ class="index-toolbar__filters"
78
+ @click="emit('open-filters')"
79
+ >
80
+ <Funnel :size="16" />
81
+ <span class="index-toolbar__filters-text">{{ filtersLabel }}</span>
82
+ <span v-if="activeCount > 0" class="index-toolbar__badge">{{ activeCount }}</span>
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </template>
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { ArrowDownAZ, ArrowDownZA, CalendarArrowDown, CalendarArrowUp } from '@lucide/vue'
4
+
5
+ // Par de toggles de ordenación de los index: fecha (latest ⇄ oldest) y
6
+ // alfabético (name ⇄ name_desc). Pulsar el inactivo lo activa en su primer
7
+ // estado; pulsar el activo invierte el sentido. Los valores casan con el
8
+ // ?sort del catálogo público del core.
9
+ // Agnóstico de i18n (DC-29): textos por prop, defaults en castellano.
10
+
11
+ export type SortValue = 'latest' | 'oldest' | 'name' | 'name_desc'
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ modelValue?: SortValue
16
+ latestLabel?: string
17
+ oldestLabel?: string
18
+ nameLabel?: string
19
+ nameDescLabel?: string
20
+ }>(),
21
+ {
22
+ modelValue: 'latest',
23
+ latestLabel: 'Más recientes primero',
24
+ oldestLabel: 'Más antiguos primero',
25
+ nameLabel: 'Alfabético (A-Z)',
26
+ nameDescLabel: 'Alfabético (Z-A)',
27
+ },
28
+ )
29
+
30
+ const emit = defineEmits<{ 'update:modelValue': [value: SortValue] }>()
31
+
32
+ const dateActive = computed(() => props.modelValue === 'latest' || props.modelValue === 'oldest')
33
+ const alphaActive = computed(() => props.modelValue === 'name' || props.modelValue === 'name_desc')
34
+
35
+ // Cada botón anuncia el estado que representa AHORA: el activo, su sentido
36
+ // actual; el inactivo, el primer estado al que llevaría el click.
37
+ const dateLabel = computed(() =>
38
+ props.modelValue === 'oldest' ? props.oldestLabel : props.latestLabel,
39
+ )
40
+ const alphaLabel = computed(() =>
41
+ props.modelValue === 'name_desc' ? props.nameDescLabel : props.nameLabel,
42
+ )
43
+
44
+ function toggleDate() {
45
+ emit('update:modelValue', props.modelValue === 'latest' ? 'oldest' : 'latest')
46
+ }
47
+
48
+ function toggleAlpha() {
49
+ emit('update:modelValue', props.modelValue === 'name' ? 'name_desc' : 'name')
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <div class="sort-toggles" role="group">
55
+ <button
56
+ type="button"
57
+ class="sort-toggles__btn"
58
+ :class="{ 'is-active': dateActive }"
59
+ :aria-pressed="dateActive"
60
+ :aria-label="dateLabel"
61
+ :title="dateLabel"
62
+ @click="toggleDate"
63
+ >
64
+ <CalendarArrowUp v-if="modelValue === 'oldest'" :size="16" />
65
+ <CalendarArrowDown v-else :size="16" />
66
+ </button>
67
+ <button
68
+ type="button"
69
+ class="sort-toggles__btn"
70
+ :class="{ 'is-active': alphaActive }"
71
+ :aria-pressed="alphaActive"
72
+ :aria-label="alphaLabel"
73
+ :title="alphaLabel"
74
+ @click="toggleAlpha"
75
+ >
76
+ <ArrowDownZA v-if="modelValue === 'name_desc'" :size="16" />
77
+ <ArrowDownAZ v-else :size="16" />
78
+ </button>
79
+ </div>
80
+ </template>
package/src/index.ts CHANGED
@@ -23,6 +23,9 @@ export { default as ImageUpload } from './components/ImageUpload.vue'
23
23
  export { default as FontUpload } from './components/FontUpload.vue'
24
24
  export { default as BaseModal } from './components/BaseModal.vue'
25
25
  export { default as EditModal } from './components/EditModal.vue'
26
+ export { default as FiltersModal } from './components/FiltersModal.vue'
27
+ export { default as SortToggles, type SortValue } from './components/SortToggles.vue'
28
+ export { default as IndexToolbar } from './components/IndexToolbar.vue'
26
29
  export { default as BaseTabs } from './components/BaseTabs.vue'
27
30
  export { default as BaseToast } from './components/BaseToast.vue'
28
31
  export { default as ToastContainer } from './components/ToastContainer.vue'