@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.
Files changed (104) hide show
  1. package/README.md +28 -1
  2. package/dist/init/css.js +17 -0
  3. package/dist/init/index.js +7 -5
  4. package/dist/init/items.js +9 -0
  5. package/dist/registry.json +482 -1
  6. package/dist/templates/components/avatar/avatar.ts +1 -1
  7. package/dist/templates/components/avatar/avatar.variants.ts +4 -4
  8. package/dist/templates/components/badge/badge.ts +41 -6
  9. package/dist/templates/components/badge/badge.variants.ts +105 -11
  10. package/dist/templates/components/button/button.ts +74 -7
  11. package/dist/templates/components/button/button.variants.ts +145 -21
  12. package/dist/templates/components/calendar/calendar-texts.ts +72 -0
  13. package/dist/templates/components/calendar/calendar.ts +588 -0
  14. package/dist/templates/components/calendar/dates.ts +181 -0
  15. package/dist/templates/components/calendar/index.ts +3 -0
  16. package/dist/templates/components/card/card.ts +8 -5
  17. package/dist/templates/components/checkbox/checkbox.ts +3 -3
  18. package/dist/templates/components/chip/chip-texts.ts +40 -0
  19. package/dist/templates/components/chip/chip.ts +116 -0
  20. package/dist/templates/components/chip/index.ts +2 -0
  21. package/dist/templates/components/color-picker/color-picker-texts.ts +57 -0
  22. package/dist/templates/components/color-picker/color-picker.ts +718 -0
  23. package/dist/templates/components/color-picker/color.ts +157 -0
  24. package/dist/templates/components/color-picker/index.ts +3 -0
  25. package/dist/templates/components/combobox/combobox-texts.ts +70 -0
  26. package/dist/templates/components/combobox/combobox.ts +798 -0
  27. package/dist/templates/components/combobox/index.ts +2 -0
  28. package/dist/templates/components/confirm/confirm-texts.ts +46 -0
  29. package/dist/templates/components/confirm/confirm.ts +190 -0
  30. package/dist/templates/components/confirm/index.ts +2 -0
  31. package/dist/templates/components/context-menu/context-menu.ts +378 -0
  32. package/dist/templates/components/context-menu/index.ts +14 -0
  33. package/dist/templates/components/date-picker/date-picker.ts +240 -0
  34. package/dist/templates/components/date-picker/index.ts +3 -0
  35. package/dist/templates/components/date-range/date-range.ts +308 -0
  36. package/dist/templates/components/date-range/index.ts +1 -0
  37. package/dist/templates/components/dialog/dialog-texts.ts +41 -0
  38. package/dist/templates/components/dialog/dialog.ts +701 -0
  39. package/dist/templates/components/dialog/index.ts +18 -0
  40. package/dist/templates/components/dropdown-menu/dropdown-menu.ts +252 -0
  41. package/dist/templates/components/dropdown-menu/index.ts +14 -0
  42. package/dist/templates/components/file-upload/file-upload-texts.ts +77 -0
  43. package/dist/templates/components/file-upload/file-upload.ts +698 -0
  44. package/dist/templates/components/file-upload/index.ts +2 -0
  45. package/dist/templates/components/form-field/form-error.ts +1 -1
  46. package/dist/templates/components/form-field/form-field.ts +3 -2
  47. package/dist/templates/components/input/input.variants.ts +6 -4
  48. package/dist/templates/components/input-mask/index.ts +1 -0
  49. package/dist/templates/components/input-mask/input-mask.ts +319 -0
  50. package/dist/templates/components/input-otp/index.ts +1 -0
  51. package/dist/templates/components/input-otp/input-otp.ts +212 -0
  52. package/dist/templates/components/menu/index.ts +15 -0
  53. package/dist/templates/components/menu/menu.ts +656 -0
  54. package/dist/templates/components/number-input/index.ts +2 -0
  55. package/dist/templates/components/number-input/number-input-texts.ts +46 -0
  56. package/dist/templates/components/number-input/number-input.ts +450 -0
  57. package/dist/templates/components/option-list/index.ts +11 -0
  58. package/dist/templates/components/option-list/option-list.ts +518 -0
  59. package/dist/templates/components/pagination/index.ts +2 -0
  60. package/dist/templates/components/pagination/pagination-page-size.ts +74 -0
  61. package/dist/templates/components/pagination/pagination-texts.ts +69 -0
  62. package/dist/templates/components/pagination/pagination.ts +443 -0
  63. package/dist/templates/components/password/index.ts +2 -0
  64. package/dist/templates/components/password/password-texts.ts +68 -0
  65. package/dist/templates/components/password/password.ts +363 -0
  66. package/dist/templates/components/popover/index.ts +12 -0
  67. package/dist/templates/components/popover/popover.ts +407 -0
  68. package/dist/templates/components/progress/index.ts +1 -0
  69. package/dist/templates/components/progress/progress.ts +217 -0
  70. package/dist/templates/components/select/index.ts +12 -0
  71. package/dist/templates/components/select/select-texts.ts +62 -0
  72. package/dist/templates/components/select/select.ts +1086 -0
  73. package/dist/templates/components/separator/separator.variants.ts +10 -7
  74. package/dist/templates/components/skeleton/skeleton.ts +4 -1
  75. package/dist/templates/components/slider/index.ts +2 -0
  76. package/dist/templates/components/slider/slider-texts.ts +26 -0
  77. package/dist/templates/components/slider/slider.ts +425 -0
  78. package/dist/templates/components/switch/switch.ts +6 -6
  79. package/dist/templates/components/tabs/index.ts +2 -0
  80. package/dist/templates/components/tabs/tabs.ts +426 -0
  81. package/dist/templates/components/tags-input/index.ts +2 -0
  82. package/dist/templates/components/tags-input/tags-input-texts.ts +44 -0
  83. package/dist/templates/components/tags-input/tags-input.ts +303 -0
  84. package/dist/templates/components/textarea/textarea.variants.ts +14 -11
  85. package/dist/templates/components/toast/index.ts +3 -0
  86. package/dist/templates/components/toast/toast-config.ts +45 -0
  87. package/dist/templates/components/toast/toast-texts.ts +46 -0
  88. package/dist/templates/components/toast/toast.ts +1000 -0
  89. package/dist/templates/components/toggle-group/index.ts +1 -0
  90. package/dist/templates/components/toggle-group/toggle-group.ts +406 -0
  91. package/dist/templates/components/toolbar/index.ts +1 -0
  92. package/dist/templates/components/toolbar/toolbar.ts +377 -0
  93. package/dist/templates/components/tooltip/index.ts +7 -0
  94. package/dist/templates/components/tooltip/tooltip.ts +407 -0
  95. package/dist/templates/theme/provider.ts +75 -6
  96. package/dist/templates/theme/theme-base.css +410 -9
  97. package/dist/templates/theme/types.ts +494 -4
  98. package/dist/templates/utils/cn.ts +15 -1
  99. package/dist/templates/utils/control-styles.ts +2 -2
  100. package/dist/templates/utils/overlay.ts +153 -0
  101. package/dist/ui/index.js +184 -8
  102. package/dist/ui/schema.json +100 -0
  103. package/dist/utils/write-files.js +166 -59
  104. package/package.json +1 -1
