@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.
Files changed (55) hide show
  1. package/README.md +15 -1
  2. package/dist/registry.json +274 -14
  3. package/dist/templates/components/calendar/calendar-texts.ts +72 -0
  4. package/dist/templates/components/calendar/calendar.ts +588 -0
  5. package/dist/templates/components/calendar/dates.ts +181 -0
  6. package/dist/templates/components/calendar/index.ts +3 -0
  7. package/dist/templates/components/chip/chip-texts.ts +40 -0
  8. package/dist/templates/components/chip/chip.ts +116 -0
  9. package/dist/templates/components/chip/index.ts +2 -0
  10. package/dist/templates/components/color-picker/color-picker-texts.ts +57 -0
  11. package/dist/templates/components/color-picker/color-picker.ts +718 -0
  12. package/dist/templates/components/color-picker/color.ts +157 -0
  13. package/dist/templates/components/color-picker/index.ts +3 -0
  14. package/dist/templates/components/combobox/combobox-texts.ts +70 -0
  15. package/dist/templates/components/combobox/combobox.ts +798 -0
  16. package/dist/templates/components/combobox/index.ts +2 -0
  17. package/dist/templates/components/date-picker/date-picker.ts +240 -0
  18. package/dist/templates/components/date-picker/index.ts +3 -0
  19. package/dist/templates/components/date-range/date-range.ts +308 -0
  20. package/dist/templates/components/date-range/index.ts +1 -0
  21. package/dist/templates/components/file-upload/file-upload-texts.ts +77 -0
  22. package/dist/templates/components/file-upload/file-upload.ts +698 -0
  23. package/dist/templates/components/file-upload/index.ts +2 -0
  24. package/dist/templates/components/input-mask/index.ts +1 -0
  25. package/dist/templates/components/input-mask/input-mask.ts +319 -0
  26. package/dist/templates/components/input-otp/index.ts +1 -0
  27. package/dist/templates/components/input-otp/input-otp.ts +212 -0
  28. package/dist/templates/components/number-input/index.ts +2 -0
  29. package/dist/templates/components/number-input/number-input-texts.ts +46 -0
  30. package/dist/templates/components/number-input/number-input.ts +450 -0
  31. package/dist/templates/components/option-list/index.ts +11 -0
  32. package/dist/templates/components/option-list/option-list.ts +518 -0
  33. package/dist/templates/components/password/index.ts +2 -0
  34. package/dist/templates/components/password/password-texts.ts +68 -0
  35. package/dist/templates/components/password/password.ts +363 -0
  36. package/dist/templates/components/popover/popover.ts +12 -4
  37. package/dist/templates/components/progress/index.ts +1 -0
  38. package/dist/templates/components/progress/progress.ts +217 -0
  39. package/dist/templates/components/select/select.ts +3 -2
  40. package/dist/templates/components/slider/index.ts +2 -0
  41. package/dist/templates/components/slider/slider-texts.ts +26 -0
  42. package/dist/templates/components/slider/slider.ts +425 -0
  43. package/dist/templates/components/tags-input/index.ts +2 -0
  44. package/dist/templates/components/tags-input/tags-input-texts.ts +44 -0
  45. package/dist/templates/components/tags-input/tags-input.ts +303 -0
  46. package/dist/templates/components/toggle-group/index.ts +1 -0
  47. package/dist/templates/components/toggle-group/toggle-group.ts +406 -0
  48. package/dist/templates/theme/provider.ts +13 -0
  49. package/dist/templates/theme/theme-base.css +103 -0
  50. package/dist/templates/theme/types.ts +216 -0
  51. package/dist/templates/utils/overlay.ts +17 -4
  52. package/dist/ui/index.js +105 -1
  53. package/dist/ui/schema.json +56 -0
  54. package/dist/utils/write-files.js +12 -2
  55. package/package.json +1 -1
