@mimi-ng/cli 0.1.1 → 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 +28 -1
- package/dist/init/css.js +17 -0
- package/dist/init/index.js +7 -5
- package/dist/init/items.js +9 -0
- package/dist/registry.json +482 -1
- package/dist/templates/components/avatar/avatar.ts +1 -1
- package/dist/templates/components/avatar/avatar.variants.ts +4 -4
- package/dist/templates/components/badge/badge.ts +41 -6
- package/dist/templates/components/badge/badge.variants.ts +105 -11
- package/dist/templates/components/button/button.ts +74 -7
- package/dist/templates/components/button/button.variants.ts +145 -21
- 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/card/card.ts +8 -5
- package/dist/templates/components/checkbox/checkbox.ts +3 -3
- 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/confirm/confirm-texts.ts +46 -0
- package/dist/templates/components/confirm/confirm.ts +190 -0
- package/dist/templates/components/confirm/index.ts +2 -0
- package/dist/templates/components/context-menu/context-menu.ts +378 -0
- package/dist/templates/components/context-menu/index.ts +14 -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/dialog/dialog-texts.ts +41 -0
- package/dist/templates/components/dialog/dialog.ts +701 -0
- package/dist/templates/components/dialog/index.ts +18 -0
- package/dist/templates/components/dropdown-menu/dropdown-menu.ts +252 -0
- package/dist/templates/components/dropdown-menu/index.ts +14 -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/form-field/form-error.ts +1 -1
- package/dist/templates/components/form-field/form-field.ts +3 -2
- package/dist/templates/components/input/input.variants.ts +6 -4
- 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/menu/index.ts +15 -0
- package/dist/templates/components/menu/menu.ts +656 -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/pagination/index.ts +2 -0
- package/dist/templates/components/pagination/pagination-page-size.ts +74 -0
- package/dist/templates/components/pagination/pagination-texts.ts +69 -0
- package/dist/templates/components/pagination/pagination.ts +443 -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/index.ts +12 -0
- package/dist/templates/components/popover/popover.ts +407 -0
- package/dist/templates/components/progress/index.ts +1 -0
- package/dist/templates/components/progress/progress.ts +217 -0
- package/dist/templates/components/select/index.ts +12 -0
- package/dist/templates/components/select/select-texts.ts +62 -0
- package/dist/templates/components/select/select.ts +1086 -0
- package/dist/templates/components/separator/separator.variants.ts +10 -7
- package/dist/templates/components/skeleton/skeleton.ts +4 -1
- 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/switch/switch.ts +6 -6
- package/dist/templates/components/tabs/index.ts +2 -0
- package/dist/templates/components/tabs/tabs.ts +426 -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/textarea/textarea.variants.ts +14 -11
- package/dist/templates/components/toast/index.ts +3 -0
- package/dist/templates/components/toast/toast-config.ts +45 -0
- package/dist/templates/components/toast/toast-texts.ts +46 -0
- package/dist/templates/components/toast/toast.ts +1000 -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/components/toolbar/index.ts +1 -0
- package/dist/templates/components/toolbar/toolbar.ts +377 -0
- package/dist/templates/components/tooltip/index.ts +7 -0
- package/dist/templates/components/tooltip/tooltip.ts +407 -0
- package/dist/templates/theme/provider.ts +75 -6
- package/dist/templates/theme/theme-base.css +410 -9
- package/dist/templates/theme/types.ts +494 -4
- package/dist/templates/utils/cn.ts +15 -1
- package/dist/templates/utils/control-styles.ts +2 -2
- package/dist/templates/utils/overlay.ts +153 -0
- package/dist/ui/index.js +184 -8
- package/dist/ui/schema.json +100 -0
- package/dist/utils/write-files.js +166 -59
- package/package.json +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
booleanAttribute,
|
|
6
|
+
computed,
|
|
7
|
+
inject,
|
|
8
|
+
input,
|
|
9
|
+
model,
|
|
10
|
+
output,
|
|
11
|
+
signal,
|
|
12
|
+
viewChild,
|
|
13
|
+
} from '@angular/core';
|
|
14
|
+
import type { FormValueControl } from '@angular/forms/signals';
|
|
15
|
+
import { MimiChip, injectMimiChipAnnouncer } from '@/components/ui/chip';
|
|
16
|
+
import { cn } from '@/components/ui/utils/cn';
|
|
17
|
+
import {
|
|
18
|
+
MIMI_FIELD_CONTROL,
|
|
19
|
+
injectFieldState,
|
|
20
|
+
type MimiFieldControl,
|
|
21
|
+
} from '@/components/ui/utils/field-state';
|
|
22
|
+
import { MIMI_TAGS_INPUT_TEXT } from './tags-input-texts';
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* Tags Input (docs/design/Mimi F3 Archivos Tags Toggle Color.dc.html, «10 · Tags Input»;
|
|
26
|
+
* prefijo de tokens `tags`, spec 13). Las etiquetas son chips del ítem `chip`.
|
|
27
|
+
*
|
|
28
|
+
* Teclado (decidido por defecto en la G2.6, como el diseño): Enter o un separador agregan;
|
|
29
|
+
* Backspace con el campo vacío quita la última; Tab recorre el campo y los botones de quitar.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const BOX_CLASSES = cn(
|
|
33
|
+
'relative flex w-full min-w-0 cursor-text flex-wrap items-center font-sans text-foreground mimi-transition',
|
|
34
|
+
'min-h-[var(--mimi-input-height,var(--mimi-control-height,2.5rem))] rounded-[var(--mimi-input-radius,var(--mimi-radius,0.75rem))]',
|
|
35
|
+
'gap-[var(--mimi-tags-gap,6px)] p-[var(--mimi-tags-padding,5px_10px_5px_6px)]',
|
|
36
|
+
'border border-input bg-input-background',
|
|
37
|
+
'not-data-[disabled]:hover:border-input-hover',
|
|
38
|
+
'data-[full]:border-[color:var(--mimi-tags-full-border,color-mix(in_oklab,var(--mimi-warning)_70%,var(--mimi-input)))]',
|
|
39
|
+
'focus-within:border-ring focus-within:ring-3 focus-within:ring-ring-soft',
|
|
40
|
+
'data-[invalid]:border-destructive data-[invalid]:focus-within:border-destructive data-[invalid]:focus-within:ring-destructive-ring',
|
|
41
|
+
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[var(--mimi-disabled-opacity,0.5)]',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const INPUT_CLASSES =
|
|
45
|
+
'h-7 min-w-[var(--mimi-tags-input-min-width,120px)] flex-1 border-0 bg-transparent px-1 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed';
|
|
46
|
+
|
|
47
|
+
/** Por defecto, solo quita los espacios de los lados (decidido por defecto en la G2.6). */
|
|
48
|
+
const trim = (tag: string): string => tag.trim();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Tags Input:
|
|
52
|
+
*
|
|
53
|
+
* ```html
|
|
54
|
+
* <mimi-tags-input aria-label="Etiquetas" [limit]="5" formControlName="tags" />
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Implementa `FormValueControl<string[]>`. Lo escrito que no se agregó no es parte del valor.
|
|
58
|
+
*/
|
|
59
|
+
@Component({
|
|
60
|
+
selector: 'mimi-tags-input',
|
|
61
|
+
imports: [MimiChip],
|
|
62
|
+
template: `
|
|
63
|
+
<div
|
|
64
|
+
data-slot="box"
|
|
65
|
+
[class]="boxClasses()"
|
|
66
|
+
[attr.data-invalid]="invalidShown() ? '' : null"
|
|
67
|
+
[attr.data-full]="full() ? '' : null"
|
|
68
|
+
[attr.data-disabled]="disabled() ? '' : null"
|
|
69
|
+
(click)="focusField($event)"
|
|
70
|
+
>
|
|
71
|
+
@for (tag of tags(); track $index) {
|
|
72
|
+
<mimi-chip
|
|
73
|
+
[label]="tag"
|
|
74
|
+
[removable]="!disabled() && !readonly()"
|
|
75
|
+
[invalid]="tag === conflict()"
|
|
76
|
+
(remove)="removeAt($index)"
|
|
77
|
+
>{{ tag }}</mimi-chip
|
|
78
|
+
>
|
|
79
|
+
}
|
|
80
|
+
<input
|
|
81
|
+
#field
|
|
82
|
+
type="text"
|
|
83
|
+
[class]="inputClasses"
|
|
84
|
+
[id]="controlId()"
|
|
85
|
+
[value]="draft()"
|
|
86
|
+
[disabled]="disabled()"
|
|
87
|
+
[readOnly]="readonly() || full()"
|
|
88
|
+
[attr.placeholder]="full() ? text.fullPlaceholder : placeholder() || null"
|
|
89
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
90
|
+
[attr.aria-labelledby]="ariaLabelledby() || null"
|
|
91
|
+
[attr.aria-describedby]="describedBy()"
|
|
92
|
+
[attr.aria-invalid]="invalidShown() || null"
|
|
93
|
+
[attr.aria-required]="required() || null"
|
|
94
|
+
(input)="onInput($event)"
|
|
95
|
+
(keydown)="onKeydown($event)"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="flex items-start justify-between gap-3 text-[12.5px]">
|
|
99
|
+
<span
|
|
100
|
+
[id]="messageId"
|
|
101
|
+
role="status"
|
|
102
|
+
data-slot="message"
|
|
103
|
+
class="flex-1"
|
|
104
|
+
[class]="messageClass()"
|
|
105
|
+
>{{ message() }}</span
|
|
106
|
+
>
|
|
107
|
+
@if (limit(); as max) {
|
|
108
|
+
<span
|
|
109
|
+
data-slot="count"
|
|
110
|
+
[id]="countId"
|
|
111
|
+
class="font-semibold tabular-nums"
|
|
112
|
+
[class]="full() ? 'text-warning-soft-foreground' : 'text-muted-foreground'"
|
|
113
|
+
>{{ tags().length }}/{{ max }}</span
|
|
114
|
+
>
|
|
115
|
+
}
|
|
116
|
+
</div>
|
|
117
|
+
`,
|
|
118
|
+
host: {
|
|
119
|
+
class: 'flex w-full min-w-0 flex-col gap-1.5',
|
|
120
|
+
'[attr.data-state]': 'tags().length ? "filled" : "empty"',
|
|
121
|
+
'[attr.aria-label]': 'null',
|
|
122
|
+
'[attr.aria-labelledby]': 'null',
|
|
123
|
+
'[attr.id]': 'null',
|
|
124
|
+
'(focusout)': 'onFocusOut($event)',
|
|
125
|
+
},
|
|
126
|
+
providers: [{ provide: MIMI_FIELD_CONTROL, useExisting: MimiTagsInput }],
|
|
127
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
128
|
+
})
|
|
129
|
+
export class MimiTagsInput implements FormValueControl<string[]>, MimiFieldControl {
|
|
130
|
+
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
|
|
131
|
+
protected readonly text = inject(MIMI_TAGS_INPUT_TEXT);
|
|
132
|
+
private readonly announce = injectMimiChipAnnouncer();
|
|
133
|
+
|
|
134
|
+
/** Las etiquetas. */
|
|
135
|
+
readonly value = model<string[]>([]);
|
|
136
|
+
/**
|
|
137
|
+
* Máximo de etiquetas (con contador). Sin él, sin límite. No se llama `max`: ese nombre lo
|
|
138
|
+
* escribe Signal Forms con su validador numérico.
|
|
139
|
+
*/
|
|
140
|
+
readonly limit = input<number | undefined, unknown>(undefined, {
|
|
141
|
+
transform: (v: unknown) => (v === null || v === undefined || v === '' ? undefined : Number(v)),
|
|
142
|
+
});
|
|
143
|
+
/** Caracteres que, al escribirlos, agregan la etiqueta (además de Enter). */
|
|
144
|
+
readonly separators = input<readonly string[]>([',']);
|
|
145
|
+
readonly allowDuplicates = input(false, { transform: booleanAttribute });
|
|
146
|
+
/** Normaliza la etiqueta antes de agregarla. Por defecto, quita los espacios de los lados. */
|
|
147
|
+
readonly normalize = input<(tag: string) => string>(trim);
|
|
148
|
+
/** Valida una etiqueta: devuelve el mensaje de error o nada. */
|
|
149
|
+
readonly validate = input<((tag: string) => string | null | undefined) | undefined>(undefined);
|
|
150
|
+
/** Ayuda debajo del campo. Por defecto, la de los textos («Enter o coma…»). */
|
|
151
|
+
readonly hint = input<string | undefined>(undefined);
|
|
152
|
+
readonly placeholder = input('');
|
|
153
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
154
|
+
readonly readonly = input(false, { transform: booleanAttribute });
|
|
155
|
+
/** Los pasa el formulario. Se muestra el error si es inválido y se tocó o modificó. */
|
|
156
|
+
readonly invalid = input(false, { transform: booleanAttribute });
|
|
157
|
+
readonly touched = input(false, { transform: booleanAttribute });
|
|
158
|
+
readonly dirty = input(false, { transform: booleanAttribute });
|
|
159
|
+
readonly required = input(false, { transform: booleanAttribute });
|
|
160
|
+
readonly id = input<string>();
|
|
161
|
+
readonly ariaLabel = input<string>(undefined, { alias: 'aria-label' });
|
|
162
|
+
readonly ariaLabelledby = input<string>(undefined, { alias: 'aria-labelledby' });
|
|
163
|
+
/** Clases de la caja, mezcladas con `cn()` (gana la del usuario). */
|
|
164
|
+
readonly userClass = input('', { alias: 'class' });
|
|
165
|
+
|
|
166
|
+
readonly touch = output<void>();
|
|
167
|
+
|
|
168
|
+
private readonly field = viewChild.required<ElementRef<HTMLInputElement>>('field');
|
|
169
|
+
readonly fieldState = injectFieldState();
|
|
170
|
+
get controlElement(): HTMLElement {
|
|
171
|
+
return this.field().nativeElement;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private static nextId = 0;
|
|
175
|
+
private readonly baseId = `mimi-tags-input-${MimiTagsInput.nextId++}`;
|
|
176
|
+
protected readonly controlId = computed(() => this.id() || this.baseId);
|
|
177
|
+
protected readonly messageId = `${this.baseId}-message`;
|
|
178
|
+
protected readonly countId = `${this.baseId}-count`;
|
|
179
|
+
readonly showInvalid = computed(() => this.invalid() && (this.touched() || this.dirty()));
|
|
180
|
+
|
|
181
|
+
/** Lo que se está escribiendo (no es parte del valor). */
|
|
182
|
+
protected readonly draft = signal('');
|
|
183
|
+
/** Error de la última etiqueta que no se pudo agregar (duplicada o inválida). */
|
|
184
|
+
private readonly localError = signal<string | null>(null);
|
|
185
|
+
/** La etiqueta repetida que se intentó agregar (su chip se pinta en rojo). */
|
|
186
|
+
protected readonly conflict = signal<string | null>(null);
|
|
187
|
+
|
|
188
|
+
protected readonly tags = computed(() => this.value() ?? []);
|
|
189
|
+
protected readonly full = computed(() => {
|
|
190
|
+
const max = this.limit();
|
|
191
|
+
return max !== undefined && this.tags().length >= max;
|
|
192
|
+
});
|
|
193
|
+
protected readonly invalidShown = computed(() => !!this.localError() || this.showInvalid());
|
|
194
|
+
protected readonly message = computed(() => {
|
|
195
|
+
const error = this.localError();
|
|
196
|
+
if (error) return error;
|
|
197
|
+
const max = this.limit();
|
|
198
|
+
if (this.full() && max !== undefined) return this.text.full(max);
|
|
199
|
+
return this.hint() ?? this.text.help;
|
|
200
|
+
});
|
|
201
|
+
protected readonly messageClass = computed(() =>
|
|
202
|
+
this.localError()
|
|
203
|
+
? 'text-destructive-soft-foreground'
|
|
204
|
+
: this.full()
|
|
205
|
+
? 'text-warning-soft-foreground'
|
|
206
|
+
: 'text-muted-foreground',
|
|
207
|
+
);
|
|
208
|
+
protected readonly describedBy = computed(() =>
|
|
209
|
+
this.limit() !== undefined ? `${this.messageId} ${this.countId}` : this.messageId,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
protected readonly boxClasses = computed(() => cn(BOX_CLASSES, this.userClass()));
|
|
213
|
+
protected readonly inputClasses = INPUT_CLASSES;
|
|
214
|
+
|
|
215
|
+
/** Un clic en la caja enfoca el campo (no en un botón de quitar). */
|
|
216
|
+
protected focusField(event: MouseEvent): void {
|
|
217
|
+
const el = this.field().nativeElement;
|
|
218
|
+
if ((event.target as HTMLElement).closest('button') || el.disabled) return;
|
|
219
|
+
el.focus();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
protected onInput(event: Event): void {
|
|
223
|
+
const el = event.target as HTMLInputElement;
|
|
224
|
+
const text = el.value;
|
|
225
|
+
this.localError.set(null);
|
|
226
|
+
this.conflict.set(null);
|
|
227
|
+
const separators = this.separators();
|
|
228
|
+
const split = new RegExp(`[${separators.map((s) => s.replace(/[\\\]^-]/g, '\\$&')).join('')}]`);
|
|
229
|
+
const pasted = (event as InputEvent).inputType === 'insertFromPaste';
|
|
230
|
+
if (!separators.length || (!split.test(text) && !pasted)) {
|
|
231
|
+
this.draft.set(text);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Lo escrito antes de un separador se agrega; lo de después queda escrito. Al pegar
|
|
235
|
+
// («angular, diseño, ventas»), se agregan todas.
|
|
236
|
+
const parts = separators.length ? text.split(split) : [text];
|
|
237
|
+
const rest = pasted ? '' : (parts.pop() ?? '');
|
|
238
|
+
for (const part of parts) {
|
|
239
|
+
if (!this.add(part)) {
|
|
240
|
+
this.draft.set(part.trim());
|
|
241
|
+
el.value = this.draft();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this.draft.set(rest);
|
|
246
|
+
el.value = rest;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
protected onKeydown(event: KeyboardEvent): void {
|
|
250
|
+
if (event.key === 'Enter') {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
if (this.add(this.draft())) {
|
|
253
|
+
this.draft.set('');
|
|
254
|
+
this.field().nativeElement.value = '';
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (event.key === 'Backspace' && !this.draft() && this.tags().length && !this.readonly()) {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
this.removeAt(this.tags().length - 1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Quita una etiqueta. El foco va al campo antes (si no, caería a `body` con el botón). */
|
|
265
|
+
removeAt(index: number): void {
|
|
266
|
+
if (this.disabled() || this.readonly()) return;
|
|
267
|
+
const tag = this.tags()[index];
|
|
268
|
+
if (tag === undefined) return;
|
|
269
|
+
this.field().nativeElement.focus();
|
|
270
|
+
this.value.set(this.tags().filter((_, i) => i !== index));
|
|
271
|
+
this.localError.set(null);
|
|
272
|
+
this.conflict.set(null);
|
|
273
|
+
this.announce.removed(tag);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Agrega una etiqueta. Devuelve `false` si no se pudo (y deja el mensaje de error). */
|
|
277
|
+
private add(raw: string): boolean {
|
|
278
|
+
const tag = this.normalize()(raw);
|
|
279
|
+
if (!tag) return true;
|
|
280
|
+
if (this.full()) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
if (!this.allowDuplicates() && this.tags().includes(tag)) {
|
|
284
|
+
this.localError.set(this.text.duplicate(tag));
|
|
285
|
+
this.conflict.set(tag);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const error = this.validate()?.(tag);
|
|
289
|
+
if (error) {
|
|
290
|
+
this.localError.set(error);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
this.value.set([...this.tags(), tag]);
|
|
294
|
+
this.announce.added(tag);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
protected onFocusOut(event: FocusEvent): void {
|
|
299
|
+
const next = event.relatedTarget as Node | null;
|
|
300
|
+
if (next && this.host.contains(next)) return;
|
|
301
|
+
this.touch.emit();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -6,22 +6,25 @@ import {
|
|
|
6
6
|
} from '@/components/ui/utils/control-styles';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Clases de mimiTextarea (docs/design/Mimi Componentes.dc.html, sección 2).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Clases de mimiTextarea (docs/design/Mimi Componentes.dc.html, sección 2). Tokens propios
|
|
10
|
+
* (--mimi-textarea-*) con respaldo en los de Input (--mimi-input-*): quien ya personalizó Input
|
|
11
|
+
* no ve cambios. Con una fila mide lo mismo que Input y Button: alto mínimo = alto del control,
|
|
12
|
+
* interlineado de 20px y padding vertical (alto − interlineado − 2 bordes) / 2.
|
|
12
13
|
*/
|
|
13
14
|
export const textareaVariants = cva([
|
|
14
|
-
'block w-full min-w-0 rounded-[var(--mimi-input-radius,var(--mimi-radius,0.75rem))]',
|
|
15
|
-
'border-[length:var(--mimi-input-border-width,1px)] border-input bg-input-background',
|
|
16
|
-
'min-h-[var(--mimi-input-height,var(--mimi-control-height,2.5rem))]',
|
|
17
|
-
'px-[var(--mimi-input-padding-x,0.75rem)]',
|
|
18
|
-
'py-[calc((var(--mimi-input-height,var(--mimi-control-height,2.5rem))_-
|
|
19
|
-
'font-sans text-[length:var(--mimi-input-font-size,0.875rem)] leading-
|
|
15
|
+
'block w-full min-w-0 rounded-[var(--mimi-textarea-radius,var(--mimi-input-radius,var(--mimi-radius,0.75rem)))]',
|
|
16
|
+
'border-[length:var(--mimi-input-border-width,1px)] border-[color:var(--mimi-textarea-border,var(--mimi-input-border,var(--mimi-input)))] bg-[color:var(--mimi-textarea-bg,var(--mimi-input-bg,var(--mimi-input-background)))]',
|
|
17
|
+
'min-h-[var(--mimi-textarea-min-height,var(--mimi-input-height,var(--mimi-control-height,2.5rem)))]',
|
|
18
|
+
'px-[var(--mimi-input-px,var(--mimi-input-padding-x,0.75rem))]',
|
|
19
|
+
'py-[var(--mimi-textarea-py,calc((var(--mimi-textarea-min-height,var(--mimi-input-height,var(--mimi-control-height,2.5rem)))_-_var(--mimi-textarea-line-height,1.25rem)_-_2*var(--mimi-input-border-width,1px))/2))]',
|
|
20
|
+
'font-sans text-[length:var(--mimi-input-font-size,0.875rem)] leading-[var(--mimi-textarea-line-height,1.25rem)] text-[color:var(--mimi-input-fg,var(--mimi-foreground))]',
|
|
20
21
|
'resize-y mimi-transition disabled:resize-none',
|
|
21
|
-
'placeholder:text-[color:var(--mimi-input-placeholder-color,var(--mimi-muted-foreground))]',
|
|
22
|
+
'placeholder:text-[color:var(--mimi-input-placeholder,var(--mimi-input-placeholder-color,var(--mimi-muted-foreground)))]',
|
|
22
23
|
fieldFocusStyles,
|
|
24
|
+
'focus-visible:border-[color:var(--mimi-input-border-focus,var(--mimi-ring))]',
|
|
23
25
|
'focus-visible:ring-[length:var(--mimi-input-focus-ring-width,3px)]',
|
|
24
26
|
controlDisabledStyles,
|
|
25
|
-
'disabled:opacity-[var(--mimi-input-disabled-opacity,0.5)]',
|
|
27
|
+
'disabled:opacity-[var(--mimi-input-disabled-opacity,var(--mimi-disabled-opacity,0.5))]',
|
|
26
28
|
controlInvalidStyles,
|
|
29
|
+
'aria-invalid:border-[color:var(--mimi-input-error,var(--mimi-destructive))] aria-invalid:focus-visible:border-[color:var(--mimi-input-error,var(--mimi-destructive))]',
|
|
27
30
|
]);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { InjectionToken, type Provider } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Opciones globales de Toast, en archivo propio y sin el componente: así `provideMimiToast()` se
|
|
5
|
+
* puede poner en `app.config.ts` sin llevar Toast al chunk inicial.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Dónde aparece la pila. El diseño solo muestra `bottom-right`. */
|
|
9
|
+
export type MimiToastPosition =
|
|
10
|
+
'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
11
|
+
|
|
12
|
+
/** Opciones de todos los toasts de la aplicación. */
|
|
13
|
+
export interface MimiToastConfig {
|
|
14
|
+
/** Por defecto, `bottom-right`. */
|
|
15
|
+
position: MimiToastPosition;
|
|
16
|
+
/** Cuánto dura un toast sin acción, en milisegundos. Por defecto, 6000. */
|
|
17
|
+
duration: number;
|
|
18
|
+
/** Tecla (`KeyboardEvent.key`) que lleva el foco a los toasts. Por defecto, `F8`. */
|
|
19
|
+
hotkey: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const MIMI_TOAST_DEFAULTS: MimiToastConfig = {
|
|
23
|
+
position: 'bottom-right',
|
|
24
|
+
duration: 6000,
|
|
25
|
+
hotkey: 'F8',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Opciones activas. Sin `provideMimiToast`, las predeterminadas. */
|
|
29
|
+
export const MIMI_TOAST_CONFIG = new InjectionToken<MimiToastConfig>('MIMI_TOAST_CONFIG', {
|
|
30
|
+
providedIn: 'root',
|
|
31
|
+
factory: () => MIMI_TOAST_DEFAULTS,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cambia las opciones de todos los toasts. Lo que no definas queda como está:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* provideMimiToast({ position: 'top-center' })
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Va en `app.config.ts`: los toasts viven en la raíz de la aplicación.
|
|
42
|
+
*/
|
|
43
|
+
export function provideMimiToast(config: Partial<MimiToastConfig>): Provider {
|
|
44
|
+
return { provide: MIMI_TOAST_CONFIG, useValue: { ...MIMI_TOAST_DEFAULTS, ...config } };
|
|
45
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { InjectionToken, type Provider } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Textos de Toast, en archivo propio y sin el componente ni @angular/cdk: así se pueden proveer
|
|
5
|
+
* en `app.config.ts` sin llevar Toast al chunk inicial.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Textos que Toast anuncia. */
|
|
9
|
+
export interface MimiToastText {
|
|
10
|
+
/** Nombre de la región de los toasts, con la tecla que lleva el foco a ellos. */
|
|
11
|
+
region: (hotkey: string) => string;
|
|
12
|
+
/** Nombre accesible del botón de cerrar (la X). */
|
|
13
|
+
close: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Textos predeterminados (inglés). */
|
|
17
|
+
export const MIMI_TOAST_TEXT_EN: MimiToastText = {
|
|
18
|
+
region: (hotkey) => `Notifications (${hotkey})`,
|
|
19
|
+
close: 'Close notification',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Textos en español: `provideMimiToastText(MIMI_TOAST_TEXT_ES)`. */
|
|
23
|
+
export const MIMI_TOAST_TEXT_ES: MimiToastText = {
|
|
24
|
+
region: (hotkey) => `Notificaciones (${hotkey})`,
|
|
25
|
+
close: 'Cerrar notificación',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Textos activos. Sin `provideMimiToastText`, los predeterminados en inglés. */
|
|
29
|
+
export const MIMI_TOAST_TEXT = new InjectionToken<MimiToastText>('MIMI_TOAST_TEXT', {
|
|
30
|
+
providedIn: 'root',
|
|
31
|
+
factory: () => MIMI_TOAST_TEXT_EN,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cambia los textos de Toast. Lo que no definas sigue en inglés:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* provideMimiToastText(MIMI_TOAST_TEXT_ES)
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* El nombre de la región se lee en la raíz: ponlo en `app.config.ts`. El de la X se lee donde se
|
|
42
|
+
* llama a `injectMimiToast()`, así que también sirve en los `providers` de una ruta o componente.
|
|
43
|
+
*/
|
|
44
|
+
export function provideMimiToastText(text: Partial<MimiToastText>): Provider {
|
|
45
|
+
return { provide: MIMI_TOAST_TEXT, useValue: { ...MIMI_TOAST_TEXT_EN, ...text } };
|
|
46
|
+
}
|