@edc-motor/ui 0.4.36 → 0.4.37
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 +1 -1
- package/scss/_forms.scss +1 -3
- package/scss/components/_index.scss +0 -1
- package/scss/components/_multi-select.scss +43 -1
- package/src/components/MultiSelect.vue +34 -12
- package/src/index.ts +0 -1
- package/scss/components/_number-input.scss +0 -48
- package/src/components/NumberInput.vue +0 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edc-motor/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.37",
|
|
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
|
@@ -9,13 +9,11 @@
|
|
|
9
9
|
.field { display: flex; flex-direction: column; gap: $space-1; }
|
|
10
10
|
.field__label, .field label { font-size: $fs-13; color: $text-2; }
|
|
11
11
|
|
|
12
|
-
// El numérico compacto del NumberInput queda FUERA: esta regla genérica le
|
|
13
|
-
// ganaba por especificidad y le imponía el 100%/36px de un campo de línea.
|
|
14
12
|
input[type="text"],
|
|
15
13
|
input[type="email"],
|
|
16
14
|
input[type="password"],
|
|
17
15
|
input[type="search"],
|
|
18
|
-
input[type="number"]
|
|
16
|
+
input[type="number"],
|
|
19
17
|
select,
|
|
20
18
|
textarea {
|
|
21
19
|
font: inherit;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
@use "tokens" as *;
|
|
2
2
|
|
|
3
3
|
// Select múltiple (MultiSelect.vue): hereda toda la estética de .base-select;
|
|
4
|
-
// aquí
|
|
4
|
+
// aquí la casilla de check de cada opción y las etiquetas del trigger (las
|
|
5
|
+
// elegidas se quitan desde su aspa sin abrir el panel).
|
|
5
6
|
.multi-select {
|
|
6
7
|
.base-select__value {
|
|
7
8
|
overflow: hidden;
|
|
@@ -9,6 +10,47 @@
|
|
|
9
10
|
white-space: nowrap;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
// El trigger crece con las etiquetas (min-height en vez del alto fijo).
|
|
14
|
+
.base-select__trigger {
|
|
15
|
+
height: auto;
|
|
16
|
+
min-height: $input-height;
|
|
17
|
+
padding-block: 4px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&__tags {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 4px;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&__tag {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 4px;
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
padding: 1px $space-1 1px $space-2;
|
|
34
|
+
border: 1px solid $border;
|
|
35
|
+
border-radius: $radius-sm;
|
|
36
|
+
background: $surface-2;
|
|
37
|
+
font-size: $fs-13;
|
|
38
|
+
line-height: 1.4;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&__tag-remove {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
flex: none;
|
|
46
|
+
color: $text-3;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
|
|
49
|
+
&:hover {
|
|
50
|
+
color: $danger;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
12
54
|
&__option {
|
|
13
55
|
display: flex;
|
|
14
56
|
align-items: center;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
|
|
3
|
-
import { Check } from '@lucide/vue'
|
|
3
|
+
import { Check, X } from '@lucide/vue'
|
|
4
4
|
import { useDropdownPanel } from '../composables/useDropdownPanel'
|
|
5
5
|
import type { SelectOption } from './BaseSelect.vue'
|
|
6
6
|
|
|
@@ -10,9 +10,9 @@ import type { SelectOption } from './BaseSelect.vue'
|
|
|
10
10
|
// marca de check y el panel SE CIERRA al elegir (como el simple: un valor
|
|
11
11
|
// por apertura — para añadir otro se reabre y las marcas siguen ahí). El
|
|
12
12
|
// valor viaja como array de strings (mismo criterio String() que
|
|
13
|
-
// BaseSelect). El trigger pinta las etiquetas
|
|
14
|
-
//
|
|
15
|
-
// "N seleccionadas" que traducir.
|
|
13
|
+
// BaseSelect). El trigger pinta las elegidas como etiquetas con aspa: se
|
|
14
|
+
// quitan desde ahí sin abrir el panel (también con Backspace); así no
|
|
15
|
+
// hace falta ningún texto "N seleccionadas" que traducir.
|
|
16
16
|
const props = withDefaults(
|
|
17
17
|
defineProps<{
|
|
18
18
|
modelValue?: (string | number)[]
|
|
@@ -42,12 +42,17 @@ useDropdownPanel(root, panel, open)
|
|
|
42
42
|
|
|
43
43
|
const selectedValues = computed(() => new Set(props.modelValue.map((v) => String(v))))
|
|
44
44
|
const isSelected = (option: SelectOption) => selectedValues.value.has(String(option.value))
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
45
|
+
const selectedOptions = computed(() => props.options.filter((o) => isSelected(o)))
|
|
46
|
+
|
|
47
|
+
/** Quita una elegida desde su etiqueta del trigger, sin tocar el panel. */
|
|
48
|
+
function removeOption(option: SelectOption) {
|
|
49
|
+
if (props.disabled) return
|
|
50
|
+
const value = String(option.value)
|
|
51
|
+
emit(
|
|
52
|
+
'update:modelValue',
|
|
53
|
+
props.modelValue.map((v) => String(v)).filter((v) => v !== value),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
51
56
|
|
|
52
57
|
function optionId(index: number) {
|
|
53
58
|
return `${selectId}-opt-${index}`
|
|
@@ -101,6 +106,10 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
101
106
|
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
|
|
102
107
|
e.preventDefault()
|
|
103
108
|
void openPanel()
|
|
109
|
+
} else if (e.key === 'Backspace' && selectedOptions.value.length) {
|
|
110
|
+
// Quitar la última elegida sin abrir el panel (espejo del aspa).
|
|
111
|
+
e.preventDefault()
|
|
112
|
+
removeOption(selectedOptions.value[selectedOptions.value.length - 1])
|
|
104
113
|
}
|
|
105
114
|
return
|
|
106
115
|
}
|
|
@@ -171,9 +180,22 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onOutside))
|
|
|
171
180
|
@click="toggle"
|
|
172
181
|
@keydown="onKeydown"
|
|
173
182
|
>
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
<!-- Elegidas como etiquetas con aspa (el aspa NO abre el panel);
|
|
184
|
+
span con role=button porque un button no puede anidar otro -->
|
|
185
|
+
<span v-if="selectedOptions.length" class="multi-select__tags">
|
|
186
|
+
<span v-for="option in selectedOptions" :key="option.value" class="multi-select__tag">
|
|
187
|
+
{{ option.label }}
|
|
188
|
+
<span
|
|
189
|
+
class="multi-select__tag-remove"
|
|
190
|
+
role="button"
|
|
191
|
+
:aria-label="String(option.label)"
|
|
192
|
+
@click.stop="removeOption(option)"
|
|
193
|
+
>
|
|
194
|
+
<X :size="12" />
|
|
195
|
+
</span>
|
|
196
|
+
</span>
|
|
176
197
|
</span>
|
|
198
|
+
<span v-else class="base-select__value is-placeholder">{{ placeholder }}</span>
|
|
177
199
|
</button>
|
|
178
200
|
|
|
179
201
|
<ul
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,6 @@ export {
|
|
|
31
31
|
export const RichTextInput = defineAsyncComponent(() => import('./components/RichTextInput.vue'))
|
|
32
32
|
export type { RichIcon, RichTextLabels } from './components/RichTextInput.vue'
|
|
33
33
|
export { default as ImageUpload } from './components/ImageUpload.vue'
|
|
34
|
-
export { default as NumberInput } from './components/NumberInput.vue'
|
|
35
34
|
export { default as MultiSelect } from './components/MultiSelect.vue'
|
|
36
35
|
export { default as FontUpload } from './components/FontUpload.vue'
|
|
37
36
|
export { default as BaseModal } from './components/BaseModal.vue'
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
@use "tokens" as *;
|
|
2
|
-
|
|
3
|
-
// Input numérico compacto con steppers −/+ (NumberInput.vue): la estética
|
|
4
|
-
// del contador de copias del editor de mazos de CDL, generalizada.
|
|
5
|
-
.number-input {
|
|
6
|
-
display: inline-flex;
|
|
7
|
-
align-items: center;
|
|
8
|
-
gap: $space-1;
|
|
9
|
-
|
|
10
|
-
&__step {
|
|
11
|
-
display: inline-flex;
|
|
12
|
-
align-items: center;
|
|
13
|
-
justify-content: center;
|
|
14
|
-
width: 1.6rem;
|
|
15
|
-
height: 1.6rem;
|
|
16
|
-
padding: 0;
|
|
17
|
-
border: 1px solid $border;
|
|
18
|
-
border-radius: $radius-sm;
|
|
19
|
-
background: $surface-2;
|
|
20
|
-
color: $text-2;
|
|
21
|
-
cursor: pointer;
|
|
22
|
-
|
|
23
|
-
&:hover:not(:disabled) { color: $text-1; border-color: $text-3; }
|
|
24
|
-
&:disabled { opacity: $disabled-opacity; cursor: not-allowed; }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
&__field {
|
|
28
|
-
width: 2.6rem;
|
|
29
|
-
padding: 0.1rem 0.2rem;
|
|
30
|
-
text-align: center;
|
|
31
|
-
font-weight: $k-fw-bold;
|
|
32
|
-
font-size: $fs-13;
|
|
33
|
-
color: $text-1;
|
|
34
|
-
border: 1px solid $border;
|
|
35
|
-
border-radius: $radius-sm;
|
|
36
|
-
background: $surface;
|
|
37
|
-
appearance: textfield;
|
|
38
|
-
-moz-appearance: textfield;
|
|
39
|
-
|
|
40
|
-
&::-webkit-outer-spin-button,
|
|
41
|
-
&::-webkit-inner-spin-button {
|
|
42
|
-
appearance: none;
|
|
43
|
-
margin: 0;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
&--invalid &__field { color: $danger; border-color: $danger; }
|
|
48
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
3
|
-
import { Minus, Plus } from '@lucide/vue'
|
|
4
|
-
|
|
5
|
-
// Input numérico compacto con steppers −/+ (nacido como el contador de
|
|
6
|
-
// copias del editor de mazos de CDL): input centrado sin flechas nativas,
|
|
7
|
-
// botones que respetan min/max y clamp al teclear. `invalid` pinta el borde
|
|
8
|
-
// en danger (p. ej. copias por encima del límite del modo).
|
|
9
|
-
const props = withDefaults(
|
|
10
|
-
defineProps<{
|
|
11
|
-
modelValue: number
|
|
12
|
-
min?: number
|
|
13
|
-
max?: number
|
|
14
|
-
step?: number
|
|
15
|
-
disabled?: boolean
|
|
16
|
-
invalid?: boolean
|
|
17
|
-
/** aria-label del input (los botones llevan los suyos). */
|
|
18
|
-
label?: string
|
|
19
|
-
decreaseLabel?: string
|
|
20
|
-
increaseLabel?: string
|
|
21
|
-
}>(),
|
|
22
|
-
{ step: 1, disabled: false, invalid: false },
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const emit = defineEmits<{ 'update:modelValue': [number] }>()
|
|
26
|
-
|
|
27
|
-
function clamp(value: number): number {
|
|
28
|
-
if (Number.isNaN(value)) value = props.min ?? 0
|
|
29
|
-
if (props.min !== undefined) value = Math.max(props.min, value)
|
|
30
|
-
if (props.max !== undefined) value = Math.min(props.max, value)
|
|
31
|
-
return value
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const atMin = computed(() => props.min !== undefined && props.modelValue <= props.min)
|
|
35
|
-
const atMax = computed(() => props.max !== undefined && props.modelValue >= props.max)
|
|
36
|
-
|
|
37
|
-
function stepBy(delta: number) {
|
|
38
|
-
emit('update:modelValue', clamp(props.modelValue + delta * props.step))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function onChange(event: Event) {
|
|
42
|
-
const input = event.target as HTMLInputElement
|
|
43
|
-
const value = clamp(Number(input.value))
|
|
44
|
-
// El clamp puede dejar el mismo modelValue (sin re-render): se repinta a mano.
|
|
45
|
-
input.value = String(value)
|
|
46
|
-
emit('update:modelValue', value)
|
|
47
|
-
}
|
|
48
|
-
</script>
|
|
49
|
-
|
|
50
|
-
<template>
|
|
51
|
-
<span class="number-input" :class="{ 'number-input--invalid': invalid }">
|
|
52
|
-
<button
|
|
53
|
-
type="button"
|
|
54
|
-
class="number-input__step"
|
|
55
|
-
:disabled="disabled || atMin"
|
|
56
|
-
:aria-label="decreaseLabel"
|
|
57
|
-
@click="stepBy(-1)"
|
|
58
|
-
>
|
|
59
|
-
<Minus :size="14" />
|
|
60
|
-
</button>
|
|
61
|
-
<input
|
|
62
|
-
class="number-input__field"
|
|
63
|
-
type="number"
|
|
64
|
-
:value="modelValue"
|
|
65
|
-
:min="min"
|
|
66
|
-
:max="max"
|
|
67
|
-
:step="step"
|
|
68
|
-
:disabled="disabled"
|
|
69
|
-
:aria-label="label"
|
|
70
|
-
@change="onChange"
|
|
71
|
-
/>
|
|
72
|
-
<button
|
|
73
|
-
type="button"
|
|
74
|
-
class="number-input__step"
|
|
75
|
-
:disabled="disabled || atMax"
|
|
76
|
-
:aria-label="increaseLabel"
|
|
77
|
-
@click="stepBy(1)"
|
|
78
|
-
>
|
|
79
|
-
<Plus :size="14" />
|
|
80
|
-
</button>
|
|
81
|
-
</span>
|
|
82
|
-
</template>
|