@@ -0,0 +1,406 @@
1
+ import { FocusKeyManager, type FocusableOption } from '@angular/cdk/a11y';
2
+ import { Directionality } from '@angular/cdk/bidi';
3
+ import {
4
+ ChangeDetectionStrategy,
5
+ Component,
6
+ DestroyRef,
7
+ Directive,
8
+ ElementRef,
9
+ Injector,
10
+ afterRenderEffect,
11
+ booleanAttribute,
12
+ computed,
13
+ contentChildren,
14
+ effect,
15
+ inject,
16
+ input,
17
+ isDevMode,
18
+ model,
19
+ output,
20
+ signal,
21
+ untracked,
22
+ } from '@angular/core';
23
+ import type { FormValueControl } from '@angular/forms/signals';
24
+ import { cva } from 'class-variance-authority';
25
+ import { cn } from '@/components/ui/utils/cn';
26
+ import { buttonFocusStyles } from '@/components/ui/utils/control-styles';
27
+
28
+ /*
29
+ * Toggle Group (docs/design/Mimi F3 Archivos Tags Toggle Color.dc.html, «11 · Toggle Group ·
30
+ * control segmentado»; prefijo de tokens `toggle-group`, spec 13).
31
+ *
32
+ * Simple: patrón «Radio Group» de la APG (`radiogroup` y `radio`, una sola parada de Tab, las
33
+ * flechas mueven el foco y eligen, con vuelta). Múltiple: botones de alternancia (`aria-pressed`),
34
+ * cada uno con su parada de Tab. El foco itinerante usa el FocusKeyManager de @angular/cdk/a11y
35
+ * (como mimi-toolbar, G1.10): @angular/aria no tiene un grupo de radio propio (plan, G2.1).
36
+ *
37
+ * Un solo `value` (spec 8, «Valor simple o múltiple»): `T | null` en simple y `T[]` con
38
+ * `multiple`. Los botones son del usuario y nativos: Espacio y el clic funcionan sin rodeos.
39
+ */
40
+
41
+ /** `default`: el elegido sobre `card`. `solid`: el elegido en `primary` (el diseño la llama así). */
42
+ export type MimiToggleGroupVariant = 'default' | 'solid';
43
+
44
+ const groupVariants = cva(
45
+ [
46
+ 'relative isolate items-stretch align-middle font-sans',
47
+ 'gap-[var(--mimi-toggle-group-gap,2px)] p-[var(--mimi-toggle-group-padding,3px)]',
48
+ 'rounded-[calc(var(--mimi-toggle-group-radius,var(--mimi-radius))_+_var(--mimi-toggle-group-padding,3px))]',
49
+ 'bg-[color:var(--mimi-toggle-group-bg,var(--mimi-muted))] mimi-transition',
50
+ // Deshabilitado: el grupo entero (el diseño no tiene ítems deshabilitados de a uno).
51
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-(--mimi-disabled-opacity)',
52
+ // Error del formulario (sin diseño, plan D6): un borde de 1px como el de Input.
53
+ 'data-invalid:ring-1 data-invalid:ring-destructive',
54
+ ],
55
+ {
56
+ variants: {
57
+ // Simple: columnas iguales (el indicador se desplaza su propio ancho). Múltiple: a lo ancho
58
+ // de cada ítem, como el diseño («Texto e ícono · múltiple»).
59
+ multiple: {
60
+ false: 'inline-grid grid-flow-col auto-cols-fr',
61
+ true: 'inline-flex',
62
+ },
63
+ },
64
+ defaultVariants: { multiple: false },
65
+ },
66
+ );
67
+
68
+ /*
69
+ * Las clases de cada estado se eligen en TypeScript (no con `data-[…]:` encadenados) y la
70
+ * geometría del indicador va en `style`: el mismo aspecto con menos CSS global (G2.1, spec 10).
71
+ */
72
+
73
+ /** Fondo y sombra del elegido: el indicador (simple) o cada botón (múltiple). */
74
+ const SELECTED_SURFACE: Record<MimiToggleGroupVariant, string> = {
75
+ default:
76
+ 'bg-[color:var(--mimi-toggle-group-indicator,var(--mimi-card))] shadow-[shadow:var(--mimi-toggle-group-indicator-shadow,var(--mimi-shadow-neutral))]',
77
+ solid: 'bg-primary shadow-primary',
78
+ };
79
+
80
+ /** El indicador de la selección simple (su posición y su ancho van en `style`). */
81
+ const INDICATOR_CLASSES =
82
+ 'pointer-events-none absolute -z-1 rounded-[var(--mimi-toggle-group-radius,var(--mimi-radius))] transition-[translate,background-color] duration-[450ms] motion-reduce:transition-none';
83
+
84
+ /**
85
+ * Posición del indicador: a un relleno del borde; ancho de una columna, (100% − 2 × relleno −
86
+ * (n − 1) × separación) / n; se desplaza un ancho más una separación por ítem (en RTL, al revés).
87
+ */
88
+ const PAD = 'var(--mimi-toggle-group-padding, 3px)';
89
+ const GAP = 'var(--mimi-toggle-group-gap, 2px)';
90
+ const INDICATOR_STYLE = {
91
+ top: PAD,
92
+ bottom: PAD,
93
+ start: PAD,
94
+ width: `calc((100% - 2 * ${PAD} - (var(--mimi-toggle-group-count) - 1) * ${GAP}) / var(--mimi-toggle-group-count))`,
95
+ easing: 'var(--mimi-toggle-group-spring, cubic-bezier(.3, 1.35, .5, 1))',
96
+ } as const;
97
+
98
+ const ITEM_CLASSES = cn(
99
+ 'relative inline-flex min-w-[var(--mimi-toggle-group-icon-width,44px)] cursor-pointer items-center justify-center gap-1.5 border-0 bg-transparent whitespace-nowrap',
100
+ 'h-[var(--mimi-toggle-group-item-height,32px)] px-[var(--mimi-toggle-group-item-px,12px)]',
101
+ 'rounded-[var(--mimi-toggle-group-radius,var(--mimi-radius))]',
102
+ 'text-[length:var(--mimi-toggle-group-font-size,13.5px)] font-medium text-[color:var(--mimi-toggle-group-fg,var(--mimi-muted-foreground))]',
103
+ '[&_svg]:size-4 [&_svg]:shrink-0',
104
+ 'mimi-transition enabled:hover:text-[color:var(--mimi-toggle-group-active-fg,var(--mimi-foreground))]',
105
+ // 0,9 (con movimiento reducido, 1); el diseño usa 0,93 sin token: plan D6.
106
+ 'enabled:active:scale-(--mimi-press-scale-sm) disabled:cursor-not-allowed',
107
+ buttonFocusStyles,
108
+ );
109
+
110
+ /** Elegido: más peso y el color activo; en `solid`, el texto sobre primary. */
111
+ const ON_TEXT: Record<MimiToggleGroupVariant, string> = {
112
+ default: 'font-semibold text-[color:var(--mimi-toggle-group-active-fg,var(--mimi-foreground))]',
113
+ solid: 'font-semibold text-primary-foreground enabled:hover:text-primary-foreground',
114
+ };
115
+
116
+ /**
117
+ * Un botón del grupo: `<button mimiToggleGroupItem="semanal">Semanal</button>`. El contenido
118
+ * (texto, ícono o los dos) y el `aria-label` (si no tiene texto) son del usuario.
119
+ */
120
+ @Directive({
121
+ selector: 'button[mimiToggleGroupItem]',
122
+ host: {
123
+ type: 'button',
124
+ '[attr.role]': 'group.multiple() ? null : "radio"',
125
+ '[attr.aria-checked]': 'group.multiple() ? null : on()',
126
+ '[attr.aria-pressed]': 'group.multiple() ? on() : null',
127
+ '[attr.tabindex]': 'tabIndex()',
128
+ '[disabled]': 'group.disabled()',
129
+ '[attr.data-state]': 'on() ? "on" : "off"',
130
+ '[attr.data-variant]': 'group.variant()',
131
+ '[attr.data-multiple]': 'group.multiple() ? "" : null',
132
+ '[attr.data-disabled]': 'group.disabled() ? "" : null',
133
+ '[class]': 'classes()',
134
+ '(click)': 'group.select(this)',
135
+ },
136
+ })
137
+ export class MimiToggleGroupItem implements FocusableOption {
138
+ /** El valor del botón (único en el grupo). */
139
+ readonly value = input<unknown>(undefined, { alias: 'mimiToggleGroupItem' });
140
+ /** Clases del botón, mezcladas con `cn()` (gana la del usuario). */
141
+ readonly userClass = input('', { alias: 'class' });
142
+
143
+ /** Uso interno. */
144
+ readonly element = inject<ElementRef<HTMLButtonElement>>(ElementRef).nativeElement;
145
+ protected readonly group = inject(MimiToggleGroup);
146
+
147
+ /** Para el FocusKeyManager. */
148
+ get disabled(): boolean {
149
+ return this.group.disabled();
150
+ }
151
+
152
+ protected readonly on = computed(() => this.group.isOn(this.value()));
153
+ protected readonly classes = computed(() => {
154
+ const variant = this.group.variant();
155
+ const on = this.on();
156
+ return cn(
157
+ ITEM_CLASSES,
158
+ on && ON_TEXT[variant],
159
+ // Múltiple: cada elegido lleva su fondo (no hay indicador que se desliza).
160
+ on && this.group.multiple() && SELECTED_SURFACE[variant],
161
+ this.userClass(),
162
+ );
163
+ });
164
+ /** Simple: solo el elegido (o el primero) es parada de Tab. Múltiple: todos. */
165
+ protected readonly tabIndex = computed(() =>
166
+ this.group.multiple() ? null : this.group.isTabStop(this) ? 0 : -1,
167
+ );
168
+
169
+ focus(): void {
170
+ this.element.focus();
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Toggle Group:
176
+ *
177
+ * ```html
178
+ * <mimi-toggle-group aria-label="Periodo" [(value)]="periodo">
179
+ * <button mimiToggleGroupItem="dia">Diario</button>
180
+ * <button mimiToggleGroupItem="semana">Semanal</button>
181
+ * </mimi-toggle-group>
182
+ *
183
+ * <mimi-toggle-group aria-label="Formato" multiple [(value)]="estilos">
184
+ * <button mimiToggleGroupItem="negrita" aria-label="Negrita"><svg lucideBold></svg></button>
185
+ * </mimi-toggle-group>
186
+ * ```
187
+ *
188
+ * Implementa `FormValueControl`: funciona con `[formField]`, `formControlName` y `[(ngModel)]`.
189
+ * Un valor sin botón se conserva (no se cambia por `null`).
190
+ */
191
+ @Component({
192
+ selector: 'mimi-toggle-group',
193
+ template: `
194
+ @if (!multiple()) {
195
+ <span
196
+ aria-hidden="true"
197
+ data-slot="indicator"
198
+ [class]="indicatorClasses()"
199
+ [style.top]="indicatorStyle.top"
200
+ [style.bottom]="indicatorStyle.bottom"
201
+ [style.inset-inline-start]="indicatorStyle.start"
202
+ [style.width]="indicatorStyle.width"
203
+ [style.transition-timing-function]="indicatorStyle.easing"
204
+ [style.translate]="indicatorTranslate()"
205
+ [attr.data-variant]="variant()"
206
+ [hidden]="paintedIndex() < 0"
207
+ ></span>
208
+ }
209
+ <ng-content />
210
+ `,
211
+ host: {
212
+ '[attr.role]': 'multiple() ? "group" : "radiogroup"',
213
+ '[attr.aria-label]': 'ariaLabel() || null',
214
+ '[attr.aria-labelledby]': 'ariaLabelledby() || null',
215
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
216
+ '[attr.aria-invalid]': 'showInvalid() ? "true" : null',
217
+ '[attr.aria-required]': 'required() && !multiple() ? "true" : null',
218
+ '[attr.data-variant]': 'variant()',
219
+ '[attr.data-multiple]': 'multiple() ? "" : null',
220
+ '[attr.data-disabled]': 'disabled() ? "" : null',
221
+ '[attr.data-invalid]': 'showInvalid() ? "" : null',
222
+ '[style.--mimi-toggle-group-count]': 'items().length',
223
+ '[style.--mimi-toggle-group-index]': 'paintedIndex()',
224
+ '[class]': 'classes()',
225
+ '(keydown)': 'onKeydown($event)',
226
+ '(focusin)': 'onFocusIn($event)',
227
+ '(focusout)': 'onFocusOut($event)',
228
+ },
229
+ changeDetection: ChangeDetectionStrategy.OnPush,
230
+ })
231
+ export class MimiToggleGroup<T = unknown> implements FormValueControl<T | null | T[]> {
232
+ private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
233
+
234
+ /** El elegido (`T | null`) o, con `multiple`, los elegidos (`T[]`). Admite `[(value)]`. */
235
+ readonly value = model<T | null | T[]>(null);
236
+ /** Varios elegidos a la vez (botones de alternancia). Sin él, uno solo (como una radio). */
237
+ readonly multiple = input(false, { transform: booleanAttribute });
238
+ /** `solid`: el elegido en `primary`. */
239
+ readonly variant = input<MimiToggleGroupVariant>('default');
240
+ readonly disabled = input(false, { transform: booleanAttribute });
241
+ /** Los pasa el formulario. Se muestra el error si es inválido y se tocó o modificó. */
242
+ readonly invalid = input(false, { transform: booleanAttribute });
243
+ readonly touched = input(false, { transform: booleanAttribute });
244
+ readonly dirty = input(false, { transform: booleanAttribute });
245
+ readonly required = input(false, { transform: booleanAttribute });
246
+ /** Nombre accesible (obligatorio, o `aria-labelledby`). */
247
+ readonly ariaLabel = input<string>(undefined, { alias: 'aria-label' });
248
+ readonly ariaLabelledby = input<string>(undefined, { alias: 'aria-labelledby' });
249
+ /** Clases del grupo, mezcladas con `cn()` (gana la del usuario). */
250
+ readonly userClass = input('', { alias: 'class' });
251
+
252
+ /** Se emite cuando el foco sale del grupo entero: marca el control como tocado. */
253
+ readonly touch = output<void>();
254
+
255
+ private readonly found = contentChildren(MimiToggleGroupItem, { descendants: true });
256
+ /** En el orden del documento. 4 es `Node.DOCUMENT_POSITION_FOLLOWING` (sin `Node` en el servidor). */
257
+ protected readonly items = computed(() =>
258
+ [...this.found()].sort((a, b) => (a.element.compareDocumentPosition(b.element) & 4 ? -1 : 1)),
259
+ );
260
+
261
+ private readonly dir = inject(Directionality).valueSignal;
262
+ private readonly manager = new FocusKeyManager(this.items, inject(Injector))
263
+ .withWrap()
264
+ .withVerticalOrientation(true);
265
+ /** El último botón enfocado (por teclado, clic o código). */
266
+ private readonly active = signal<MimiToggleGroupItem | null>(null);
267
+
268
+ /** Los elegidos, como arreglo en los dos modos. */
269
+ private readonly selected = computed<readonly unknown[]>(() => {
270
+ const value = this.value();
271
+ if (this.multiple()) return Array.isArray(value) ? value : [];
272
+ return value === null || value === undefined || Array.isArray(value) ? [] : [value];
273
+ });
274
+ /** Índice del elegido (simple), para el indicador; −1 sin elegido o con un valor sin botón. */
275
+ protected readonly paintedIndex = computed(() => {
276
+ const [value] = this.selected();
277
+ return this.selected().length ? this.items().findIndex((i) => Object.is(i.value(), value)) : -1;
278
+ });
279
+ /** La parada de Tab en simple: el elegido; si no hay, el último enfocado o el primero. */
280
+ private readonly tabStop = computed(() => {
281
+ const items = this.items();
282
+ const index = this.paintedIndex();
283
+ if (index >= 0) return items[index];
284
+ const active = this.active();
285
+ return active && items.includes(active) ? active : items[0];
286
+ });
287
+ /** Error visible (inválido y tocado o modificado). */
288
+ protected readonly showInvalid = computed(
289
+ () => this.invalid() && (this.touched() || this.dirty()),
290
+ );
291
+
292
+ protected readonly indicatorClasses = computed(() =>
293
+ cn(INDICATOR_CLASSES, SELECTED_SURFACE[this.variant()]),
294
+ );
295
+ // Bindings `[style.x]` sueltas: un `[style]` de objeto traería otra instrucción de
296
+ // @angular/core al JS inicial del showcase (spec 10).
297
+ protected readonly indicatorStyle = INDICATOR_STYLE;
298
+ protected readonly indicatorTranslate = computed(
299
+ () =>
300
+ `calc(${this.dir() === 'rtl' ? -1 : 1} * var(--mimi-toggle-group-index) * (100% + ${GAP})) 0`,
301
+ );
302
+ protected readonly classes = computed(() =>
303
+ cn(groupVariants({ multiple: this.multiple() }), this.userClass()),
304
+ );
305
+
306
+ constructor() {
307
+ effect(() => this.manager.withHorizontalOrientation(this.dir()));
308
+ inject(DestroyRef).onDestroy(() => this.manager.destroy());
309
+ if (isDevMode()) afterRenderEffect({ read: () => this.warn(this.items(), this.value()) });
310
+ }
311
+
312
+ /** Uso interno: ¿está elegido este valor? */
313
+ isOn(value: unknown): boolean {
314
+ return this.selected().some((v) => Object.is(v, value));
315
+ }
316
+
317
+ /** Uso interno: ¿lleva `tabindex="0"` (simple)? */
318
+ isTabStop(item: MimiToggleGroupItem): boolean {
319
+ return this.tabStop() === item;
320
+ }
321
+
322
+ /** Uso interno: el clic (y Espacio o Enter, que el navegador convierte en clic). */
323
+ select(item: MimiToggleGroupItem): void {
324
+ if (this.disabled()) return;
325
+ const value = item.value() as T;
326
+ if (this.multiple()) {
327
+ const current = this.selected() as T[];
328
+ this.value.set(
329
+ current.some((v) => Object.is(v, value))
330
+ ? current.filter((v) => !Object.is(v, value))
331
+ : [...current, value],
332
+ );
333
+ return;
334
+ }
335
+ // Como una radio: volver a pulsar la elegida no la quita (decidido por defecto, G2.1).
336
+ if (!this.isOn(value)) this.value.set(value);
337
+ }
338
+
339
+ /** Simple: las flechas mueven el foco y eligen (con vuelta; en RTL, ← y → al revés). */
340
+ protected onKeydown(event: KeyboardEvent): void {
341
+ if (this.multiple() || this.disabled()) return;
342
+ if (!this.items().some((item) => item.element === event.target)) return;
343
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
344
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
345
+ this.manager.onKeydown(event);
346
+ const item = this.manager.activeItem;
347
+ if (item) {
348
+ this.active.set(item);
349
+ this.select(item);
350
+ }
351
+ }
352
+
353
+ /** El foco llegó a un botón (Tab, clic o código): el gestor sigue desde ahí. */
354
+ protected onFocusIn(event: FocusEvent): void {
355
+ const item = this.items().find((i) => i.element === event.target);
356
+ if (!item) return;
357
+ this.manager.updateActiveItem(item);
358
+ this.active.set(item);
359
+ }
360
+
361
+ /** El foco salió del grupo entero. */
362
+ protected onFocusOut(event: FocusEvent): void {
363
+ const next = event.relatedTarget as Node | null;
364
+ if (next && this.host.contains(next)) return;
365
+ this.touch.emit();
366
+ }
367
+
368
+ private warned = false;
369
+ private warn(items: readonly MimiToggleGroupItem[], value: unknown): void {
370
+ if (this.warned) return;
371
+ const multiple = untracked(this.multiple);
372
+ const values = items.map((item) => untracked(item.value));
373
+ const problems = [
374
+ !this.host.getAttribute('aria-label')?.trim() &&
375
+ !this.host.getAttribute('aria-labelledby')?.trim() &&
376
+ 'el grupo no tiene aria-label (su nombre accesible)',
377
+ values.some((v, i) => values.findIndex((w) => Object.is(v, w)) !== i) &&
378
+ 'hay valores repetidos',
379
+ items.some((item) => !accessibleName(item.element)) &&
380
+ 'un botón sin texto visible no tiene aria-label',
381
+ multiple &&
382
+ value !== null &&
383
+ !Array.isArray(value) &&
384
+ 'con multiple, value tiene que ser un arreglo',
385
+ !multiple && Array.isArray(value) && 'sin multiple, value no puede ser un arreglo',
386
+ ].filter(Boolean);
387
+ if (!problems.length) return;
388
+ this.warned = true;
389
+ console.warn(`[Mimi] Toggle Group: ${problems.join('; ')}.`);
390
+ }
391
+ }
392
+
393
+ /** Todas las piezas del Toggle Group, para importarlas juntas. */
394
+ export const MimiToggleGroupImports = [MimiToggleGroup, MimiToggleGroupItem] as const;
395
+
396
+ /** Nombre del botón: aria-labelledby, aria-label o el texto visible. */
397
+ function accessibleName(button: HTMLElement): string {
398
+ const label = button.getAttribute('aria-label') ?? button.getAttribute('aria-labelledby');
399
+ return label?.trim() || visibleText(button).trim();
400
+ }
401
+
402
+ function visibleText(node: Node): string {
403
+ if (node.nodeType === 3) return node.textContent ?? '';
404
+ if (!(node instanceof Element) || node.getAttribute('aria-hidden') === 'true') return '';
405
+ return Array.from(node.childNodes).map(visibleText).join('');
406
+ }
@@ -88,6 +88,19 @@ const COMPONENT_PREFIX: Record<keyof MimiComponentTokens, string> = {
88
88
  tabs: 'tabs',
89
89
  toolbar: 'toolbar',
90
90
  pagination: 'pagination',
91
+ toggleGroup: 'toggle-group',
92
+ progress: 'progress',
93
+ numberInput: 'number',
94
+ password: 'password',
95
+ inputMask: 'mask',
96
+ inputOtp: 'otp',
97
+ chip: 'chip',
98
+ tagsInput: 'tags',
99
+ slider: 'slider',
100
+ combobox: 'combobox',
101
+ date: 'date',
102
+ fileUpload: 'upload',
103
+ colorPicker: 'color-picker',
91
104
  };
92
105
 
93
106
  /** Nombres de la 0.1.0 que siguen funcionando hasta la 1.0 (spec 12): ruta → qué usar. */
@@ -214,6 +214,22 @@
214
214
  --mimi-destructive-border: color-mix(in oklch, var(--mimi-destructive) 55%, transparent);
215
215
  --mimi-destructive-subtle: color-mix(in oklch, var(--mimi-destructive) 10%, transparent);
216
216
 
217
+ /* Entradas avanzadas, Grupo 2 (tokens-mimi.css; G2.0b). Los componentes los leen con valor
218
+ de respaldo, var(--mimi-track, …), para funcionar con un tema anterior a la 0.3.0. */
219
+ --mimi-range: color-mix(in oklab, var(--mimi-primary) 9%, var(--mimi-background));
220
+ --mimi-track: color-mix(in oklab, var(--mimi-foreground) 12%, var(--mimi-background));
221
+ --mimi-track-fill: var(--mimi-primary);
222
+ --mimi-thumb: oklch(1 0 0);
223
+ --mimi-chip: var(--mimi-secondary);
224
+ --mimi-chip-foreground: var(--mimi-secondary-foreground);
225
+ --mimi-dropzone: color-mix(in oklab, var(--mimi-muted) 55%, var(--mimi-background));
226
+ --mimi-dropzone-active: var(--mimi-info-soft);
227
+ --mimi-dropzone-active-border: var(--mimi-info);
228
+ --mimi-strength-weak: var(--mimi-destructive);
229
+ --mimi-strength-medium: var(--mimi-warning);
230
+ --mimi-strength-strong: var(--mimi-success);
231
+ --mimi-selection: var(--mimi-info);
232
+
217
233
  color-scheme: light;
218
234
  }
219
235
 
@@ -385,6 +401,21 @@
385
401
  --mimi-destructive-border: color-mix(in oklch, var(--mimi-destructive) 55%, transparent);
386
402
  --mimi-destructive-subtle: color-mix(in oklch, var(--mimi-destructive) 10%, transparent);
387
403
 
404
+ /* Entradas avanzadas: se repiten las que mezclan otros tokens, por si .dark no está en <html>. */
405
+ --mimi-range: color-mix(in oklab, var(--mimi-primary) 14%, var(--mimi-background));
406
+ --mimi-track: color-mix(in oklab, var(--mimi-foreground) 16%, var(--mimi-background));
407
+ --mimi-track-fill: var(--mimi-primary);
408
+ --mimi-thumb: oklch(0.985 0 0);
409
+ --mimi-chip: var(--mimi-secondary);
410
+ --mimi-chip-foreground: var(--mimi-secondary-foreground);
411
+ --mimi-dropzone: color-mix(in oklab, var(--mimi-muted) 50%, var(--mimi-background));
412
+ --mimi-dropzone-active: var(--mimi-info-soft);
413
+ --mimi-dropzone-active-border: var(--mimi-info);
414
+ --mimi-strength-weak: var(--mimi-destructive);
415
+ --mimi-strength-medium: var(--mimi-warning);
416
+ --mimi-strength-strong: var(--mimi-success);
417
+ --mimi-selection: var(--mimi-info);
418
+
388
419
  color-scheme: dark;
389
420
  }
