@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
@@ -352,6 +352,209 @@ export interface MimiPaginationTokens {
352
352
  blur?: string;
353
353
  }
354
354
 
355
+ /**
356
+ * Toggle Group. Fondo, textos, indicador y su sombra van en el CSS (`--mimi-toggle-group-bg`,
357
+ * `-fg`, `-active-fg`, `-indicator`, `-indicator-shadow`).
358
+ */
359
+ export interface MimiToggleGroupTokens {
360
+ /** Radio de cada ítem y del indicador (el global `radius`); el del grupo es radio + relleno. */
361
+ radius?: string;
362
+ /** Relleno del grupo (3px). */
363
+ padding?: string;
364
+ /** Separación entre ítems (2px). */
365
+ gap?: string;
366
+ /** Alto de cada ítem (32px). */
367
+ itemHeight?: string;
368
+ /** Relleno horizontal de cada ítem (12px). */
369
+ itemPx?: string;
370
+ /** Ancho mínimo de un ítem (el de uno solo con ícono, 44px). */
371
+ iconWidth?: string;
372
+ /** Tamaño de texto (13.5px). */
373
+ fontSize?: string;
374
+ /** Curva del indicador (`cubic-bezier(.3, 1.35, .5, 1)`, la de Tabs). */
375
+ spring?: string;
376
+ }
377
+
378
+ /**
379
+ * Progress. La pista y la parte llena van en el CSS (`--mimi-progress-track`, `-fill`).
380
+ */
381
+ export interface MimiProgressTokens {
382
+ /** Grosor de la barra (8px). */
383
+ height?: string;
384
+ /** Tamaño del círculo `default` (76px). */
385
+ circleSize?: string;
386
+ /** Grosor del trazo del círculo `default`, en unidades del viewBox de 36 (3). */
387
+ circleWidth?: string | number;
388
+ }
389
+
390
+ /**
391
+ * Number Input. El fondo del botón en hover va en el CSS (`--mimi-number-button-hover`); el
392
+ * borde, el fondo y los anillos son los de Input (globales).
393
+ */
394
+ export interface MimiNumberInputTokens {
395
+ /** Altura (el alto de control). */
396
+ height?: string;
397
+ /** Radio de la caja (el global `radius`). */
398
+ radius?: string;
399
+ /** Relleno horizontal de la caja (4px). */
400
+ px?: string;
401
+ /** Tamaño de los botones − y + (30px). */
402
+ buttonSize?: string;
403
+ /** Radio de los botones (el global `radius-sm`). */
404
+ buttonRadius?: string;
405
+ }
406
+
407
+ /**
408
+ * Password. El hover del botón y la pista del medidor van en el CSS
409
+ * (`--mimi-password-toggle-hover`, `-meter-track`); los colores de los niveles son los globales
410
+ * `--mimi-strength-*`.
411
+ */
412
+ export interface MimiPasswordTokens {
413
+ /** Botón de mostrar u ocultar (32px). */
414
+ toggleSize?: string;
415
+ /** Radio del botón (el global `radius-sm`). */
416
+ toggleRadius?: string;
417
+ /** Alto de cada barra del medidor (6px). */
418
+ meterHeight?: string;
419
+ /** Separación entre barras (4px). */
420
+ meterGap?: string;
421
+ }
422
+
423
+ /**
424
+ * Input Mask. El color del marcador y el fondo del prefijo van en el CSS
425
+ * (`--mimi-mask-placeholder`, `-prefix-bg`); la caja usa los tokens de Input.
426
+ */
427
+ export interface MimiInputMaskTokens {
428
+ /** Separación entre el prefijo, el ícono y el texto (8px). */
429
+ gap?: string;
430
+ /** Alto del prefijo (26px). */
431
+ prefixHeight?: string;
432
+ /** Radio del prefijo (el global `radius-sm`). */
433
+ prefixRadius?: string;
434
+ }
435
+
436
+ /**
437
+ * Input OTP. Fondo, borde, separador y cursor van en el CSS (`--mimi-otp-bg`, `-border`,
438
+ * `-separator`, `-caret`).
439
+ */
440
+ export interface MimiInputOtpTokens {
441
+ /** Ancho de la casilla (46px). */
442
+ width?: string;
443
+ /** Alto de la casilla (54px). */
444
+ height?: string;
445
+ /** Separación entre casillas (8px). */
446
+ gap?: string;
447
+ /** Radio (el global `radius`). */
448
+ radius?: string;
449
+ /** Tamaño del carácter (22px). */
450
+ fontSize?: string;
451
+ }
452
+
453
+ /** Chip (Tags Input y Combobox). Los colores van en el CSS (`--mimi-chip-bg`, `-fg`). */
454
+ export interface MimiChipTokens {
455
+ /** Alto (26px). */
456
+ height?: string;
457
+ /** Radio (el global `badge-radius`). */
458
+ radius?: string;
459
+ /** Tamaño de texto (13px). */
460
+ fontSize?: string;
461
+ /** Botón de quitar (18px). */
462
+ removeSize?: string;
463
+ }
464
+
465
+ /** Tags Input. El borde con el límite alcanzado va en el CSS (`--mimi-tags-full-border`). */
466
+ export interface MimiTagsInputTokens {
467
+ /** Separación entre chips (6px). */
468
+ gap?: string;
469
+ /** Relleno de la caja (`5px 10px 5px 6px`). */
470
+ padding?: string;
471
+ /** Ancho mínimo del campo de texto (120px). */
472
+ inputMinWidth?: string;
473
+ }
474
+
475
+ /**
476
+ * Slider. Pista, relleno, pulgar y su sombra van en el CSS (`--mimi-slider-track`, `-fill`,
477
+ * `-thumb`, `-thumb-shadow`); la burbuja usa los colores de Tooltip.
478
+ */
479
+ export interface MimiSliderTokens {
480
+ /** Grosor de la pista (6px). */
481
+ height?: string;
482
+ /** Tamaño del pulgar (20px). */
483
+ thumbSize?: string;
484
+ }
485
+
486
+ /**
487
+ * Combobox. El fondo y la sombra del panel y la opción activa van en el CSS
488
+ * (`--mimi-combobox-menu-bg`, `-menu-shadow`, `-option-hover`).
489
+ */
490
+ export interface MimiComboboxTokens {
491
+ /** Alto del campo (el global `control-height`). */
492
+ height?: string;
493
+ /** Radio del campo y del panel (el global `radius`). */
494
+ radius?: string;
495
+ /** Alto máximo del panel (228px). */
496
+ menuMaxHeight?: string;
497
+ /** Alto mínimo de cada opción (34px). */
498
+ optionHeight?: string;
499
+ /** Radio de cada opción (el global `radius-sm`). */
500
+ optionRadius?: string;
501
+ }
502
+
503
+ /**
504
+ * Calendario, Date Picker y Date Range (una tabla, decidido por defecto en la G2.9). Los colores
505
+ * van en el CSS (`--mimi-date-cell-hover`, `-selected-bg`, `-selected-fg`, `-range`,
506
+ * `-today-ring`, `-menu-bg`, `-menu-shadow`).
507
+ */
508
+ export interface MimiDateTokens {
509
+ /** Tamaño de cada día (36px). */
510
+ cellSize?: string;
511
+ /** Radio de cada día (el global `radius-sm`). */
512
+ cellRadius?: string;
513
+ /** Relleno del panel (12px). */
514
+ padding?: string;
515
+ /** Ancho de la columna de atajos del rango (130px). */
516
+ presetsWidth?: string;
517
+ }
518
+
519
+ /**
520
+ * File Upload. Fondos y bordes de la zona van en el CSS (`--mimi-upload-bg`, `-border`,
521
+ * `-active-bg`, `-active-border`).
522
+ */
523
+ export interface MimiFileUploadTokens {
524
+ /** Radio de la zona (el global `radius-card`). */
525
+ radius?: string;
526
+ /** Alto mínimo de la zona (176px). */
527
+ height?: string;
528
+ /** Círculo del ícono (44px). */
529
+ iconSize?: string;
530
+ /** Radio de cada fila (el global `radius`). */
531
+ itemRadius?: string;
532
+ /** Miniatura de cada fila (44px). */
533
+ thumbSize?: string;
534
+ }
535
+
536
+ /**
537
+ * Color Picker. Fondo, sombra, el borde y la sombra de los pulgares y el borde de las muestras
538
+ * van en el CSS (`--mimi-color-picker-bg`, `-shadow`, `-thumb-border`, `-thumb-shadow`,
539
+ * `-swatch-border`).
540
+ */
541
+ export interface MimiColorPickerTokens {
542
+ /** Radio del panel (el global `radius`). */
543
+ radius?: string;
544
+ /** Ancho del panel (280px). */
545
+ width?: string;
546
+ /** Relleno y separación (12px). */
547
+ padding?: string;
548
+ /** Alto del área de saturación (150px). */
549
+ areaHeight?: string;
550
+ /** Radio del área (el global `radius-sm`). */
551
+ areaRadius?: string;
552
+ /** Alto de la barra de tono (12px). */
553
+ hueHeight?: string;
554
+ /** Separación de las muestras (6px). */
555
+ swatchGap?: string;
556
+ }
557
+
355
558
  /**
356
559
  * Tokens por componente (spec 13). Solo los que no dependen del modo: los colores de los
357
560
  * componentes (`--mimi-<prefijo>-bg`, `-fg`, `-border`…) se cambian en el CSS (spec 6.2).
@@ -378,6 +581,19 @@ export interface MimiComponentTokens {
378
581
  tabs?: MimiTabsTokens;
379
582
  toolbar?: MimiToolbarTokens;
380
583
  pagination?: MimiPaginationTokens;
584
+ toggleGroup?: MimiToggleGroupTokens;
585
+ progress?: MimiProgressTokens;
586
+ numberInput?: MimiNumberInputTokens;
587
+ password?: MimiPasswordTokens;
588
+ inputMask?: MimiInputMaskTokens;
589
+ inputOtp?: MimiInputOtpTokens;
590
+ chip?: MimiChipTokens;
591
+ tagsInput?: MimiTagsInputTokens;
592
+ slider?: MimiSliderTokens;
593
+ combobox?: MimiComboboxTokens;
594
+ date?: MimiDateTokens;
595
+ fileUpload?: MimiFileUploadTokens;
596
+ colorPicker?: MimiColorPickerTokens;
381
597
  }
382
598
 
383
599
  export interface MimiThemePreset {
@@ -36,9 +36,21 @@ function connectedPosition(
36
36
  : { originX: 'start', originY: y, overlayX: 'end', overlayY: y, offsetX: -offset };
37
37
  }
38
38
 
39
+ /** Las otras alineaciones, en orden de preferencia (la opuesta antes que el centro). */
40
+ const FALLBACK_ALIGNS: Record<MimiOverlayAlign, MimiOverlayAlign[]> = {
41
+ start: ['end', 'center'],
42
+ end: ['start', 'center'],
43
+ center: ['start', 'end'],
44
+ };
45
+
39
46
  /**
40
- * Posiciones para el CDK, en orden de preferencia: el lado y la alineación pedidos, el lado
41
- * opuesto y, por último, los otros dos lados centrados. `offset` es la separación con el trigger.
47
+ * Posiciones para el CDK, en orden de preferencia: el lado pedido con la alineación pedida y
48
+ * después con las otras dos; el lado opuesto, igual; y, por último, los otros dos lados
49
+ * centrados. `offset` es la separación con el trigger.
50
+ *
51
+ * Las otras alineaciones del mismo lado (5.17a) evitan que en una pantalla chica el panel se
52
+ * salga por un costado: en 360px, un menú de 240px alineado al inicio de un trigger cercano al
53
+ * borde derecho no cabía en ninguna de las cuatro posiciones de antes y el CDK lo dejaba fuera.
42
54
  */
