@mimi-ng/cli 0.2.0 → 0.4.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 +35 -2
- package/dist/init/css.js +1 -1
- package/dist/init/index.js +25 -0
- package/dist/init/paths.js +20 -0
- package/dist/registry.json +385 -14
- package/dist/templates/blocks/login-01/index.ts +1 -0
- package/dist/templates/blocks/login-01/login-01.html +174 -0
- package/dist/templates/blocks/login-01/login-01.ts +105 -0
- package/dist/templates/blocks/login-02/index.ts +1 -0
- package/dist/templates/blocks/login-02/login-02.html +186 -0
- package/dist/templates/blocks/login-02/login-02.ts +92 -0
- package/dist/templates/blocks/otp-01/index.ts +1 -0
- package/dist/templates/blocks/otp-01/otp-01.html +166 -0
- package/dist/templates/blocks/otp-01/otp-01.ts +212 -0
- package/dist/templates/blocks/pricing-01/index.ts +1 -0
- package/dist/templates/blocks/pricing-01/pricing-01.html +173 -0
- package/dist/templates/blocks/pricing-01/pricing-01.ts +178 -0
- package/dist/templates/blocks/settings-01/index.ts +1 -0
- package/dist/templates/blocks/settings-01/settings-01.html +424 -0
- package/dist/templates/blocks/settings-01/settings-01.ts +382 -0
- package/dist/templates/blocks/signup-01/index.ts +1 -0
- package/dist/templates/blocks/signup-01/signup-01.html +332 -0
- package/dist/templates/blocks/signup-01/signup-01.ts +176 -0
- 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/tabs/tabs.ts +88 -23
- 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 +232 -10
- package/dist/ui/schema.json +82 -2
- package/dist/utils/project.js +35 -0
- package/dist/utils/tailwind.js +80 -0
- package/dist/utils/write-files.js +49 -9
- 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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './toggle-group';
|