@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.
Files changed (79) hide show
  1. package/README.md +35 -2
  2. package/dist/init/css.js +1 -1
  3. package/dist/init/index.js +25 -0
  4. package/dist/init/paths.js +20 -0
  5. package/dist/registry.json +385 -14
  6. package/dist/templates/blocks/login-01/index.ts +1 -0
  7. package/dist/templates/blocks/login-01/login-01.html +174 -0
  8. package/dist/templates/blocks/login-01/login-01.ts +105 -0
  9. package/dist/templates/blocks/login-02/index.ts +1 -0
  10. package/dist/templates/blocks/login-02/login-02.html +186 -0
  11. package/dist/templates/blocks/login-02/login-02.ts +92 -0
  12. package/dist/templates/blocks/otp-01/index.ts +1 -0
  13. package/dist/templates/blocks/otp-01/otp-01.html +166 -0
  14. package/dist/templates/blocks/otp-01/otp-01.ts +212 -0
  15. package/dist/templates/blocks/pricing-01/index.ts +1 -0
  16. package/dist/templates/blocks/pricing-01/pricing-01.html +173 -0
  17. package/dist/templates/blocks/pricing-01/pricing-01.ts +178 -0
  18. package/dist/templates/blocks/settings-01/index.ts +1 -0
  19. package/dist/templates/blocks/settings-01/settings-01.html +424 -0
  20. package/dist/templates/blocks/settings-01/settings-01.ts +382 -0
  21. package/dist/templates/blocks/signup-01/index.ts +1 -0
  22. package/dist/templates/blocks/signup-01/signup-01.html +332 -0
  23. package/dist/templates/blocks/signup-01/signup-01.ts +176 -0
  24. package/dist/templates/components/calendar/calendar-texts.ts +72 -0
  25. package/dist/templates/components/calendar/calendar.ts +588 -0
  26. package/dist/templates/components/calendar/dates.ts +181 -0
  27. package/dist/templates/components/calendar/index.ts +3 -0
  28. package/dist/templates/components/chip/chip-texts.ts +40 -0
  29. package/dist/templates/components/chip/chip.ts +116 -0
  30. package/dist/templates/components/chip/index.ts +2 -0
  31. package/dist/templates/components/color-picker/color-picker-texts.ts +57 -0
  32. package/dist/templates/components/color-picker/color-picker.ts +718 -0
  33. package/dist/templates/components/color-picker/color.ts +157 -0
  34. package/dist/templates/components/color-picker/index.ts +3 -0
  35. package/dist/templates/components/combobox/combobox-texts.ts +70 -0
  36. package/dist/templates/components/combobox/combobox.ts +798 -0
  37. package/dist/templates/components/combobox/index.ts +2 -0
  38. package/dist/templates/components/date-picker/date-picker.ts +240 -0
  39. package/dist/templates/components/date-picker/index.ts +3 -0
  40. package/dist/templates/components/date-range/date-range.ts +308 -0
  41. package/dist/templates/components/date-range/index.ts +1 -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/input-mask/index.ts +1 -0
  46. package/dist/templates/components/input-mask/input-mask.ts +319 -0
  47. package/dist/templates/components/input-otp/index.ts +1 -0
  48. package/dist/templates/components/input-otp/input-otp.ts +212 -0
  49. package/dist/templates/components/number-input/index.ts +2 -0
  50. package/dist/templates/components/number-input/number-input-texts.ts +46 -0
  51. package/dist/templates/components/number-input/number-input.ts +450 -0
  52. package/dist/templates/components/option-list/index.ts +11 -0
  53. package/dist/templates/components/option-list/option-list.ts +518 -0
  54. package/dist/templates/components/password/index.ts +2 -0
  55. package/dist/templates/components/password/password-texts.ts +68 -0
  56. package/dist/templates/components/password/password.ts +363 -0
  57. package/dist/templates/components/popover/popover.ts +12 -4
  58. package/dist/templates/components/progress/index.ts +1 -0
  59. package/dist/templates/components/progress/progress.ts +217 -0
  60. package/dist/templates/components/select/select.ts +3 -2
  61. package/dist/templates/components/slider/index.ts +2 -0
  62. package/dist/templates/components/slider/slider-texts.ts +26 -0
  63. package/dist/templates/components/slider/slider.ts +425 -0
  64. package/dist/templates/components/tabs/tabs.ts +88 -23
  65. package/dist/templates/components/tags-input/index.ts +2 -0
  66. package/dist/templates/components/tags-input/tags-input-texts.ts +44 -0
  67. package/dist/templates/components/tags-input/tags-input.ts +303 -0
  68. package/dist/templates/components/toggle-group/index.ts +1 -0
  69. package/dist/templates/components/toggle-group/toggle-group.ts +406 -0
  70. package/dist/templates/theme/provider.ts +13 -0
  71. package/dist/templates/theme/theme-base.css +103 -0
  72. package/dist/templates/theme/types.ts +216 -0
  73. package/dist/templates/utils/overlay.ts +17 -4
  74. package/dist/ui/index.js +232 -10
  75. package/dist/ui/schema.json +82 -2
  76. package/dist/utils/project.js +35 -0
  77. package/dist/utils/tailwind.js +80 -0
  78. package/dist/utils/write-files.js +49 -9
  79. package/package.json +1 -1