@@ -0,0 +1,407 @@
1
+ import { CdkConnectedOverlay, type ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
2
+ import { InteractivityChecker } from '@angular/cdk/a11y';
3
+ import {
4
+ ChangeDetectionStrategy,
5
+ Component,
6
+ DOCUMENT,
7
+ DestroyRef,
8
+ Directive,
9
+ ElementRef,
10
+ Injector,
11
+ TemplateRef,
12
+ ViewContainerRef,
13
+ afterNextRender,
14
+ booleanAttribute,
15
+ computed,
16
+ contentChild,
17
+ effect,
18
+ inject,
19
+ input,
20
+ isDevMode,
21
+ model,
22
+ signal,
23
+ untracked,
24
+ viewChild,
25
+ } from '@angular/core';
26
+ import { cn } from '@/components/ui/utils/cn';
27
+ import {
28
+ MIMI_OVERLAY_ANIMATION,
29
+ MIMI_VIEWPORT_MARGIN,
30
+ type MimiOverlayAlign,
31
+ type MimiOverlaySide,
32
+ mimiArrowOffset,
33
+ mimiConnectedPositions,
34
+ mimiPlacedSide,
35
+ mimiWaitForExit,
36
+ } from '@/components/ui/utils/overlay';
37
+
38
+ /*
39
+ * Popover (docs/design/Mimi F2 Overlays.dc.html, «3 · Popover»): panel de 300px, padding 16px,
40
+ * gap 12px, a 12px del trigger, con flecha de 10px. Tokens de la spec 13 (MimiPopoverTokens), con
41
+ * la cascada de la 6.2: --mimi-popover-* → token global → valor del diseño.
42
+ *
43
+ * Por dentro usa @angular/cdk/overlay (decisión 4.3): CdkConnectedOverlay posiciona el panel como
44
+ * popover nativo (top layer) insertado junto al trigger ('inline'), con flip si no cabe. Nada del
45
+ * CDK sale en la API pública. Los estilos del overlay vienen de @angular/cdk/overlay-prebuilt.css,
46
+ * que la CLI agrega al CSS global (spec 4.3). Posición, flecha y animación: utils/overlay.ts.
47
+ */
48
+
49
+ export type MimiPopoverSide = MimiOverlaySide;
50
+ export type MimiPopoverAlign = MimiOverlayAlign;
51
+
52
+ /** Distancia entre el trigger y el panel (diseño: margin-top 12px). */
53
+ const OFFSET = 12;
54
+
55
+ let nextId = 0;
56
+
57
+ /**
58
+ * Uso interno de mimi-popover (no está en MimiPopoverImports; se exporta porque el compilador de
59
+ * Angular lo exige): pinta el contenido del usuario dentro del panel. Hace lo mismo que
60
+ * NgTemplateOutlet, pero sin importar @angular/common: esbuild pondría ese módulo en el chunk
61
+ * inicial del proyecto, aunque Popover sea lazy (spec 10).
62
+ * @internal No está en el índice; no forma parte de la API de Mimi.
63
+ */
64
+ @Directive({ selector: '[mimiPopoverOutlet]' })
65
+ export class MimiPopoverOutlet {
66
+ readonly template = input.required<TemplateRef<unknown>>({ alias: 'mimiPopoverOutlet' });
67
+ private readonly viewContainer = inject(ViewContainerRef);
68
+ private readonly render = effect(() => {
69
+ const template = this.template();
70
+ untracked(() => {
71
+ this.viewContainer.clear();
72
+ this.viewContainer.createEmbeddedView(template);
73
+ });
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Contenedor del popover: un trigger (`[mimiPopoverTrigger]`) y el contenido en un
79
+ * `<ng-template mimiPopoverContent>`. No dibuja nada propio (`display: contents`): su `class` y
80
+ * su `aria-label` van al panel.
81
+ */
82
+ @Component({
83
+ selector: 'mimi-popover',
84
+ imports: [CdkConnectedOverlay, MimiPopoverOutlet],
85
+ template: `
86
+ <ng-content />
87
+ <ng-template
88
+ cdkConnectedOverlay
89
+ [cdkConnectedOverlayOrigin]="originElement()!"
90
+ [cdkConnectedOverlayOpen]="rendered()"
91
+ [cdkConnectedOverlayPositions]="positions()"
92
+ [cdkConnectedOverlayViewportMargin]="viewportMargin"
93
+ [cdkConnectedOverlayDisableClose]="true"
94
+ cdkConnectedOverlayUsePopover="inline"
95
+ cdkConnectedOverlayTransformOriginOn="[data-mimi-overlay-panel]"
96
+ (attach)="onAttach()"
97
+ (detach)="onDetach()"
98
+ (overlayKeydown)="onKeydown($event)"
99
+ (overlayOutsideClick)="close()"
100
+ (positionChange)="onPositionChange($event)"
101
+ >
102
+ <div
103
+ #panel
104
+ data-mimi-overlay-panel
105
+ role="dialog"
106
+ tabindex="-1"
107
+ [id]="panelId"
108
+ [class]="panelClasses()"
109
+ [attr.aria-label]="ariaLabel() || null"
110
+ [attr.aria-labelledby]="ariaLabel() ? null : titleId()"
111
+ [attr.aria-describedby]="descriptionId()"
112
+ [attr.data-state]="state()"
113
+ [attr.data-side]="placedSide()"
114
+ [attr.data-align]="align()"
115
+ [attr.inert]="state() === 'closed' ? '' : null"
116
+ [style.--mimi-popover-arrow-offset]="arrowOffset()"
117
+ >
118
+ @if (arrow()) {
119
+ <span aria-hidden="true" [attr.data-side]="placedSide()" [class]="arrowClasses"></span>
120
+ }
121
+ @if (content(); as content) {
122
+ <ng-container [mimiPopoverOutlet]="content.templateRef" />
123
+ }
124
+ </div>
125
+ </ng-template>
126
+ `,
127
+ styles: MIMI_OVERLAY_ANIMATION,
128
+ host: {
129
+ style: 'display: contents',
130
+ // El class y el aria-label son del panel: en el host no tendrían efecto o se heredarían.
131
+ '[attr.class]': 'null',
132
+ '[attr.aria-label]': 'null',
133
+ '[attr.data-state]': 'open() ? "open" : "closed"',
134
+ '[attr.data-side]': 'side()',
135
+ '[attr.data-align]': 'align()',
136
+ // `align` es un atributo HTML antiguo: el navegador lo convierte en text-align (se heredaría).
137
+ '[attr.align]': 'null',
138
+ },
139
+ changeDetection: ChangeDetectionStrategy.OnPush,
140
+ })
141
+ export class MimiPopover {
142
+ private readonly document = inject(DOCUMENT);
143
+ private readonly injector = inject(Injector);
144
+ private readonly checker = inject(InteractivityChecker);
145
+
146
+ /** Abierto o cerrado. Admite `[(open)]`. */
147
+ readonly open = model(false);
148
+ /** Lado preferido. Si no cabe, pasa al lado opuesto. */
149
+ readonly side = input<MimiPopoverSide>('bottom');
150
+ /** Alineación con el trigger. */
151
+ readonly align = input<MimiPopoverAlign>('center');
152
+ /** Flecha hacia el trigger (decorativa). */
153
+ readonly arrow = input(true, { transform: booleanAttribute });
154
+ /** Nombre accesible del panel (`aria-label`). Si hay `mimiPopoverTitle`, no hace falta. */
155
+ readonly ariaLabel = input<string>(undefined, { alias: 'aria-label' });
156
+ /** Clases del panel, mezcladas con `cn()` (gana la del usuario). */
157
+ readonly userClass = input('', { alias: 'class' });
158
+
159
+ protected readonly trigger = contentChild(MimiPopoverTrigger);
160
+ protected readonly content = contentChild(MimiPopoverContent);
161
+ private readonly panel = viewChild<ElementRef<HTMLElement>>('panel');
162
+
163
+ /** id del panel (aria-controls del trigger). */
164
+ readonly panelId = `mimi-popover-${nextId++}`;
165
+ /** Registrados por `mimiPopoverTitle` y `mimiPopoverDescription`. */
166
+ readonly titleId = signal<string | null>(null);
167
+ readonly descriptionId = signal<string | null>(null);
168
+
169
+ /** El panel está en el DOM (también mientras dura la animación de salida). */
170
+ protected readonly rendered = signal(false);
171
+ protected readonly state = signal<'open' | 'closed'>('closed');
172
+ protected readonly placedSide = signal<MimiPopoverSide>('bottom');
173
+ protected readonly arrowOffset = signal<string | null>(null);
174
+ protected readonly viewportMargin = MIMI_VIEWPORT_MARGIN;
175
+
176
+ protected readonly originElement = computed(() => this.trigger()?.element ?? null);
177
+
178
+ /** Lado y alineación pedidos, el lado opuesto y, por último, los otros dos lados. */
179
+ protected readonly positions = computed(() =>
180
+ mimiConnectedPositions(this.side(), this.align(), OFFSET),
181
+ );
182
+
183
+ protected readonly panelClasses = computed(() =>
184
+ cn(
185
+ 'relative flex w-[300px] max-w-[calc(100vw_-_16px)] flex-col gap-3 outline-none',
186
+ 'rounded-[var(--mimi-popover-radius,var(--mimi-radius))] border border-[color:var(--mimi-popover-border,var(--mimi-border))]',
187
+ 'bg-[color:var(--mimi-popover-bg,var(--mimi-popover))] text-popover-foreground',
188
+ 'p-[var(--mimi-popover-padding,16px)] shadow-[shadow:var(--mimi-popover-shadow,var(--mimi-shadow-popover))]',
189
+ this.userClass(),
190
+ ),
191
+ );
192
+
193
+ protected readonly arrowClasses = cn(
194
+ 'pointer-events-none absolute size-[var(--mimi-popover-arrow,10px)] rounded-tl-[2px] border-t border-l',
195
+ 'border-[color:var(--mimi-popover-border,var(--mimi-border))] bg-[color:var(--mimi-popover-bg,var(--mimi-popover))]',
196
+ 'data-[side=bottom]:top-[calc(var(--mimi-popover-arrow,10px)/-2_-_1px)] data-[side=bottom]:left-[var(--mimi-popover-arrow-offset,50%)] data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:rotate-45',
197
+ 'data-[side=top]:bottom-[calc(var(--mimi-popover-arrow,10px)/-2_-_1px)] data-[side=top]:left-[var(--mimi-popover-arrow-offset,50%)] data-[side=top]:-translate-x-1/2 data-[side=top]:rotate-[225deg]',
198
+ 'data-[side=right]:left-[calc(var(--mimi-popover-arrow,10px)/-2_-_1px)] data-[side=right]:top-[var(--mimi-popover-arrow-offset,50%)] data-[side=right]:-translate-y-1/2 data-[side=right]:-rotate-45',
199
+ 'data-[side=left]:right-[calc(var(--mimi-popover-arrow,10px)/-2_-_1px)] data-[side=left]:top-[var(--mimi-popover-arrow-offset,50%)] data-[side=left]:-translate-y-1/2 data-[side=left]:rotate-[135deg]',
200
+ );
201
+
202
+ /** Cuenta los cierres: si se vuelve a abrir durante la animación de salida, no se desmonta. */
203
+ private closing = 0;
204
+ private warnedLabel = false;
205
+
206
+ private readonly syncOpen = effect(() => {
207
+ const open = this.open();
208
+ untracked(() => (open ? this.show() : this.hide()));
209
+ });
210
+
211
+ /** Abre o cierra (lo usa el trigger). */
212
+ toggle(): void {
213
+ this.open.update((open) => !open);
214
+ }
215
+
216
+ /** Cierra. El foco vuelve al trigger si estaba dentro del panel o en ningún lado. */
217
+ close(): void {
218
+ this.open.set(false);
219
+ }
220
+
221
+ private show(): void {
222
+ this.closing++;
223
+ this.placedSide.set(this.side());
224
+ this.state.set('open');
225
+ if (this.rendered()) {
226
+ // Se reabrió durante la salida: el panel sigue montado, así que no habrá (attach).
227
+ this.focusPanel();
228
+ } else {
229
+ this.rendered.set(true);
230
+ }
231
+ }
232
+
233
+ private hide(): void {
234
+ if (!this.rendered()) return;
235
+ this.state.set('closed');
236
+ const panel = this.panel()?.nativeElement;
237
+ const active = this.document.activeElement;
238
+ if (!active || active === this.document.body || panel?.contains(active)) {
239
+ this.trigger()?.element.focus();
240
+ }
241
+ const closing = ++this.closing;
242
+ afterNextRender(
243
+ {
244
+ read: () => {
245
+ // Espera la animación de salida (sin animación, como con movimiento reducido, no hay).
246
+ void mimiWaitForExit(panel).then(() => {
247
+ if (closing === this.closing) this.rendered.set(false);
248
+ });
249
+ },
250
+ },
251
+ { injector: this.injector },
252
+ );
253
+ }
254
+
255
+ protected onAttach(): void {
256
+ this.focusPanel();
257
+ }
258
+
259
+ protected onDetach(): void {
260
+ // El CDK lo desmontó por su cuenta (p. ej. al destruirse): se sincroniza el estado.
261
+ this.rendered.set(false);
262
+ if (this.open()) this.open.set(false);
263
+ }
264
+
265
+ protected onKeydown(event: KeyboardEvent): void {
266
+ if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
267
+ event.preventDefault();
268
+ this.close();
269
+ }
270
+ }
271
+
272
+ protected onPositionChange({ connectionPair }: ConnectedOverlayPositionChange): void {
273
+ const side = mimiPlacedSide(connectionPair);
274
+ this.placedSide.set(side);
275
+ // La flecha apunta al centro del trigger.
276
+ const trigger = this.trigger()?.element.getBoundingClientRect();
277
+ const panel = this.panel()?.nativeElement.getBoundingClientRect();
278
+ this.arrowOffset.set(trigger && panel ? mimiArrowOffset(trigger, panel, side) : null);
279
+ }
280
+
281
+ /**
282
+ * Al abrir, el foco va al elemento con `autofocus` del panel (el día elegido del calendario,
283
+ * G2.9) o, si no hay, al primer elemento enfocable o al panel.
284
+ */
285
+ private focusPanel(): void {
286
+ afterNextRender(
287
+ {
288
+ write: () => {
289
+ const panel = this.panel()?.nativeElement;
290
+ if (!panel || this.state() !== 'open') return;
291
+ this.warnMissingLabel();
292
+ const preferred = panel.querySelector<HTMLElement>('[autofocus]');
293
+ const first =
294
+ preferred && this.checker.isFocusable(preferred)
295
+ ? preferred
296
+ : Array.from(panel.querySelectorAll<HTMLElement>('*')).find(
297
+ (element) =>
298
+ this.checker.isFocusable(element) && this.checker.isTabbable(element),
299
+ );
300
+ (first ?? panel).focus();
301
+ },
302
+ },
303
+ { injector: this.injector },
304
+ );
305
+ }
306
+
307
+ private warnMissingLabel(): void {
308
+ if (!isDevMode() || this.warnedLabel) return;
309
+ if (this.ariaLabel() || this.titleId()) return;
310
+ this.warnedLabel = true;
311
+ console.warn(
312
+ '[Mimi] Popover: el panel no tiene nombre accesible. Agrega aria-label="…" al ' +
313
+ '<mimi-popover> o marca su título con mimiPopoverTitle.',
314
+ );
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Botón que abre y cierra el popover: `<button mimiBtn mimiPopoverTrigger>`. Lleva
320
+ * `aria-haspopup="dialog"`, `aria-expanded` y, abierto, `aria-controls`.
321
+ */
322
+ @Directive({
323
+ selector: '[mimiPopoverTrigger]',
324
+ host: {
325
+ 'aria-haspopup': 'dialog',
326
+ '[attr.aria-expanded]': 'popover.open()',
327
+ '[attr.aria-controls]': 'popover.open() ? popover.panelId : null',
328
+ '[attr.data-state]': 'popover.open() ? "open" : "closed"',
329
+ '(click)': 'popover.toggle()',
330
+ },
331
+ })
332
+ export class MimiPopoverTrigger {
333
+ protected readonly popover = inject(MimiPopover);
334
+ readonly element = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
335
+ }
336
+
337
+ /**
338
+ * Contenido del popover: `<ng-template mimiPopoverContent>`. Se crea al abrir y se destruye al
339
+ * cerrar. El `class` y el `label` del panel van en `<mimi-popover>`: Angular no pasa un `class`
340
+ * estático de un ng-template a la directiva.
341
+ */
342
+ @Directive({ selector: 'ng-template[mimiPopoverContent]' })
343
+ export class MimiPopoverContent {
344
+ readonly templateRef = inject(TemplateRef);
345
+ }
346
+
347
+ /** Título del panel: le da su nombre accesible (aria-labelledby). */
348
+ @Directive({
349
+ selector: '[mimiPopoverTitle]',
350
+ host: { '[id]': 'id', '[class]': 'classes()' },
351
+ })
352
+ export class MimiPopoverTitle {
353
+ private readonly popover = inject(MimiPopover);
354
+ private readonly destroyRef = inject(DestroyRef);
355
+ readonly id = `mimi-popover-title-${nextId++}`;
356
+ readonly userClass = input('', { alias: 'class' });
357
+ protected readonly classes = computed(() =>
358
+ cn('text-[15px] leading-tight font-bold', this.userClass()),
359
+ );
360
+
361
+ constructor() {
362
+ this.popover.titleId.set(this.id);
363
+ this.destroyRef.onDestroy(() => {
364
+ if (this.popover.titleId() === this.id) this.popover.titleId.set(null);
365
+ });
366
+ }
367
+ }
368
+
369
+ /** Descripción del panel (aria-describedby). */
370
+ @Directive({
371
+ selector: '[mimiPopoverDescription]',
372
+ host: { '[id]': 'id', '[class]': 'classes()' },
373
+ })
374
+ export class MimiPopoverDescription {
375
+ private readonly popover = inject(MimiPopover);
376
+ private readonly destroyRef = inject(DestroyRef);
377
+ readonly id = `mimi-popover-description-${nextId++}`;
378
+ readonly userClass = input('', { alias: 'class' });
379
+ protected readonly classes = computed(() =>
380
+ cn('text-[13px] text-muted-foreground', this.userClass()),
381
+ );
382
+
383
+ constructor() {
384
+ this.popover.descriptionId.set(this.id);
385
+ this.destroyRef.onDestroy(() => {
386
+ if (this.popover.descriptionId() === this.id) this.popover.descriptionId.set(null);
387
+ });
388
+ }
389
+ }
390
+
391
+ /** Cierra el popover al hacer clic (p. ej. «Cancelar» o «Enviar»). El foco vuelve al trigger. */
392
+ @Directive({
393
+ selector: '[mimiPopoverClose]',
394
+ host: { '(click)': 'popover.close()' },
395
+ })
396
+ export class MimiPopoverClose {
397
+ protected readonly popover = inject(MimiPopover);
398
+ }
399
+
400
+ export const MimiPopoverImports = [
401
+ MimiPopover,
402
+ MimiPopoverTrigger,
403
+ MimiPopoverContent,
404
+ MimiPopoverTitle,
405
+ MimiPopoverDescription,
406
+ MimiPopoverClose,
407
+ ] as const;
@@ -0,0 +1 @@
1
+ export * from './progress';
@@ -0,0 +1,217 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ ElementRef,
5
+ afterRenderEffect,
6
+ computed,
7
+ inject,
8
+ input,
9
+ isDevMode,
10
+ numberAttribute,
11
+ } from '@angular/core';
12
+ import { cn } from '@/components/ui/utils/cn';
13
+
14
+ /*
15
+ * Progress (docs/design/Mimi F5 Estructura.dc.html, «4 · Progress»; prefijo de tokens
16
+ * `progress`, spec 13): barra o círculo, determinado o indeterminado.
17
+ *
18
+ * Rol `progressbar` de WAI-ARIA en el propio elemento (la APG no tiene un patrón propio y
19
+ * @angular/aria no trae uno). No recibe el foco y no anuncia nada por su cuenta: el valor se lee
20
+ * cuando el lector llega a la barra (`aria-valuenow`); el final de la tarea lo avisa la app
21
+ * (decidido el 03/11/2026, plan G2.10).
22
+ */
23
+
24
+ /** Tonos del diseño (spec 4, «Variante y tono»; el diseño escribe `destructive`). */
25
+ export type MimiProgressTone = 'primary' | 'success' | 'warning' | 'info' | 'danger';
26
+ export type MimiProgressShape = 'bar' | 'circle';
27
+ /** Solo el círculo: 76px (`default`) o 44px (`sm`), los del diseño. */
28
+ export type MimiProgressSize = 'default' | 'sm';
29
+
30
+ /*
31
+ * Pista: el token del componente, el global `--mimi-track` y, para un tema anterior a la 0.3.0
32
+ * que no lo tenga, `--mimi-input` (G2.0b; spec 6.2): casi el mismo gris en claro y en oscuro, y
33
+ * sin el `color-mix` de la fórmula, que Lightning CSS duplica en el CSS global. Escrita entera en
34
+ * cada clase (Tailwind solo ve clases literales).
35
+ */
36
+
37
+ /** Color de la parte llena por tono: el token solo cambia el primario (los tonos son globales). */
38
+ const FILL_BG: Record<MimiProgressTone, string> = {
39
+ primary: 'bg-[color:var(--mimi-progress-fill,var(--mimi-primary))]',
40
+ success: 'bg-success',
41
+ warning: 'bg-warning',
42
+ info: 'bg-info',
43
+ danger: 'bg-destructive',
44
+ };
45
+ const FILL_STROKE: Record<MimiProgressTone, string> = {
46
+ primary: 'stroke-[color:var(--mimi-progress-fill,var(--mimi-primary))]',
47
+ success: 'stroke-success',
48
+ warning: 'stroke-warning',
49
+ info: 'stroke-info',
50
+ danger: 'stroke-destructive',
51
+ };
52
+
53
+ const BAR_CLASSES =
54
+ 'relative block w-full h-[var(--mimi-progress-height,8px)] overflow-hidden rounded-full bg-[color:var(--mimi-progress-track,var(--mimi-track,var(--mimi-input)))]';
55
+ const CIRCLE_CLASSES: Record<MimiProgressSize, string> = {
56
+ default:
57
+ 'relative inline-grid shrink-0 place-items-center size-[var(--mimi-progress-circle-size,76px)] text-[16px]',
58
+ // 44px, trazo 4 y texto de 11px: el pequeño del diseño, sin token propio (spec 13).
59
+ sm: 'relative inline-grid shrink-0 place-items-center size-11 text-[11px]',
60
+ };
61
+
62
+ /** Barra determinada: el ancho anima 0,35 s con la curva del diseño. */
63
+ const BAR_FILL =
64
+ 'block h-full rounded-full [transition:width_.35s_cubic-bezier(.2,.8,.2,1)] motion-reduce:transition-none';
65
+ /**
66
+ * Barra indeterminada: un tramo del 40 % que cruza (`mimi-indet`). Con movimiento reducido, la
67
+ * barra entera late (la nota del diseño: «late en lugar de desplazarse»).
68
+ */
69
+ const BAR_INDETERMINATE =
70
+ 'absolute inset-y-0 start-0 w-2/5 rounded-full animate-[mimi-indet_1.4s_cubic-bezier(.4,0,.2,1)_infinite] rtl:-scale-x-100 motion-reduce:w-full motion-reduce:animate-mimi-pulse';
71
+
72
+ /** Arco del círculo: el trazo anima 0,4 s; indeterminado gira (con movimiento reducido, late). */
73
+ const ARC =
74
+ 'fill-none [stroke-linecap:round] [transition:stroke-dasharray_.4s_cubic-bezier(.2,.8,.2,1)] motion-reduce:transition-none';
75
+ const SPIN = 'animate-[mimi-spin_1s_linear_infinite] motion-reduce:animate-mimi-pulse';
76
+
77
+ /** Grosor del trazo, en unidades del `viewBox` de 36. */
78
+ const STROKE: Record<MimiProgressSize, string> = {
79
+ default: '[stroke-width:var(--mimi-progress-circle-width,3)]',
80
+ sm: '[stroke-width:4]',
81
+ };
82
+
83
+ /**
84
+ * Progress:
85
+ *
86
+ * ```html
87
+ * <mimi-progress aria-label="Subiendo informe.pdf" [value]="64" />
88
+ * <mimi-progress aria-label="Sincronizando" /> <!-- indeterminado -->
89
+ * <mimi-progress aria-label="Ventas" shape="circle" tone="success" [value]="100" />
90
+ * ```
91
+ *
92
+ * `value` sin definir (o `null`) es indeterminado. El valor se acota entre 0 y `max`.
93
+ */
94
+ @Component({
95
+ selector: 'mimi-progress',
96
+ template: `
97
+ @if (shape() === 'bar') {
98
+ @if (determinate()) {
99
+ <span data-slot="fill" [class]="barFill()" [style.width.%]="percent()"></span>
100
+ } @else {
101
+ <span data-slot="fill" [class]="barIndeterminate()"></span>
102
+ }
103
+ } @else {
104
+ <svg aria-hidden="true" viewBox="0 0 36 36" [class]="spinClass()">
105
+ <circle cx="18" cy="18" r="15.915" [class]="trackClass()" />
106
+ <circle
107
+ data-slot="fill"
108
+ cx="18"
109
+ cy="18"
110
+ r="15.915"
111
+ [class]="arcClass()"
112
+ [attr.stroke-dasharray]="dashArray()"
113
+ />
114
+ </svg>
115
+ @if (determinate()) {
116
+ <span aria-hidden="true" class="relative font-bold tabular-nums">{{ circleText() }}</span>
117
+ }
118
+ }
119
+ `,
120
+ host: {
121
+ role: 'progressbar',
122
+ '[attr.aria-label]': 'ariaLabel() || null',
123
+ '[attr.aria-labelledby]': 'ariaLabelledby() || null',
124
+ 'aria-valuemin': '0',
125
+ '[attr.aria-valuemax]': 'safeMax()',
126
+ '[attr.aria-valuenow]': 'determinate() ? clamped() : null',
127
+ '[attr.aria-busy]': 'determinate() ? null : "true"',
128
+ '[attr.data-state]': 'determinate() ? "determinate" : "indeterminate"',
129
+ '[attr.data-tone]': 'tone()',
130
+ '[attr.data-shape]': 'shape()',
131
+ '[attr.data-size]': 'shape() === "circle" ? size() : null',
132
+ '[class]': 'classes()',
133
+ },
134
+ changeDetection: ChangeDetectionStrategy.OnPush,
135
+ })
136
+ export class MimiProgress {
137
+ private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
138
+
139
+ /** El avance, entre 0 y `max`. Sin valor (o `null`), indeterminado. */
140
+ readonly value = input<number | null | undefined, unknown>(undefined, {
141
+ transform: (v: unknown) =>
142
+ v === null || v === undefined || v === '' ? null : numberAttribute(v),
143
+ });
144
+ /** El total (100 por defecto). */
145
+ readonly max = input(100, { transform: numberAttribute });
146
+ readonly tone = input<MimiProgressTone>('primary');
147
+ readonly shape = input<MimiProgressShape>('bar');
148
+ /** Tamaño del círculo. */
149
+ readonly size = input<MimiProgressSize>('default');
150
+ /** Nombre accesible (obligatorio, o `aria-labelledby`). */
151
+ readonly ariaLabel = input<string>(undefined, { alias: 'aria-label' });
152
+ readonly ariaLabelledby = input<string>(undefined, { alias: 'aria-labelledby' });
153
+ /** Clases del elemento con el rol (la pista o el círculo), mezcladas con `cn()`. */
154
+ readonly userClass = input('', { alias: 'class' });
155
+
156
+ /** `max` válido: un número mayor que 0 (si no, 100). */
157
+ protected readonly safeMax = computed(() => {
158
+ const max = this.max();
159
+ return Number.isFinite(max) && max > 0 ? max : 100;
160
+ });
161
+ protected readonly determinate = computed(() => {
162
+ const value = this.value();
163
+ return value !== null && value !== undefined && !Number.isNaN(value);
164
+ });
165
+ /** El valor acotado entre 0 y `max`. */
166
+ protected readonly clamped = computed(() =>
167
+ Math.min(this.safeMax(), Math.max(0, this.value() ?? 0)),
168
+ );
169
+ protected readonly percent = computed(() => (this.clamped() / this.safeMax()) * 100);
170
+ /** En el círculo grande, «64%»; en el pequeño, «64» (como el diseño). */
171
+ protected readonly circleText = computed(() => {
172
+ const n = Math.round(this.percent());
173
+ return this.size() === 'sm' ? `${n}` : `${n}%`;
174
+ });
175
+ protected readonly dashArray = computed(() =>
176
+ this.determinate() ? `${this.percent()} 100` : '28 100',
177
+ );
178
+
179
+ protected readonly classes = computed(() =>
180
+ cn(
181
+ this.shape() === 'bar' ? BAR_CLASSES : CIRCLE_CLASSES[this.size()],
182
+ 'align-middle font-sans',
183
+ this.userClass(),
184
+ ),
185
+ );
186
+ protected readonly barFill = computed(() => cn(BAR_FILL, FILL_BG[this.tone()]));
187
+ protected readonly barIndeterminate = computed(() => cn(BAR_INDETERMINATE, FILL_BG[this.tone()]));
188
+ protected readonly trackClass = computed(() =>
189
+ cn(
190
+ 'fill-none stroke-[color:var(--mimi-progress-track,var(--mimi-track,var(--mimi-input)))]',
191
+ STROKE[this.size()],
192
+ ),
193
+ );
194
+ protected readonly arcClass = computed(() =>
195
+ cn(ARC, FILL_STROKE[this.tone()], STROKE[this.size()]),
196
+ );
197
+ protected readonly spinClass = computed(() =>
198
+ cn('absolute inset-0 size-full -rotate-90', !this.determinate() && SPIN),
199
+ );
200
+
201
+ constructor() {
202
+ if (isDevMode()) afterRenderEffect({ read: () => this.warn() });
203
+ }
204
+
205
+ private warned = false;
206
+ private warn(): void {
207
+ if (this.warned) return;
208
+ if (
209
+ this.host.getAttribute('aria-label')?.trim() ||
210
+ this.host.getAttribute('aria-labelledby')?.trim()
211
+ ) {
212
+ return;
213
+ }
214
+ this.warned = true;
215
+ console.warn('[Mimi] Progress: no tiene aria-label ni aria-labelledby (su nombre accesible).');
216
+ }
217
+ }
@@ -0,0 +1,12 @@
1
+ // Solo la API pública: MimiSelectList y MimiSelectOutlet son internos (se quedan en './select').
2
+ export {
3
+ type MimiOption,
4
+ type MimiOptionGroup,
5
+ MimiSelect,
6
+ type MimiSelectFilter,
7
+ MimiSelectHighlight,
8
+ MimiSelectImports,
9
+ MimiSelectOption,
10
+ type MimiSelectOptionContext,
11
+ } from './select';
12
+ export * from './select-texts';