390
421
 
@@ -469,6 +500,19 @@
469
500
  --color-ring-soft: var(--mimi-ring-soft);
470
501
  --color-overlay: var(--mimi-overlay);
471
502
  --color-switch-off: var(--mimi-switch-off);
503
+ --color-range: var(--mimi-range);
504
+ --color-track: var(--mimi-track);
505
+ --color-track-fill: var(--mimi-track-fill);
506
+ --color-thumb: var(--mimi-thumb);
507
+ --color-chip: var(--mimi-chip);
508
+ --color-chip-foreground: var(--mimi-chip-foreground);
509
+ --color-dropzone: var(--mimi-dropzone);
510
+ --color-dropzone-active: var(--mimi-dropzone-active);
511
+ --color-dropzone-active-border: var(--mimi-dropzone-active-border);
512
+ --color-strength-weak: var(--mimi-strength-weak);
513
+ --color-strength-medium: var(--mimi-strength-medium);
514
+ --color-strength-strong: var(--mimi-strength-strong);
515
+ --color-selection: var(--mimi-selection);
472
516
 
473
517
  --radius-sm: var(--mimi-radius-sm);
474
518
  --radius-md: calc(var(--mimi-radius) - 2px);
@@ -526,6 +570,32 @@
526
570
  }
527
571
  }
