@mimi-ng/cli 0.2.0 → 0.3.0
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/README.md +15 -1
- package/dist/registry.json +274 -14
- package/dist/templates/components/calendar/calendar-texts.ts +72 -0
- package/dist/templates/components/calendar/calendar.ts +588 -0
- package/dist/templates/components/calendar/dates.ts +181 -0
- package/dist/templates/components/calendar/index.ts +3 -0
- package/dist/templates/components/chip/chip-texts.ts +40 -0
- package/dist/templates/components/chip/chip.ts +116 -0
- package/dist/templates/components/chip/index.ts +2 -0
- package/dist/templates/components/color-picker/color-picker-texts.ts +57 -0
- package/dist/templates/components/color-picker/color-picker.ts +718 -0
- package/dist/templates/components/color-picker/color.ts +157 -0
- package/dist/templates/components/color-picker/index.ts +3 -0
- package/dist/templates/components/combobox/combobox-texts.ts +70 -0
- package/dist/templates/components/combobox/combobox.ts +798 -0
- package/dist/templates/components/combobox/index.ts +2 -0
- package/dist/templates/components/date-picker/date-picker.ts +240 -0
- package/dist/templates/components/date-picker/index.ts +3 -0
- package/dist/templates/components/date-range/date-range.ts +308 -0
- package/dist/templates/components/date-range/index.ts +1 -0
- package/dist/templates/components/file-upload/file-upload-texts.ts +77 -0
- package/dist/templates/components/file-upload/file-upload.ts +698 -0
- package/dist/templates/components/file-upload/index.ts +2 -0
- package/dist/templates/components/input-mask/index.ts +1 -0
- package/dist/templates/components/input-mask/input-mask.ts +319 -0
- package/dist/templates/components/input-otp/index.ts +1 -0
- package/dist/templates/components/input-otp/input-otp.ts +212 -0
- package/dist/templates/components/number-input/index.ts +2 -0
- package/dist/templates/components/number-input/number-input-texts.ts +46 -0
- package/dist/templates/components/number-input/number-input.ts +450 -0
- package/dist/templates/components/option-list/index.ts +11 -0
- package/dist/templates/components/option-list/option-list.ts +518 -0
- package/dist/templates/components/password/index.ts +2 -0
- package/dist/templates/components/password/password-texts.ts +68 -0
- package/dist/templates/components/password/password.ts +363 -0
- package/dist/templates/components/popover/popover.ts +12 -4
- package/dist/templates/components/progress/index.ts +1 -0
- package/dist/templates/components/progress/progress.ts +217 -0
- package/dist/templates/components/select/select.ts +3 -2
- package/dist/templates/components/slider/index.ts +2 -0
- package/dist/templates/components/slider/slider-texts.ts +26 -0
- package/dist/templates/components/slider/slider.ts +425 -0
- package/dist/templates/components/tags-input/index.ts +2 -0
- package/dist/templates/components/tags-input/tags-input-texts.ts +44 -0
- package/dist/templates/components/tags-input/tags-input.ts +303 -0
- package/dist/templates/components/toggle-group/index.ts +1 -0
- package/dist/templates/components/toggle-group/toggle-group.ts +406 -0
- package/dist/templates/theme/provider.ts +13 -0
- package/dist/templates/theme/theme-base.css +103 -0
- package/dist/templates/theme/types.ts +216 -0
- package/dist/templates/utils/overlay.ts +17 -4
- package/dist/ui/index.js +105 -1
- package/dist/ui/schema.json +56 -0
- package/dist/utils/write-files.js +12 -2
- package/package.json +1 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { Directionality } from '@angular/cdk/bidi';
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
ElementRef,
|
|
6
|
+
booleanAttribute,
|
|
7
|
+
computed,
|
|
8
|
+
inject,
|
|
9
|
+
input,
|
|
10
|
+
model,
|
|
11
|
+
numberAttribute,
|
|
12
|
+
output,
|
|
13
|
+
signal,
|
|
14
|
+
viewChildren,
|
|
15
|
+
} from '@angular/core';
|
|
16
|
+
import type { FormValueControl } from '@angular/forms/signals';
|
|
17
|
+
import { cn } from '@/components/ui/utils/cn';
|
|
18
|
+
import { buttonFocusStyles } from '@/components/ui/utils/control-styles';
|
|
19
|
+
import {
|
|
20
|
+
MIMI_FIELD_CONTROL,
|
|
21
|
+
injectFieldState,
|
|
22
|
+
type MimiFieldControl,
|
|
23
|
+
} from '@/components/ui/utils/field-state';
|
|
24
|
+
import { MIMI_SLIDER_TEXT } from './slider-texts';
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Slider (docs/design/Mimi F3 Slider OTP Numero Mascara.dc.html, «4 · Slider»; prefijo de tokens
|
|
28
|
+
* `slider`, spec 13). Pulgares propios con `role="slider"` (patrones «Slider» y «Slider
|
|
29
|
+
* (Multi-Thumb)» de la APG), como el diseño: decidido por defecto en la G2.7 (opción (c)), porque
|
|
30
|
+
* el spike de la opción (d) —inputs nativos invisibles— necesitaba Firefox y Safari para
|
|
31
|
+
* confirmarse. Teclado, arrastre, RTL y el rango sin cruce los programa Mimi.
|
|
32
|
+
*
|
|
33
|
+
* Un solo `value` (spec 8): un número o, con `range`, `[desde, hasta]`.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const optionalNumber = (v: unknown): number | undefined =>
|
|
37
|
+
v === null || v === undefined || v === '' || Number.isNaN(Number(v)) ? undefined : Number(v);
|
|
38
|
+
|
|
39
|
+
const first = (v: MimiSliderValue | undefined): number | undefined =>
|
|
40
|
+
Array.isArray(v) ? v[0] : (v ?? undefined);
|
|
41
|
+
|
|
42
|
+
/** Una marca: un valor y, si se quiere, su texto debajo de la pista. */
|
|
43
|
+
export type MimiSliderMark = number | { value: number; label?: string };
|
|
44
|
+
/** Cuándo se ve la burbuja con el valor. `auto`: al arrastrar y con el foco del teclado. */
|
|
45
|
+
export type MimiSliderShowValue = 'auto' | 'always' | 'never';
|
|
46
|
+
export type MimiSliderValue = number | [number, number] | null;
|
|
47
|
+
|
|
48
|
+
const THUMB_BASE = cn(
|
|
49
|
+
'absolute top-1/2 box-border -translate-y-1/2 cursor-grab rounded-full border-2',
|
|
50
|
+
'size-[var(--mimi-slider-thumb-size,20px)] bg-[color:var(--mimi-slider-thumb,var(--mimi-thumb,var(--mimi-background)))]',
|
|
51
|
+
'shadow-[shadow:var(--mimi-slider-thumb-shadow,var(--mimi-shadow-neutral))]',
|
|
52
|
+
'[transition:box-shadow_.2s_ease,scale_.15s_cubic-bezier(.2,.8,.2,1)] motion-reduce:transition-none',
|
|
53
|
+
// Hover y arrastre suman el halo a la sombra del pulgar (su token), con tipo (estilos 8).
|
|
54
|
+
'not-aria-disabled:hover:shadow-[shadow:0_0_0_6px_var(--mimi-ring-soft),var(--mimi-slider-thumb-shadow,var(--mimi-shadow-neutral))]',
|
|
55
|
+
'aria-disabled:cursor-not-allowed',
|
|
56
|
+
buttonFocusStyles,
|
|
57
|
+
);
|
|
58
|
+
const DRAGGING =
|
|
59
|
+
'scale-[1.12] cursor-grabbing shadow-[shadow:0_0_0_8px_var(--mimi-ring-soft),var(--mimi-slider-thumb-shadow,var(--mimi-shadow-neutral))]';
|
|
60
|
+
/** Borde del pulgar y color de la parte llena: el relleno o, con error, `destructive`. */
|
|
61
|
+
const FILL = {
|
|
62
|
+
ok: {
|
|
63
|
+
bg: 'bg-[color:var(--mimi-slider-fill,var(--mimi-track-fill,var(--mimi-primary)))]',
|
|
64
|
+
border: 'border-[color:var(--mimi-slider-fill,var(--mimi-track-fill,var(--mimi-primary)))]',
|
|
65
|
+
},
|
|
66
|
+
error: { bg: 'bg-destructive', border: 'border-destructive' },
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Slider:
|
|
71
|
+
*
|
|
72
|
+
* ```html
|
|
73
|
+
* <mimi-slider aria-label="Volumen" [(value)]="volumen" />
|
|
74
|
+
* <mimi-slider aria-labelledby="precio" range [min]="0" [max]="1000" [step]="10"
|
|
75
|
+
* [format]="moneda" formControlName="precio" /> <!-- [300, 700] -->
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
@Component({
|
|
79
|
+
selector: 'mimi-slider',
|
|
80
|
+
template: `
|
|
81
|
+
<span
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
data-slot="track"
|
|
84
|
+
class="absolute inset-x-0 h-[var(--mimi-slider-height,6px)] rounded-full bg-[color:var(--mimi-slider-track,var(--mimi-track,var(--mimi-input)))]"
|
|
85
|
+
></span>
|
|
86
|
+
<span
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
data-slot="fill"
|
|
89
|
+
class="absolute h-[var(--mimi-slider-height,6px)] rounded-full"
|
|
90
|
+
[class]="fillClass()"
|
|
91
|
+
[style.inset-inline-start.%]="fillStart()"
|
|
92
|
+
[style.width.%]="fillWidth()"
|
|
93
|
+
></span>
|
|
94
|
+
@for (mark of markList(); track mark.value) {
|
|
95
|
+
<span
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
data-slot="mark"
|
|
98
|
+
class="absolute top-1/2 size-1.5 -translate-y-1/2 rounded-full ltr:-translate-x-1/2 rtl:translate-x-1/2"
|
|
99
|
+
[class]="
|
|
100
|
+
mark.inside
|
|
101
|
+
? 'bg-primary-foreground'
|
|
102
|
+
: 'bg-[color:color-mix(in_oklab,var(--mimi-foreground)_30%,transparent)]'
|
|
103
|
+
"
|
|
104
|
+
[style.inset-inline-start.%]="mark.percent"
|
|
105
|
+
></span>
|
|
106
|
+
@if (mark.label) {
|
|
107
|
+
<span
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
data-slot="mark-label"
|
|
110
|
+
class="absolute top-[26px] text-xs tabular-nums ltr:-translate-x-1/2 rtl:translate-x-1/2"
|
|
111
|
+
[class]="mark.current ? 'font-semibold text-foreground' : 'text-muted-foreground'"
|
|
112
|
+
[style.inset-inline-start.%]="mark.percent"
|
|
113
|
+
>{{ mark.label }}</span
|
|
114
|
+
>
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
@for (thumb of thumbs(); track $index) {
|
|
118
|
+
<span
|
|
119
|
+
#thumb
|
|
120
|
+
role="slider"
|
|
121
|
+
data-slot="thumb"
|
|
122
|
+
[class]="thumb.classes"
|
|
123
|
+
[style.inset-inline-start.%]="thumb.percent"
|
|
124
|
+
[attr.tabindex]="disabled() ? -1 : 0"
|
|
125
|
+
[attr.aria-label]="thumb.label"
|
|
126
|
+
[attr.aria-labelledby]="range() ? null : ariaLabelledby() || null"
|
|
127
|
+
[attr.aria-valuemin]="thumb.min"
|
|
128
|
+
[attr.aria-valuemax]="thumb.max"
|
|
129
|
+
[attr.aria-valuenow]="thumb.value"
|
|
130
|
+
[attr.aria-valuetext]="thumb.text"
|
|
131
|
+
[attr.aria-orientation]="'horizontal'"
|
|
132
|
+
[attr.aria-disabled]="disabled() || null"
|
|
133
|
+
[attr.aria-invalid]="showInvalid() || null"
|
|
134
|
+
[attr.data-dragging]="dragging() === $index ? '' : null"
|
|
135
|
+
(keydown)="onKeydown($event, $index)"
|
|
136
|
+
(focus)="focused.set($index)"
|
|
137
|
+
(blur)="onThumbBlur()"
|
|
138
|
+
>
|
|
139
|
+
@if (thumb.bubble) {
|
|
140
|
+
<span
|
|
141
|
+
aria-hidden="true"
|
|
142
|
+
data-slot="bubble"
|
|
143
|
+
class="pointer-events-none absolute bottom-[calc(100%+10px)] left-1/2 -translate-x-1/2 rounded-[7px] bg-tooltip px-2 py-1 text-xs font-semibold whitespace-nowrap text-tooltip-foreground tabular-nums"
|
|
144
|
+
>{{ thumb.text
|
|
145
|
+
}}<span
|
|
146
|
+
class="absolute -bottom-[3px] left-1/2 -ml-[3.5px] size-[7px] rotate-45 bg-tooltip"
|
|
147
|
+
></span
|
|
148
|
+
></span>
|
|
149
|
+
}
|
|
150
|
+
</span>
|
|
151
|
+
}
|
|
152
|
+
`,
|
|
153
|
+
host: {
|
|
154
|
+
'[class]': 'classes()',
|
|
155
|
+
'[attr.data-range]': 'range() ? "" : null',
|
|
156
|
+
'[attr.data-disabled]': 'disabled() ? "" : null',
|
|
157
|
+
'[attr.data-invalid]': 'showInvalid() ? "" : null',
|
|
158
|
+
'[attr.aria-label]': 'null',
|
|
159
|
+
'[attr.aria-labelledby]': 'null',
|
|
160
|
+
'[attr.role]': 'range() ? "group" : null',
|
|
161
|
+
'(pointerdown)': 'onPointerDown($event)',
|
|
162
|
+
'(pointermove)': 'onPointerMove($event)',
|
|
163
|
+
'(pointerup)': 'onPointerUp($event)',
|
|
164
|
+
'(pointercancel)': 'onPointerUp($event)',
|
|
165
|
+
},
|
|
166
|
+
providers: [{ provide: MIMI_FIELD_CONTROL, useExisting: MimiSlider }],
|
|
167
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
168
|
+
})
|
|
169
|
+
export class MimiSlider implements FormValueControl<MimiSliderValue>, MimiFieldControl {
|
|
170
|
+
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
|
|
171
|
+
private readonly dir = inject(Directionality).valueSignal;
|
|
172
|
+
protected readonly text = inject(MIMI_SLIDER_TEXT);
|
|
173
|
+
|
|
174
|
+
/** Un número o, con `range`, `[desde, hasta]`. Con `[(value)]` o un formulario. */
|
|
175
|
+
readonly value = model<MimiSliderValue>(null);
|
|
176
|
+
/** Dos pulgares que no se cruzan; el valor es `[desde, hasta]`. */
|
|
177
|
+
readonly range = input(false, { transform: booleanAttribute });
|
|
178
|
+
/** Mínimo (0 por defecto). También lo escribe Signal Forms desde el esquema. */
|
|
179
|
+
readonly min = input<NonNullable<MimiSliderValue> | undefined, unknown>(undefined, {
|
|
180
|
+
transform: optionalNumber,
|
|
181
|
+
});
|
|
182
|
+
/** Máximo (100 por defecto). */
|
|
183
|
+
readonly max = input<NonNullable<MimiSliderValue> | undefined, unknown>(undefined, {
|
|
184
|
+
transform: optionalNumber,
|
|
185
|
+
});
|
|
186
|
+
readonly step = input(1, { transform: numberAttribute });
|
|
187
|
+
/** Marcas sobre la pista, con su texto debajo si lo tienen. */
|
|
188
|
+
readonly marks = input<readonly MimiSliderMark[]>([]);
|
|
189
|
+
/** El texto del valor: `aria-valuetext` y la burbuja. Por defecto, el número. */
|
|
190
|
+
readonly format = input<(value: number) => string>((value) => String(value));
|
|
191
|
+
readonly showValue = input<MimiSliderShowValue>('auto');
|
|
192
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
193
|
+
/** Los pasa el formulario. Se muestra el error si es inválido y se tocó o modificó. */
|
|
194
|
+
readonly invalid = input(false, { transform: booleanAttribute });
|
|
195
|
+
readonly touched = input(false, { transform: booleanAttribute });
|
|
196
|
+
readonly dirty = input(false, { transform: booleanAttribute });
|
|
197
|
+
readonly required = input(false, { transform: booleanAttribute });
|
|
198
|
+
readonly ariaLabel = input<string>(undefined, { alias: 'aria-label' });
|
|
199
|
+
readonly ariaLabelledby = input<string>(undefined, { alias: 'aria-labelledby' });
|
|
200
|
+
/** Clases del contenedor, mezcladas con `cn()` (gana la del usuario). */
|
|
201
|
+
readonly userClass = input('', { alias: 'class' });
|
|
202
|
+
|
|
203
|
+
/** Al soltar el pulgar o al salir el foco: marca el control como tocado. */
|
|
204
|
+
readonly touch = output<void>();
|
|
205
|
+
|
|
206
|
+
private readonly thumbEls = viewChildren<ElementRef<HTMLElement>>('thumb');
|
|
207
|
+
readonly fieldState = injectFieldState();
|
|
208
|
+
get controlElement(): HTMLElement {
|
|
209
|
+
return this.thumbEls()[0]?.nativeElement ?? this.host;
|
|
210
|
+
}
|
|
211
|
+
readonly showInvalid = computed(() => this.invalid() && (this.touched() || this.dirty()));
|
|
212
|
+
|
|
213
|
+
protected readonly focused = signal<number | null>(null);
|
|
214
|
+
protected readonly dragging = signal<number | null>(null);
|
|
215
|
+
|
|
216
|
+
// El tipo de `min` y `max` es el del valor (lo exige `FormValueControl`); se usa el número.
|
|
217
|
+
private readonly lo = computed(() => Math.min(first(this.min()) ?? 0, first(this.max()) ?? 100));
|
|
218
|
+
private readonly hi = computed(() => Math.max(first(this.min()) ?? 0, first(this.max()) ?? 100));
|
|
219
|
+
/** Los valores que se pintan (acotados y en orden), sin tocar el modelo. */
|
|
220
|
+
private readonly values = computed<number[]>(() => {
|
|
221
|
+
const value = this.value();
|
|
222
|
+
if (this.range()) {
|
|
223
|
+
const [a, b] = Array.isArray(value) ? value : [this.lo(), this.hi()];
|
|
224
|
+
const x = this.clamp(a);
|
|
225
|
+
const y = this.clamp(b);
|
|
226
|
+
return [Math.min(x, y), Math.max(x, y)];
|
|
227
|
+
}
|
|
228
|
+
const n = Array.isArray(value) ? value[0] : value;
|
|
229
|
+
return [this.clamp(n ?? this.lo())];
|
|
230
|
+
});
|
|
231
|
+
private percentOf(value: number): number {
|
|
232
|
+
const span = this.hi() - this.lo();
|
|
233
|
+
return span > 0 ? ((value - this.lo()) / span) * 100 : 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Nombre del grupo (el aria-label o el texto de la etiqueta), para «Precio, Mínimo». */
|
|
237
|
+
private readonly groupLabel = computed(() => {
|
|
238
|
+
const label = this.ariaLabel()?.trim();
|
|
239
|
+
if (label) return label;
|
|
240
|
+
const ids = this.ariaLabelledby()?.trim();
|
|
241
|
+
if (!ids) return '';
|
|
242
|
+
// Desde la raíz del componente: también funciona fuera del documento (en pruebas).
|
|
243
|
+
const root = this.host.getRootNode() as ParentNode;
|
|
244
|
+
return ids
|
|
245
|
+
.split(/\s+/)
|
|
246
|
+
.map(
|
|
247
|
+
(id) =>
|
|
248
|
+
root.querySelector?.(`[id="${id.replace(/["\\]/g, '\\$&')}"]`)?.textContent?.trim() ?? '',
|
|
249
|
+
)
|
|
250
|
+
.join(' ')
|
|
251
|
+
.trim();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
protected readonly fillStart = computed(() =>
|
|
255
|
+
this.range() ? this.percentOf(this.values()[0]) : 0,
|
|
256
|
+
);
|
|
257
|
+
protected readonly fillWidth = computed(() => {
|
|
258
|
+
const values = this.values();
|
|
259
|
+
return this.range()
|
|
260
|
+
? this.percentOf(values[1]) - this.percentOf(values[0])
|
|
261
|
+
: this.percentOf(values[0]);
|
|
262
|
+
});
|
|
263
|
+
private readonly tone = computed(() => FILL[this.showInvalid() ? 'error' : 'ok']);
|
|
264
|
+
protected readonly fillClass = computed(() => this.tone().bg);
|
|
265
|
+
|
|
266
|
+
protected readonly thumbs = computed(() => {
|
|
267
|
+
const values = this.values();
|
|
268
|
+
const range = this.range();
|
|
269
|
+
const show = this.showValue();
|
|
270
|
+
const group = this.groupLabel();
|
|
271
|
+
return values.map((value, i) => {
|
|
272
|
+
const own = range ? (i === 0 ? this.text.minimum : this.text.maximum) : null;
|
|
273
|
+
const active = this.dragging() === i || this.focused() === i;
|
|
274
|
+
return {
|
|
275
|
+
value,
|
|
276
|
+
percent: this.percentOf(value),
|
|
277
|
+
// En el rango, cada pulgar no puede pasar al otro.
|
|
278
|
+
min: range && i === 1 ? values[0] : this.lo(),
|
|
279
|
+
max: range && i === 0 ? values[1] : this.hi(),
|
|
280
|
+
text: this.format()(value),
|
|
281
|
+
label: own ? (group ? `${group}, ${own}` : own) : this.ariaLabel() || null,
|
|
282
|
+
bubble: show === 'always' || (show === 'auto' && active && !this.disabled()),
|
|
283
|
+
classes: cn(
|
|
284
|
+
THUMB_BASE,
|
|
285
|
+
'ltr:-translate-x-1/2 rtl:translate-x-1/2',
|
|
286
|
+
this.tone().border,
|
|
287
|
+
this.dragging() === i && DRAGGING,
|
|
288
|
+
),
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
protected readonly markList = computed(() => {
|
|
294
|
+
const values = this.values();
|
|
295
|
+
const range = this.range();
|
|
296
|
+
return this.marks().map((mark) => {
|
|
297
|
+
const value = typeof mark === 'number' ? mark : mark.value;
|
|
298
|
+
const label = typeof mark === 'number' ? '' : (mark.label ?? '');
|
|
299
|
+
const inside = range ? value >= values[0] && value <= values[1] : value <= values[0];
|
|
300
|
+
return {
|
|
301
|
+
value,
|
|
302
|
+
label,
|
|
303
|
+
percent: this.percentOf(this.clamp(value)),
|
|
304
|
+
inside,
|
|
305
|
+
current: values.includes(value),
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
protected readonly classes = computed(() =>
|
|
311
|
+
cn(
|
|
312
|
+
// 24px de alto: todo el anfitrión recibe el puntero (va el pulgar más cercano), así el
|
|
313
|
+
// objetivo táctil cumple 2.5.8 aunque los dos pulgares estén juntos (5.17b).
|
|
314
|
+
'relative flex h-6 w-full touch-none items-center select-none',
|
|
315
|
+
this.markList().some((m) => m.label) && 'mb-6',
|
|
316
|
+
this.disabled()
|
|
317
|
+
? 'cursor-not-allowed opacity-[var(--mimi-disabled-opacity,0.5)]'
|
|
318
|
+
: 'cursor-pointer',
|
|
319
|
+
this.userClass(),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
protected onKeydown(event: KeyboardEvent, index: number): void {
|
|
324
|
+
if (this.disabled()) return;
|
|
325
|
+
const step = this.step() > 0 ? this.step() : 1;
|
|
326
|
+
const rtl = this.dir() === 'rtl';
|
|
327
|
+
const current = this.values()[index];
|
|
328
|
+
const next: Record<string, number> = {
|
|
329
|
+
ArrowRight: current + (rtl ? -step : step),
|
|
330
|
+
ArrowLeft: current + (rtl ? step : -step),
|
|
331
|
+
ArrowUp: current + step,
|
|
332
|
+
ArrowDown: current - step,
|
|
333
|
+
PageUp: current + step * 10,
|
|
334
|
+
PageDown: current - step * 10,
|
|
335
|
+
Home: this.lo(),
|
|
336
|
+
End: this.hi(),
|
|
337
|
+
};
|
|
338
|
+
if (!(event.key in next)) return;
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
this.setThumb(index, next[event.key]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Puntero en el contenedor: mueve el pulgar más cercano, le da el foco y lo arrastra. */
|
|
344
|
+
protected onPointerDown(event: PointerEvent): void {
|
|
345
|
+
if (this.disabled() || event.button !== 0) return;
|
|
346
|
+
const value = this.valueAt(event.clientX);
|
|
347
|
+
if (value === null) return;
|
|
348
|
+
const values = this.values();
|
|
349
|
+
let index = 0;
|
|
350
|
+
if (this.range()) {
|
|
351
|
+
const [a, b] = values;
|
|
352
|
+
index =
|
|
353
|
+
Math.abs(value - a) < Math.abs(value - b)
|
|
354
|
+
? 0
|
|
355
|
+
: Math.abs(value - a) > Math.abs(value - b)
|
|
356
|
+
? 1
|
|
357
|
+
: value > b
|
|
358
|
+
? 1
|
|
359
|
+
: 0;
|
|
360
|
+
}
|
|
361
|
+
event.preventDefault();
|
|
362
|
+
this.dragging.set(index);
|
|
363
|
+
this.host.setPointerCapture?.(event.pointerId);
|
|
364
|
+
this.thumbEls()[index]?.nativeElement.focus();
|
|
365
|
+
this.setThumb(index, value);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
protected onPointerMove(event: PointerEvent): void {
|
|
369
|
+
const index = this.dragging();
|
|
370
|
+
if (index === null) return;
|
|
371
|
+
const value = this.valueAt(event.clientX);
|
|
372
|
+
if (value !== null) this.setThumb(index, value);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
protected onPointerUp(event: PointerEvent): void {
|
|
376
|
+
if (this.dragging() === null) return;
|
|
377
|
+
this.dragging.set(null);
|
|
378
|
+
if (this.host.hasPointerCapture?.(event.pointerId)) {
|
|
379
|
+
this.host.releasePointerCapture(event.pointerId);
|
|
380
|
+
}
|
|
381
|
+
this.touch.emit();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
protected onThumbBlur(): void {
|
|
385
|
+
this.focused.set(null);
|
|
386
|
+
if (this.dragging() === null) this.touch.emit();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** El valor bajo una posición del puntero (con el paso; en RTL, de derecha a izquierda). */
|
|
390
|
+
private valueAt(clientX: number): number | null {
|
|
391
|
+
const rect = this.host.getBoundingClientRect();
|
|
392
|
+
if (!rect.width) return null;
|
|
393
|
+
let ratio = (clientX - rect.left) / rect.width;
|
|
394
|
+
if (this.dir() === 'rtl') ratio = 1 - ratio;
|
|
395
|
+
ratio = Math.min(1, Math.max(0, ratio));
|
|
396
|
+
return this.lo() + ratio * (this.hi() - this.lo());
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Pone un pulgar: redondea al paso, acota y, en el rango, no deja que cruce al otro. */
|
|
400
|
+
private setThumb(index: number, raw: number): void {
|
|
401
|
+
let value = this.clamp(this.roundToStep(raw));
|
|
402
|
+
if (this.range()) {
|
|
403
|
+
const values = [...this.values()] as [number, number];
|
|
404
|
+
value = index === 0 ? Math.min(value, values[1]) : Math.max(value, values[0]);
|
|
405
|
+
values[index] = value;
|
|
406
|
+
const current = this.value();
|
|
407
|
+
if (!Array.isArray(current) || current[0] !== values[0] || current[1] !== values[1]) {
|
|
408
|
+
this.value.set(values);
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (this.value() !== value) this.value.set(value);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private clamp(value: number): number {
|
|
416
|
+
return Math.min(this.hi(), Math.max(this.lo(), Number.isFinite(value) ? value : this.lo()));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private roundToStep(value: number): number {
|
|
420
|
+
const step = this.step() > 0 ? this.step() : 1;
|
|
421
|
+
const decimals = (String(step).split('.')[1] ?? '').length;
|
|
422
|
+
const rounded = this.lo() + Math.round((value - this.lo()) / step) * step;
|
|
423
|
+
return Number(rounded.toFixed(decimals));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { InjectionToken, type Provider } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Textos de Tags Input, en archivo propio (como Select, G1.5). Los del chip (quitar y los
|
|
5
|
+
* anuncios) están en `chip-texts.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface MimiTagsInputText {
|
|
9
|
+
/** Ayuda debajo del campo. */
|
|
10
|
+
help: string;
|
|
11
|
+
/** La etiqueta ya está en la lista. */
|
|
12
|
+
duplicate: (tag: string) => string;
|
|
13
|
+
/** Se llegó al máximo. */
|
|
14
|
+
full: (max: number) => string;
|
|
15
|
+
/** Placeholder con el máximo alcanzado. */
|
|
16
|
+
fullPlaceholder: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Textos predeterminados (inglés). */
|
|
20
|
+
export const MIMI_TAGS_INPUT_TEXT_EN: MimiTagsInputText = {
|
|
21
|
+
help: 'Enter or comma to add · Backspace removes the last one',
|
|
22
|
+
duplicate: (tag) => `“${tag}” is already in the list.`,
|
|
23
|
+
full: (max) => `You reached the limit of ${max}. Remove one to add another.`,
|
|
24
|
+
fullPlaceholder: 'Limit reached',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Textos en español: `provideMimiTagsInputText(MIMI_TAGS_INPUT_TEXT_ES)`. */
|
|
28
|
+
export const MIMI_TAGS_INPUT_TEXT_ES: MimiTagsInputText = {
|
|
29
|
+
help: 'Enter o coma para agregar · Backspace quita la última',
|
|
30
|
+
duplicate: (tag) => `«${tag}» ya está en la lista.`,
|
|
31
|
+
full: (max) => `Llegaste al límite de ${max} etiquetas. Quita una para agregar otra.`,
|
|
32
|
+
fullPlaceholder: 'Límite alcanzado',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Textos activos. Sin `provideMimiTagsInputText`, los predeterminados en inglés. */
|
|
36
|
+
export const MIMI_TAGS_INPUT_TEXT = new InjectionToken<MimiTagsInputText>('MIMI_TAGS_INPUT_TEXT', {
|
|
37
|
+
providedIn: 'root',
|
|
38
|
+
factory: () => MIMI_TAGS_INPUT_TEXT_EN,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** Cambia los textos de Tags Input. Lo que no definas sigue en inglés. */
|
|
42
|
+
export function provideMimiTagsInputText(text: Partial<MimiTagsInputText>): Provider {
|
|
43
|
+
return { provide: MIMI_TAGS_INPUT_TEXT, useValue: { ...MIMI_TAGS_INPUT_TEXT_EN, ...text } };
|
|
44
|
+
}
|