@@ -0,0 +1,588 @@
1
+ import { Directionality } from '@angular/cdk/bidi';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ ElementRef,
6
+ Injector,
7
+ LOCALE_ID,
8
+ afterNextRender,
9
+ computed,
10
+ inject,
11
+ input,
12
+ linkedSignal,
13
+ model,
14
+ output,
15
+ signal,
16
+ } from '@angular/core';
17
+ import { cn } from '@/components/ui/utils/cn';
18
+ import { buttonFocusStyles } from '@/components/ui/utils/control-styles';
19
+ import { MIMI_DATE_PICKER_TEXT } from './calendar-texts';
20
+ import {
21
+ MIMI_TODAY,
22
+ type MimiDate,
23
+ mimiAddDays,
24
+ mimiAddMonths,
25
+ mimiFormatDate,
26
+ mimiStartOfMonth,
27
+ mimiWeekStart,
28
+ mimiWeekday,
29
+ } from './dates';
30
+
31
+ /*
32
+ * Calendario (docs/design/Mimi F3 Combobox Fechas Hora.dc.html, «2 · Date Picker y Date Range»;
33
+ * prefijo de tokens `date`, spec 13). Lo usan el Date Picker y el Date Range, y se puede usar
34
+ * suelto. Cuadrícula propia con el teclado de la APG («Date Picker Dialog»; decidido por defecto
35
+ * en la G2.9: la `ngGrid` de aria no da Re Pág/Av Pág ni el paso de mes en los bordes). Los
36
+ * nombres de meses y días salen de `Intl.DateTimeFormat` con el idioma.
37
+ */
38
+
39
+ /** Vista del calendario. */
40
+ export type MimiCalendarView = 'days' | 'months' | 'years';
41
+
42
+ interface Day {
43
+ date: MimiDate;
44
+ day: number;
45
+ inMonth: boolean;
46
+ today: boolean;
47
+ disabled: boolean;
48
+ selected: boolean;
49
+ /** Banda del rango: 'start', 'end', 'both', 'middle' o null. */
50
+ band: 'start' | 'end' | 'both' | 'middle' | null;
51
+ label: string;
52
+ }
53
+
54
+ const ICON = {
55
+ viewBox: '0 0 24 24',
56
+ left: 'm15 18-6-6 6-6',
57
+ right: 'm9 18 6-6-6-6',
58
+ down: 'm6 9 6 6 6-6',
59
+ } as const;
60
+
61
+ const NAV_CLASSES = cn(
62
+ 'inline-flex size-[30px] shrink-0 cursor-pointer items-center justify-center rounded-[var(--mimi-radius-sm)] border border-border bg-transparent text-foreground [&_svg]:size-4',
63
+ 'mimi-transition hover:bg-accent active:scale-90',
64
+ buttonFocusStyles,
65
+ );
66
+ const TITLE_BUTTON_CLASSES = cn(
67
+ 'inline-flex h-[30px] cursor-pointer items-center gap-1 rounded-[var(--mimi-radius-sm)] border-0 bg-transparent px-2.5 text-sm font-semibold text-foreground [&_svg]:size-3.5',
68
+ 'mimi-transition hover:bg-accent active:scale-[0.96]',
69
+ buttonFocusStyles,
70
+ );
71
+ const CELL_BASE = cn(
72
+ 'relative inline-flex size-[var(--mimi-date-cell-size,36px)] cursor-pointer items-center justify-center text-[13.5px] tabular-nums select-none',
73
+ 'rounded-[var(--mimi-date-cell-radius,var(--mimi-radius-sm))] mimi-transition active:scale-90',
74
+ buttonFocusStyles,
75
+ );
76
+ /** Clases de cada estado del día, elegidas en TypeScript (spec 10: sin variantes encadenadas). */
77
+ const CELL = {
78
+ selected: cn(
79
+ CELL_BASE,
80
+ 'bg-[color:var(--mimi-date-selected-bg,var(--mimi-primary))] font-semibold text-[color:var(--mimi-date-selected-fg,var(--mimi-primary-foreground))]',
81
+ ),
82
+ day: cn(
83
+ CELL_BASE,
84
+ 'text-foreground hover:bg-[color:var(--mimi-date-cell-hover,var(--mimi-accent))]',
85
+ ),
86
+ outside: cn(
87
+ CELL_BASE,
88
+ 'text-muted-foreground opacity-55 hover:bg-[color:var(--mimi-date-cell-hover,var(--mimi-accent))]',
89
+ ),
90
+ disabled: cn(
91
+ CELL_BASE,
92
+ 'cursor-not-allowed text-foreground line-through opacity-[.38] active:scale-100',
93
+ ),
94
+ disabledOutside: cn(
95
+ CELL_BASE,
96
+ 'cursor-not-allowed text-muted-foreground opacity-[.38] active:scale-100',
97
+ ),
98
+ } as const;
99
+ const TODAY_CLASSES =
100
+ 'font-semibold shadow-[shadow:inset_0_0_0_1px_var(--mimi-date-today-ring,var(--mimi-border))]';
101
+ const BAND = {
102
+ none: 'flex justify-center',
103
+ middle:
104
+ 'flex justify-center bg-[color:var(--mimi-date-range,var(--mimi-range,var(--mimi-accent)))]',
105
+ start:
106
+ 'flex justify-center rounded-s-[var(--mimi-date-cell-radius,var(--mimi-radius-sm))] bg-[color:var(--mimi-date-range,var(--mimi-range,var(--mimi-accent)))]',
107
+ end: 'flex justify-center rounded-e-[var(--mimi-date-cell-radius,var(--mimi-radius-sm))] bg-[color:var(--mimi-date-range,var(--mimi-range,var(--mimi-accent)))]',
108
+ both: 'flex justify-center',
109
+ } as const;
110
+ const PICK_BASE = cn(
111
+ 'h-11 cursor-pointer rounded-[var(--mimi-radius-sm)] border-0 text-sm tabular-nums mimi-transition hover:shadow-[shadow:inset_0_0_0_1px_var(--mimi-border)] active:scale-[0.94]',
112
+ buttonFocusStyles,
113
+ );
114
+ const PICK = {
115
+ on: cn(PICK_BASE, 'bg-primary font-semibold text-primary-foreground'),
116
+ off: cn(PICK_BASE, 'bg-transparent text-foreground'),
117
+ } as const;
118
+
119
+ let nextId = 0;
120
+
121
+ const capitalize = (text: string) => text.charAt(0).toLocaleUpperCase() + text.slice(1);
122
+
123
+ /**
124
+ * Calendario de un mes, con vistas de meses y años:
125
+ *
126
+ * ```html
127
+ * <mimi-calendar [(value)]="fecha" [min]="hoy" [isDateDisabled]="esFinDeSemana" />
128
+ * ```
129
+ *
130
+ * `value` es una fecha ISO (`'2026-09-30'`, tipo `MimiDate`). Con `mode="range"` pinta el periodo
131
+ * de `rangeStart` a `rangeEnd` y solo avisa con `(datePick)` (lo usa el Date Range).
132
+ */
133
+ @Component({
134
+ selector: 'mimi-calendar',
135
+ template: `
136
+ <div class="flex items-center justify-between gap-2">
137
+ <button
138
+ type="button"
139
+ [class]="navClasses"
140
+ [attr.aria-label]="texts.previous"
141
+ (click)="move(-1)"
142
+ >
143
+ <svg
144
+ aria-hidden="true"
145
+ fill="none"
146
+ stroke="currentColor"
147
+ stroke-width="2"
148
+ stroke-linecap="round"
149
+ stroke-linejoin="round"
150
+ [attr.viewBox]="icon.viewBox"
151
+ >
152
+ <path [attr.d]="icon.left" />
153
+ </svg>
154
+ </button>
155
+ @if (mode() === 'single') {
156
+ <button
157
+ type="button"
158
+ data-slot="title"
159
+ [id]="titleId"
160
+ [class]="titleButtonClasses"
161
+ [attr.aria-label]="title() + ', ' + texts.changeView"
162
+ (click)="cycleView()"
163
+ >
164
+ {{ title() }}
165
+ <svg
166
+ aria-hidden="true"
167
+ fill="none"
168
+ stroke="currentColor"
169
+ stroke-width="2"
170
+ stroke-linecap="round"
171
+ stroke-linejoin="round"
172
+ [attr.viewBox]="icon.viewBox"
173
+ >
174
+ <path [attr.d]="icon.down" />
175
+ </svg>
176
+ </button>
177
+ } @else {
178
+ <span data-slot="title" class="text-sm font-semibold" [id]="titleId">{{ title() }}</span>
179
+ }
180
+ <button type="button" [class]="navClasses" [attr.aria-label]="texts.next" (click)="move(1)">
181
+ <svg
182
+ aria-hidden="true"
183
+ fill="none"
184
+ stroke="currentColor"
185
+ stroke-width="2"
186
+ stroke-linecap="round"
187
+ stroke-linejoin="round"
188
+ [attr.viewBox]="icon.viewBox"
189
+ >
190
+ <path [attr.d]="icon.right" />
191
+ </svg>
192
+ </button>
193
+ </div>
194
+ <!-- El cambio de mes, año o vista se anuncia. -->
195
+ <span class="sr-only" aria-live="polite">{{ title() }}</span>
196
+
197
+ @switch (view()) {
198
+ @case ('days') {
199
+ <div
200
+ role="grid"
201
+ class="grid grid-cols-[repeat(7,var(--mimi-date-cell-size,36px))] gap-y-0.5"
202
+ [attr.aria-labelledby]="titleId"
203
+ (keydown)="onGridKeydown($event)"
204
+ >
205
+ <div role="row" class="contents">
206
+ @for (weekday of weekdays(); track $index) {
207
+ <span
208
+ role="columnheader"
209
+ class="flex h-7 items-center justify-center text-xs font-medium text-muted-foreground"
210
+ [attr.aria-label]="weekday.long"
211
+ >{{ weekday.short }}</span
212
+ >
213
+ }
214
+ </div>
215
+ @for (week of weeks(); track week[0].date) {
216
+ <div role="row" class="contents">
217
+ @for (day of week; track day.date) {
218
+ <div [class]="bandClasses[day.band ?? 'none']">
219
+ <div
220
+ role="gridcell"
221
+ [attr.data-date]="day.date"
222
+ [attr.tabindex]="day.date === active() ? 0 : -1"
223
+ [attr.autofocus]="day.date === active() ? '' : null"
224
+ [attr.aria-label]="day.label"
225
+ [attr.aria-selected]="day.selected"
226
+ [attr.aria-current]="day.today ? 'date' : null"
227
+ [attr.aria-disabled]="day.disabled || null"
228
+ [attr.data-outside]="day.inMonth ? null : ''"
229
+ [class]="cellClasses(day)"
230
+ (focus)="active.set(day.date)"
231
+ (click)="pickDay(day)"
232
+ >
233
+ {{ day.day }}
234
+ @if (day.today) {
235
+ <span
236
+ aria-hidden="true"
237
+ class="absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full bg-current"
238
+ ></span>
239
+ }
240
+ </div>
241
+ </div>
242
+ }
243
+ </div>
244
+ }
245
+ </div>
246
+ }
247
+ @case ('months') {
248
+ <div class="grid w-[252px] grid-cols-3 gap-1.5">
249
+ @for (month of months(); track month.date) {
250
+ <button
251
+ type="button"
252
+ [class]="month.on ? pickOn : pickOff"
253
+ [attr.aria-pressed]="month.on"
254
+ [attr.data-active]="month.active ? '' : null"
255
+ (click)="pickMonth(month.date)"
256
+ >
257
+ {{ month.label }}
258
+ </button>
259
+ }
260
+ </div>
261
+ }
262
+ @case ('years') {
263
+ <div class="grid w-[252px] grid-cols-3 gap-1.5">
264
+ @for (year of years(); track year.year) {
265
+ <button
266
+ type="button"
267
+ [class]="year.on ? pickOn : pickOff"
268
+ [attr.aria-pressed]="year.on"
269
+ [attr.data-active]="year.active ? '' : null"
270
+ (click)="pickYear(year.year)"
271
+ >
272
+ {{ year.year }}
273
+ </button>
274
+ }
275
+ </div>
276
+ }
277
+ }
278
+
279
+ @if (mode() === 'single') {
280
+ <div class="flex items-center justify-between border-t border-border pt-2">
281
+ <span class="flex items-center gap-1.5 text-xs text-muted-foreground">
282
+ <span aria-hidden="true" class="size-1 rounded-full bg-current"></span>{{ texts.today }}
283
+ </span>
284
+ <button
285
+ type="button"
286
+ data-slot="go-today"
287
+ class="h-7 cursor-pointer rounded-[var(--mimi-radius-sm)] border border-border bg-transparent px-2.5 text-[12.5px] font-medium text-foreground mimi-transition hover:bg-accent active:scale-[0.94]"
288
+ [class]="focusClasses"
289
+ (click)="goToday()"
290
+ >
291
+ {{ texts.goToday }}
292
+ </button>
293
+ </div>
294
+ }
295
+ `,
296
+ host: {
297
+ '[class]': 'classes()',
298
+ '[attr.data-view]': 'view()',
299
+ },
300
+ changeDetection: ChangeDetectionStrategy.OnPush,
301
+ })
302
+ export class MimiCalendar {
303
+ private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
304
+ private readonly injector = inject(Injector);
305
+ private readonly dir = inject(Directionality, { optional: true });
306
+ private readonly defaultLocale = inject(LOCALE_ID);
307
+ private readonly todayFn = inject(MIMI_TODAY);
308
+ protected readonly texts = inject(MIMI_DATE_PICKER_TEXT);
309
+
310
+ /** La fecha elegida (`'2026-09-30'`), o `null`. Con `[(value)]`. */
311
+ readonly value = model<MimiDate | null>(null);
312
+ /** `single` elige una fecha; `range` pinta el periodo de `rangeStart` a `rangeEnd`. */
313
+ readonly mode = input<'single' | 'range'>('single');
314
+ readonly rangeStart = input<MimiDate | null>(null);
315
+ readonly rangeEnd = input<MimiDate | null>(null);
316
+ /** Primera y última fecha que se pueden elegir (incluidas). */
317
+ readonly min = input<MimiDate | null>(null);
318
+ readonly max = input<MimiDate | null>(null);
319
+ /** Fechas que no se pueden elegir (por ejemplo, los fines de semana). */
320
+ readonly isDateDisabled = input<((date: MimiDate) => boolean) | null>(null);
321
+ /** Idioma (nombres y primer día de la semana). Por defecto, `LOCALE_ID`. */
322
+ readonly locale = input<string>();
323
+ /** Primer día de la semana (1 = lunes … 7 = domingo). Por defecto, el del idioma. */
324
+ readonly weekStart = input<number>();
325
+ readonly userClass = input('', { alias: 'class' });
326
+
327
+ /** Se eligió un día habilitado (con el puntero, Enter o Espacio). */
328
+ readonly datePick = output<MimiDate>();
329
+
330
+ protected readonly icon = ICON;
331
+ protected readonly navClasses = NAV_CLASSES;
332
+ protected readonly titleButtonClasses = TITLE_BUTTON_CLASSES;
333
+ protected readonly bandClasses = BAND;
334
+ protected readonly pickOn = PICK.on;
335
+ protected readonly pickOff = PICK.off;
336
+ protected readonly focusClasses = buttonFocusStyles;
337
+ protected readonly titleId = `mimi-calendar-${nextId++}-title`;
338
+
339
+ protected readonly classes = computed(() =>
340
+ cn('inline-flex flex-col gap-2 font-sans text-foreground', this.userClass()),
341
+ );
342
+
343
+ private readonly lang = computed(() => this.locale() || this.defaultLocale);
344
+ private readonly firstDay = computed(() => this.weekStart() ?? mimiWeekStart(this.lang()));
345
+ protected readonly today = computed(() => this.todayFn());
346
+
347
+ /** Vista actual. */
348
+ readonly view = signal<MimiCalendarView>('days');
349
+ /** Día con el foco (la parada de Tab de la cuadrícula). Sigue a la fecha elegida o a hoy. */
350
+ protected readonly active = linkedSignal<MimiDate>(
351
+ () => this.value() ?? this.rangeStart() ?? this.today(),
352
+ );
353
+ /** Primer día del mes que se ve. */
354
+ protected readonly month = linkedSignal(() => mimiStartOfMonth(this.active()));
355
+
356
+ private readonly year = computed(() => +this.month().slice(0, 4));
357
+
358
+ protected readonly title = computed(() => {
359
+ const view = this.view();
360
+ if (view === 'years') {
361
+ const start = this.year() - (this.year() % 12);
362
+ return `${start}–${start + 11}`;
363
+ }
364
+ if (view === 'months') return String(this.year());
365
+ return capitalize(
366
+ mimiFormatDate(this.month(), this.lang(), { month: 'long', year: 'numeric' }),
367
+ );
368
+ });
369
+
370
+ protected readonly weekdays = computed(() => {
371
+ // 2024-01-01 fue lunes: el día ISO n es 2024-01-0n.
372
+ const first = this.firstDay();
373
+ return Array.from({ length: 7 }, (_, i) => {
374
+ const weekday = ((first - 1 + i) % 7) + 1;
375
+ const date = mimiAddDays('2024-01-01', weekday - 1);
376
+ return {
377
+ short: mimiFormatDate(date, this.lang(), { weekday: 'narrow' }),
378
+ long: mimiFormatDate(date, this.lang(), { weekday: 'long' }),
379
+ };
380
+ });
381
+ });
382
+
383
+ /** `true` si la fecha no se puede elegir. */
384
+ isDisabled(date: MimiDate): boolean {
385
+ const min = this.min();
386
+ const max = this.max();
387
+ return (!!min && date < min) || (!!max && date > max) || !!this.isDateDisabled()?.(date);
388
+ }
389
+
390
+ /** Seis semanas desde el primer día de la semana que contiene el día 1. */
391
+ protected readonly weeks = computed<Day[][]>(() => {
392
+ const month = this.month();
393
+ const offset = (mimiWeekday(month) - this.firstDay() + 7) % 7;
394
+ const first = mimiAddDays(month, -offset);
395
+ const today = this.today();
396
+ const value = this.value();
397
+ const single = this.mode() === 'single';
398
+ const start = this.rangeStart();
399
+ const end = this.rangeEnd();
400
+ const lang = this.lang();
401
+ const weeks: Day[][] = [];
402
+ for (let w = 0; w < 6; w++) {
403
+ const week: Day[] = [];
404
+ for (let d = 0; d < 7; d++) {
405
+ const date = mimiAddDays(first, w * 7 + d);
406
+ const disabled = this.isDisabled(date);
407
+ const isStart = !single && date === start;
408
+ const isEnd = !single && date === end;
409
+ const band =
410
+ single || !start || !end || start === end
411
+ ? null
412
+ : isStart
413
+ ? 'start'
414
+ : isEnd
415
+ ? 'end'
416
+ : date > start && date < end
417
+ ? 'middle'
418
+ : null;
419
+ const label = mimiFormatDate(date, lang, {
420
+ day: 'numeric',
421
+ month: 'long',
422
+ year: 'numeric',
423
+ });
424
+ week.push({
425
+ date,
426
+ day: +date.slice(8),
427
+ inMonth: date.slice(0, 7) === month.slice(0, 7),
428
+ today: date === today,
429
+ disabled,
430
+ selected: single ? date === value : isStart || isEnd,
431
+ band,
432
+ label: disabled ? `${label}, ${this.texts.unavailable}` : label,
433
+ });
434
+ }
435
+ weeks.push(week);
436
+ }
437
+ return weeks;
438
+ });
439
+
440
+ protected readonly months = computed(() => {
441
+ const year = this.year();
442
+ const value = this.value();
443
+ const lang = this.lang();
444
+ return Array.from({ length: 12 }, (_, i) => {
445
+ const date = `${String(year).padStart(4, '0')}-${String(i + 1).padStart(2, '0')}-01`;
446
+ return {
447
+ date,
448
+ label: capitalize(mimiFormatDate(date, lang, { month: 'short' }).replace('.', '')),
449
+ on: !!value && value.slice(0, 7) === date.slice(0, 7),
450
+ active: date.slice(0, 7) === this.month().slice(0, 7),
451
+ };
452
+ });
453
+ });
454
+
455
+ protected readonly years = computed(() => {
456
+ const start = this.year() - (this.year() % 12);
457
+ const selected = this.value() ? +this.value()!.slice(0, 4) : null;
458
+ return Array.from({ length: 12 }, (_, i) => ({
459
+ year: start + i,
460
+ on: start + i === selected,
461
+ active: start + i === this.year(),
462
+ }));
463
+ });
464
+
465
+ protected cellClasses(day: Day): string {
466
+ const state = day.selected
467
+ ? CELL.selected
468
+ : day.disabled
469
+ ? day.inMonth
470
+ ? CELL.disabled
471
+ : CELL.disabledOutside
472
+ : day.inMonth
473
+ ? CELL.day
474
+ : CELL.outside;
475
+ return day.today && !day.selected ? cn(state, TODAY_CLASSES) : state;
476
+ }
477
+
478
+ /** Mes, año o página de años anterior (-1) o siguiente (1). */
479
+ protected move(step: number): void {
480
+ const view = this.view();
481
+ const months = view === 'days' ? step : view === 'months' ? step * 12 : step * 144;
482
+ this.month.set(mimiAddMonths(this.month(), months));
483
+ if (view === 'days') this.active.set(mimiAddMonths(this.active(), step));
484
+ }
485
+
486
+ protected cycleView(): void {
487
+ const next: Record<MimiCalendarView, MimiCalendarView> = {
488
+ days: 'months',
489
+ months: 'years',
490
+ years: 'days',
491
+ };
492
+ this.view.set(next[this.view()]);
493
+ this.focusActive();
494
+ }
495
+
496
+ protected pickMonth(date: MimiDate): void {
497
+ this.month.set(date);
498
+ this.active.set(mimiStartOfMonth(date));
499
+ this.view.set('days');
500
+ this.focusActive();
501
+ }
502
+
503
+ protected pickYear(year: number): void {
504
+ this.month.set(`${String(year).padStart(4, '0')}${this.month().slice(4)}`);
505
+ this.view.set('months');
506
+ this.focusActive();
507
+ }
508
+
509
+ protected pickDay(day: Day): void {
510
+ this.active.set(day.date);
511
+ if (day.disabled) return;
512
+ if (this.mode() === 'single') this.value.set(day.date);
513
+ if (!day.inMonth) this.month.set(mimiStartOfMonth(day.date));
514
+ this.datePick.emit(day.date);
515
+ }
516
+
517
+ /** Lleva la vista al mes de hoy y el foco a hoy. */
518
+ goToday(): void {
519
+ this.view.set('days');
520
+ this.active.set(this.today());
521
+ this.month.set(mimiStartOfMonth(this.today()));
522
+ this.focusActive();
523
+ }
524
+
525
+ /** Teclado de la APG en la cuadrícula. */
526
+ protected onGridKeydown(event: KeyboardEvent): void {
527
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
528
+ const rtl = this.dir?.value === 'rtl';
529
+ const active = this.active();
530
+ let next: MimiDate | null = null;
531
+ switch (event.key) {
532
+ case 'ArrowLeft':
533
+ next = mimiAddDays(active, rtl ? 1 : -1);
534
+ break;
535
+ case 'ArrowRight':
536
+ next = mimiAddDays(active, rtl ? -1 : 1);
537
+ break;
538
+ case 'ArrowUp':
539
+ next = mimiAddDays(active, -7);
540
+ break;
541
+ case 'ArrowDown':
542
+ next = mimiAddDays(active, 7);
543
+ break;
544
+ case 'Home':
545
+ next = mimiAddDays(active, -((mimiWeekday(active) - this.firstDay() + 7) % 7));
546
+ break;
547
+ case 'End':
548
+ next = mimiAddDays(active, 6 - ((mimiWeekday(active) - this.firstDay() + 7) % 7));
549
+ break;
550
+ case 'PageUp':
551
+ next = mimiAddMonths(active, event.shiftKey ? -12 : -1);
552
+ break;
553
+ case 'PageDown':
554
+ next = mimiAddMonths(active, event.shiftKey ? 12 : 1);
555
+ break;
556
+ case 'Enter':
557
+ case ' ': {
558
+ event.preventDefault();
559
+ const day = this.weeks()
560
+ .flat()
561
+ .find((d) => d.date === active);
562
+ if (day) this.pickDay(day);
563
+ return;
564
+ }
565
+ default:
566
+ return;
567
+ }
568
+ event.preventDefault();
569
+ this.active.set(next);
570
+ if (next.slice(0, 7) !== this.month().slice(0, 7)) this.month.set(mimiStartOfMonth(next));
571
+ this.focusActive();
572
+ }
573
+
574
+ /** Lleva el foco al día activo (o al mes o año activo en esas vistas) después de pintar. */
575
+ focusActive(): void {
576
+ afterNextRender(
577
+ {
578
+ write: () => {
579
+ const target = this.host.querySelector<HTMLElement>(
580
+ '[role="gridcell"][tabindex="0"], [data-active]',
581
+ );
582
+ (target ?? this.host.querySelector<HTMLElement>('[data-slot="title"]'))?.focus();
583
+ },
584
+ },
585
+ { injector: this.injector },
586
+ );
587
+ }
588
+ }