43
55
  export function mimiConnectedPositions(
44
56
  side: MimiOverlaySide,
@@ -47,9 +59,10 @@ export function mimiConnectedPositions(
47
59
  ): ConnectedPosition[] {
48
60
  const others: MimiOverlaySide[] =
49
61
  side === 'top' || side === 'bottom' ? ['right', 'left'] : ['bottom', 'top'];
62
+ const aligns = [align, ...FALLBACK_ALIGNS[align]];
50
63
  return [
51
- connectedPosition(side, align, offset),
52
- connectedPosition(OPPOSITE[side], align, offset),
64
+ ...aligns.map((a) => connectedPosition(side, a, offset)),
65
+ ...aligns.map((a) => connectedPosition(OPPOSITE[side], a, offset)),
53
66
  ...others.map((other) => connectedPosition(other, 'center', offset)),
54
67
  ];
55
68
  }
package/dist/ui/index.js CHANGED
@@ -9,12 +9,17 @@ const items_1 = require("../init/items");
9
9
  const paths_1 = require("../init/paths");
10
10
  const registry_1 = require("../registry");
11
11
  const project_1 = require("../utils/project");
12
+ const tailwind_1 = require("../utils/tailwind");
12
13
  const write_files_1 = require("../utils/write-files");
13
- /** Componentes que se pueden agregar: los ítems `component` del registro. */
14
- exports.AVAILABLE = Object.entries(registry_1.registry.items)
15
- .filter(([, item]) => item.type === 'component')
14
+ const namesOf = (type) => Object.entries(registry_1.registry.items)
15
+ .filter(([, item]) => item.type === type)
16
16
  .map(([name]) => name)
17
17
  .sort();
18
+ /**
19
+ * Lo que se puede agregar: los ítems `component` del registro y, después, los `block` (B.1-c;
20
+ * el mismo orden que el x-prompt).
21
+ */
22
+ exports.AVAILABLE = [...namesOf('component'), ...namesOf('block')];
18
23
  /**
19
24
  * `ng g mimi button input form-field`: copia los componentes y sus dependencias del registro a la
20
25
  * carpeta de mimi.json. Sin nombres, pregunta (x-prompt de selección múltiple).
@@ -34,12 +39,14 @@ function ui(options) {
34
39
  items: [...plan.items, ...plan.implicit],
35
40
  implicit: plan.implicit,
36
41
  componentsDir: plan.componentsDir,
42
+ blocksDir: plan.blocksDir,
37
43
  overwrite: options.overwrite ?? false,
38
44
  version: registry_1.registry.version,
39
45
  report,
40
46
  }),
41
47
  updateMimiConfig(plan, report),
42
48
  updateGlobalCss(plan),
49
+ addBlocksSource(plan),
43
50
  ...Object.entries(plan.newPackages).map(([name, version]) => (0, utility_1.addDependency)(name, version, {
44
51
  type: utility_1.DependencyType.Default,
45
52
  existing: utility_1.ExistingBehavior.Skip,
@@ -56,6 +63,7 @@ function inspect(tree, options) {
56
63
  throw new schematics_1.SchematicsException('Mimi no está configurado en este proyecto (falta mimi.json). Ejecuta primero:\n' +
57
64
  ' ng add @mimi-ng/cli');
58
65
  }
66
+ (0, project_1.assertAliases)(config);
59
67
  const requested = [...new Set((options.components ?? []).map((name) => name.trim()))].filter(Boolean);
60
68
  if (requested.length === 0) {
61
69
  throw new schematics_1.SchematicsException(`Indica qué componentes agregar, por ejemplo: ng g mimi button\nDisponibles: ${exports.AVAILABLE.join(', ')}.`);
@@ -64,7 +72,7 @@ function inspect(tree, options) {
64
72
  if (unknown.length > 0) {
65
73
  const lines = unknown.map((name) => {
66
74
  const suggestion = closest(name);
67
- return `"${name}" no es un componente de Mimi.${suggestion ? ` ¿Quisiste decir ${suggestion}?` : ''}`;
75
+ return `"${name}" no es un componente ni un Block de Mimi.${suggestion ? ` ¿Quisiste decir ${suggestion}?` : ''}`;
68
76
  });
69
77
  throw new schematics_1.SchematicsException(`${lines.join('\n')}\nDisponibles: ${exports.AVAILABLE.join(', ')}.`);
70
78
  }
@@ -103,7 +111,132 @@ function inspect(tree, options) {
103
111
  }
104
112
  const implicit = [...new Set(items_1.INIT_ITEMS.flatMap((item) => (0, registry_1.resolveItems)(item)))].filter((item) => !items.includes(item));
105
113
  const componentsDir = (0, paths_1.normalizePath)(config.aliases?.components ?? 'src/app/components/ui');
106
- return { componentsDir, config, items, implicit, newPackages };
114
+ const blocksDir = (0, paths_1.normalizePath)(config.aliases?.blocks ?? (0, project_1.defaultBlocksDir)(componentsDir));
115
+ const outdated = outdatedPeers(tree, items, installed, newPackages);
116
+ const updatable = PEERS_WITH_MINIMUM.filter((name) => installed[name] || newPackages[name]);
117
+ const missingExclusion = mimiExclusion(tree, config, installed);
118
+ return {
119
+ componentsDir,
120
+ blocksDir,
121
+ config,
122
+ items,
123
+ implicit,
124
+ newPackages,
125
+ outdated,
126
+ updatable,
127
+ missingExclusion,
128
+ };
129
+ }
130
+ /**
131
+ * Sin `@source not` hacia `.mimi/`, Tailwind escanea las copias originales (van en git) y genera
132
+ * sus clases aunque el usuario ya las haya cambiado en sus componentes. Proyectos de la 0.3.0 o
133
+ * anteriores no la tienen: se avisa para que no tengan que volver a ejecutar ng add. Con
134
+ * Tailwind 4.0 no se avisa aquí (no la entiende; init ya lo explicó).
135
+ */
136
+ function mimiExclusion(tree, config, installed) {
137
+ const cssPath = config.tailwind?.css;
138
+ if (!cssPath || !tree.exists(cssPath))
139
+ return null;
140
+ if (!(0, tailwind_1.tailwindSupportsSourceNot)(tree, installed['tailwindcss']))
141
+ return null;
142
+ if (!(0, tailwind_1.missingMimiExclusion)(tree.readText(cssPath), cssPath))
143
+ return null;
144
+ return { cssPath, line: (0, tailwind_1.mimiSourceNotLine)(cssPath) };
145
+ }
146
+ // ── Versiones mínimas de cdk y aria (M.4) ─────────────────────────────────────────────
147
+ /**
148
+ * Paquetes cuya versión mínima se vigila. La CLI no los cambia si el proyecto ya los tiene (se
149
+ * actualizan con `ng update`, junto con el resto de Angular), pero avisa si quedan por debajo de
150
+ * lo que pide el registro: con aria y cdk 22.2.3, Combobox y Select lanzan NG0950 (spec 4,
151
+ * «#33974»).
152
+ */
153
+ const PEERS_WITH_MINIMUM = ['@angular/cdk', '@angular/aria'];
154
+ const RANGE = /^\s*(\^|~|>=|=)?\s*v?(\d+)\.(\d+)\.(\d+)\s*$/;
155
+ function parseRange(range) {
156
+ const match = RANGE.exec(range);
157
+ if (!match)
158
+ return null;
159
+ return { op: match[1] ?? '=', version: [+match[2], +match[3], +match[4]] };
160
+ }
161
+ function compare(a, b) {
162
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
163
+ }
164
+ /**
165
+ * Si un rango puede instalar `required` o una posterior. Lo que no se entiende (`*`, `latest`,
166
+ * `22.2.x`, `workspace:`…) cuenta como que sí: el aviso no da falsas alarmas.
167
+ */
168
+ function canReach(range, required) {
169
+ const parsed = parseRange(range);
170
+ if (!parsed)
171
+ return true;
172
+ if (parsed.op === '>=' || compare(parsed.version, required) >= 0)
173
+ return true;
174
+ // Por debajo del mínimo: llega solo si el tope del rango (exclusivo) queda por encima.
175
+ const [major, minor, patch] = parsed.version;
176
+ const upper = parsed.op === '^'
177
+ ? major > 0
178
+ ? [major + 1, 0, 0]
179
+ : minor > 0
180
+ ? [0, minor + 1, 0]
181
+ : [0, 0, patch + 1]
182
+ : parsed.op === '~'
183
+ ? [major, minor + 1, 0]
184
+ : null;
185
+ return upper !== null && compare(required, upper) < 0;
186
+ }
187
+ /** El mínimo que piden los ítems para cada paquete vigilado (aria implica el mismo cdk). */
188
+ function requiredMinimums(items) {
189
+ const minimums = new Map();
190
+ for (const item of items) {
191
+ const { dependencies = {}, peerDependencies = {} } = registry_1.registry.items[item];
192
+ for (const [name, range] of Object.entries({ ...dependencies, ...peerDependencies })) {
193
+ if (!PEERS_WITH_MINIMUM.includes(name))
194
+ continue;
195
+ const version = parseRange(range)?.version;
196
+ if (!version)
197
+ continue;
198
+ const names = name === '@angular/aria' ? [name, '@angular/cdk'] : [name];
199
+ for (const target of names) {
200
+ const known = minimums.get(target);
201
+ if (!known || compare(version, known) > 0)
202
+ minimums.set(target, version);
203
+ }
204
+ }
205
+ }
206
+ return minimums;
207
+ }
208
+ function outdatedPeers(tree, items, installed, newPackages) {
209
+ const outdated = [];
210
+ for (const [name, required] of requiredMinimums(items)) {
211
+ const declared = installed[name] ?? newPackages[name];
212
+ if (!declared)
213
+ continue;
214
+ // Si ya está instalado, manda la versión de node_modules (la que corre de verdad).
215
+ const onDisk = installed[name] ? installedVersion(tree, name) : null;
216
+ const version = onDisk ? parseRange(onDisk)?.version : null;
217
+ const ok = version ? compare(version, required) >= 0 : canReach(declared, required);
218
+ if (!ok) {
219
+ outdated.push({
220
+ name,
221
+ current: onDisk ?? declared,
222
+ source: onDisk ? 'installed' : installed[name] ? 'declared' : 'added',
223
+ required: required.join('.'),
224
+ });
225
+ }
226
+ }
227
+ return outdated.sort((a, b) => a.name.localeCompare(b.name));
228
+ }
229
+ const SOURCE_TEXT = {
230
+ installed: (current) => `${current} instalada`,
231
+ declared: (current) => `"${current}" en package.json`,
232
+ added: (current) => `"${current}", que se agrega ahora`,
233
+ };
234
+ function installedVersion(tree, name) {
235
+ const file = tree.read(`node_modules/${name}/package.json`);
236
+ if (!file)
237
+ return null;
238
+ const { version } = JSON.parse(file.toString('utf-8'));
239
+ return version ?? null;
107
240
  }
108
241
  /**
109
242
  * @angular/aria pide como peer la versión exacta de @angular/cdk (22.1.7 → cdk 22.1.7). Si el cdk
@@ -155,16 +288,25 @@ function updateMimiConfig(plan, report) {
155
288
  const skipped = new Set(report.skipped);
156
289
  const blocked = new Set(report.blocked.map((b) => b.item));
157
290
  const components = { ...plan.config.components };
291
+ const blocks = { ...plan.config.blocks };
158
292
  for (const item of plan.items) {
159
- if (registry_1.registry.items[item].type !== 'component')
293
+ const { type, files } = registry_1.registry.items[item];
294
+ if (type !== 'component' && type !== 'block')
160
295
  continue;
161
296
  if (blocked.has(item))
162
297
  continue;
163
- if (registry_1.registry.items[item].files.some((file) => skipped.has(file)))
298
+ if (files.some((file) => skipped.has(file)))
164
299
  continue;
165
- components[item] = { version: registry_1.registry.version };
300
+ (type === 'block' ? blocks : components)[item] = { version: registry_1.registry.version };
166
301
  }
167
302
  const next = { ...plan.config, components };
303
+ if (Object.keys(blocks).length > 0) {
304
+ next.blocks = blocks;
305
+ // Proyectos de la 0.3.0 o anteriores: la carpeta queda escrita la primera vez (B.1-a).
306
+ if (!plan.config.aliases?.blocks) {
307
+ next.aliases = { ...plan.config.aliases, blocks: plan.blocksDir };
308
+ }
309
+ }
168
310
  const text = `${JSON.stringify(next, null, 2)}\n`;
169
311
  if (tree.readText('mimi.json') !== text)
170
312
  tree.overwrite('mimi.json', text);
@@ -202,11 +344,58 @@ function updateGlobalCss(plan) {
202
344
  context.logger.info(` ${line}`);
203
345
  };
204
346
  }
347
+ /**
348
+ * G1: con `@import "tailwindcss" source(none)`, Tailwind no escanea la carpeta de los Blocks y
349
+ * la pantalla queda sin sus estilos. Si se copia un Block, se agrega su `@source` (una vez).
350
+ */
351
+ function addBlocksSource(plan) {
352
+ return (tree, context) => {
353
+ if (!plan.items.some((item) => registry_1.registry.items[item].type === 'block'))
354
+ return;
355
+ const cssPath = plan.config.tailwind?.css;
356
+ if (!cssPath || !tree.exists(cssPath))
357
+ return;
358
+ const result = (0, tailwind_1.withBlocksSource)(tree.readText(cssPath), cssPath, plan.componentsDir, plan.blocksDir);
359
+ if (!result)
360
+ return;
361
+ tree.overwrite(cssPath, result.css);
362
+ context.logger.info(`Agregado a ${cssPath} (tu @import de Tailwind usa source(none), así que Tailwind solo ` +
363
+ `escanea las carpetas que indicas):\n ${result.line}`);
364
+ };
365
+ }
205
366
  // ── Mensaje final ─────────────────────────────────────────────────────────────────────
367
+ /**
368
+ * M1: un componente que se actualizó y que usa un Block instalado (en `mimi.json → blocks`) que
369
+ * no se actualizó en esta ejecución (porque el usuario lo modificó, o porque no se pidió). Solo
370
+ * avisa: la pantalla es del usuario y puede necesitar cambios con la API nueva.
371
+ */
372
+ function blocksUsingUpdated(plan, report, itemOf) {
373
+ const updated = new Set(report.updated.map(({ file }) => itemOf(file)));
374
+ const messages = [];
375
+ for (const block of Object.keys(plan.config.blocks ?? {}).sort()) {
376
+ if (updated.has(block) || registry_1.registry.items[block]?.type !== 'block')
377
+ continue;
378
+ const used = (0, registry_1.resolveItems)(block).filter((item) => updated.has(item) && registry_1.registry.items[item].type === 'component');
379
+ if (used.length === 0)
380
+ continue;
381
+ const list = used.join(', ');
382
+ messages.push(`Tu Block ${block} usa ${list}, que se actualizó a ${registry_1.registry.version}; revisa el ` +
383
+ 'CHANGELOG por si tu pantalla necesita algún cambio.');
384
+ }
385
+ return messages;
386
+ }
387
+ /**
388
+ * Cómo se importa un Block desde `app.routes.ts` (en `src/app/`): `src/app/blocks` →
389
+ * `./blocks/login-01`. Si la carpeta no está dentro de `app/`, la ruta del workspace.
390
+ */
391
+ function routesImport(blocksDir, item) {
392
+ const at = `/${blocksDir}`.lastIndexOf('/app/');
393
+ return at >= 0 ? `./${`/${blocksDir}`.slice(at + 5)}/${item}` : `${blocksDir}/${item}`;
394
+ }
206
395
  function summary(plan, report) {
207
396
  return (_tree, context) => {
208
397
  const log = context.logger;
209
- const path = (file) => `${plan.componentsDir}/${(0, write_files_1.projectPathOf)(file)}`;
398
+ const path = (file) => (0, write_files_1.targetPathOf)(file, plan.componentsDir, plan.blocksDir);
210
399
  if (report.created.length > 0) {
211
400
  log.info('Agregado:');
212
401
  for (const file of report.created)
@@ -216,17 +405,38 @@ function summary(plan, report) {
216
405
  log.info('Reemplazado (--overwrite):');
217
406
  for (const file of report.overwritten)
218
407
  log.info(` ~ ${path(file)}`);
408
+ // Un Block es una pantalla del usuario: --overwrite la reemplaza entera (B.1, trampas).
409
+ const screens = [
410
+ ...new Set(report.overwritten.filter(write_files_1.isBlockFile).map((file) => file.split('/')[1])),
411
+ ];
412
+ for (const block of screens) {
413
+ log.warn(`--overwrite reemplazó tu pantalla ${block} (${plan.blocksDir}/${block}/) por la de ` +
414
+ `Mimi ${registry_1.registry.version}. Si tenías cambios, recupéralos de git.`);
415
+ }
219
416
  }
220
417
  const itemOf = (file) => [...plan.items, ...plan.implicit].find((item) => registry_1.registry.items[item].files.includes(file)) ??
221
418
  file;
222
419
  for (const message of (0, write_files_1.updatedMessages)(report, itemOf, registry_1.registry.version))
223
420
  log.info(message);
421
+ for (const message of blocksUsingUpdated(plan, report, itemOf))
422
+ log.warn(message);
224
423
  for (const file of report.skipped) {
225
424
  log.warn(`Omitido: ${path(file)} ya existe con otro contenido (usa --overwrite para reemplazarlo).`);
226
425
  }
227
- for (const message of (0, write_files_1.blockedMessages)(report, plan.componentsDir, registry_1.registry.version)) {
426
+ for (const message of (0, write_files_1.blockedMessages)(report, plan.componentsDir, registry_1.registry.version, plan.blocksDir)) {
228
427
  log.warn(message);
229
428
  }
429
+ // Un Block nuevo es una página: se enruta (B.1, «API pública»).
430
+ const created = new Set(report.created);
431
+ for (const item of plan.items) {
432
+ const { type, files } = registry_1.registry.items[item];
433
+ if (type !== 'block' || !files.some((file) => created.has(file)))
434
+ continue;
435
+ const className = item.replace(/(^|-)([a-z0-9])/g, (_, _dash, c) => c.toUpperCase());
436
+ log.info(`${item} es una pantalla: agrégala a tus rutas, por ejemplo ` +
437
+ `{ path: '${item.split('-')[0]}', component: ${className} } ` +
438
+ `(import { ${className} } from '${routesImport(plan.blocksDir, item)}').`);
439
+ }
230
440
  if (!(0, write_files_1.hasFileChanges)(report) && report.skipped.length === 0 && report.blocked.length === 0) {
231
441
  log.info('Nada que hacer: los archivos ya están al día.');
232
442
  }
@@ -236,6 +446,18 @@ function summary(plan, report) {
236
446
  for (const [name, version] of packages)
237
447
  log.info(` ${name}@${version}`);
238
448
  }
449
+ if (plan.outdated.length > 0) {
450
+ const required = [...new Set(plan.outdated.map((peer) => peer.required))].join(', ');
451
+ const lines = plan.outdated.map((peer) => ` ${peer.name}: ${SOURCE_TEXT[peer.source](peer.current)}`);
452
+ log.warn(`Mimi necesita @angular/cdk y @angular/aria ${required} o posterior (las anteriores ` +
453
+ `tienen errores conocidos con sus componentes) y tu proyecto tiene:\n${lines.join('\n')}\n` +
454
+ `Actualízalos con:\n ng update ${plan.updatable.join(' ')}`);
455
+ }
456
+ if (plan.missingExclusion) {
457
+ const { cssPath, line } = plan.missingExclusion;
458
+ log.warn(`Tailwind escanea ${write_files_1.BASE_DIR}/ (las copias originales de Mimi) y genera CSS de clases que ` +
459
+ `quizá ya no usas. Agrega a ${cssPath}, después de @import "tailwindcss":\n ${line}`);
460
+ }
239
461
  if ((0, write_files_1.hasFileChanges)(report)) {
240
462
  log.info(`Guarda la carpeta ${write_files_1.BASE_DIR.split('/')[0]}/ en git: Mimi la usa para actualizar tus ` +
241
463
  'componentes sin perder tus cambios.');