528
572
 
573
+ /* Texto solo para lectores de pantalla del CDK: el LiveAnnouncer (Pagination, Toast, Password,
574
+ File Upload, Tags Input y Combobox múltiple) crea al final del body un elemento con esta clase,
575
+ pero no carga su CSS (el CDK solo lo inyecta con Dialog y las trampas de foco): sin esta
576
+ regla, el anuncio («Página 2 de 12») se ve al final de la página. Igual que
577
+ @angular/cdk/a11y-prebuilt.css, y sin capa, como la inyecta el CDK. */
578
+ .cdk-visually-hidden {
579
+ border: 0;
580
+ clip: rect(0 0 0 0);
581
+ height: 1px;
582
+ margin: -1px;
583
+ overflow: hidden;
584
+ padding: 0;
585
+ position: absolute;
586
+ width: 1px;
587
+ white-space: nowrap;
588
+ outline: 0;
589
+ -webkit-appearance: none;
590
+ -moz-appearance: none;
591
+ left: 0;
592
+ }
593
+
594
+ [dir='rtl'] .cdk-visually-hidden {
595
+ left: auto;
596
+ right: 0;
597
+ }
598
+
529
599
  @keyframes mimi-spin {
530
600
  to {
531
601
  transform: rotate(360deg);
@@ -537,3 +607,36 @@
537
607
  opacity: 0.45;
538
608
  }
539
609
  }
610
+
611
+ /* Grupo 2 (tokens-mimi.css y, mimi-indet, F5 Estructura; G2.0b) */
612
+ @keyframes mimi-caret {
613
+ 50% {
614
+ opacity: 0;
615
+ }
616
+ }
617
+
618
+ @keyframes mimi-bob {
619
+ 50% {
620
+ transform: translateY(-4px);
621
+ }
622
+ }
623
+
624
+ @keyframes mimi-shake {
625
+ 20%,
626
+ 60% {
627
+ transform: translateX(-4px);
628
+ }
629
+ 40%,
630
+ 80% {
631
+ transform: translateX(4px);
632
+ }
633
+ }
634
+
635
+ @keyframes mimi-indet {
636
+ 0% {
637
+ transform: translateX(-100%);
638
+ }
639
+ 100% {
640
+ transform: translateX(260%);
641
+ }
642
+ }