@mimi-ng/cli 0.3.0 → 0.5.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 (50) hide show
  1. package/README.md +42 -1
  2. package/dist/init/css.js +1 -1
  3. package/dist/init/index.js +33 -0
  4. package/dist/init/paths.js +20 -0
  5. package/dist/registry.js +4 -1
  6. package/dist/registry.json +198 -1
  7. package/dist/templates/blocks/login-01/index.ts +1 -0
  8. package/dist/templates/blocks/login-01/login-01.html +174 -0
  9. package/dist/templates/blocks/login-01/login-01.ts +114 -0
  10. package/dist/templates/blocks/login-02/index.ts +1 -0
  11. package/dist/templates/blocks/login-02/login-02.html +186 -0
  12. package/dist/templates/blocks/login-02/login-02.ts +101 -0
  13. package/dist/templates/blocks/otp-01/index.ts +1 -0
  14. package/dist/templates/blocks/otp-01/otp-01.html +166 -0
  15. package/dist/templates/blocks/otp-01/otp-01.ts +221 -0
  16. package/dist/templates/blocks/pricing-01/index.ts +1 -0
  17. package/dist/templates/blocks/pricing-01/pricing-01.html +173 -0
  18. package/dist/templates/blocks/pricing-01/pricing-01.ts +178 -0
  19. package/dist/templates/blocks/settings-01/index.ts +1 -0
  20. package/dist/templates/blocks/settings-01/settings-01.html +424 -0
  21. package/dist/templates/blocks/settings-01/settings-01.ts +392 -0
  22. package/dist/templates/blocks/signup-01/index.ts +1 -0
  23. package/dist/templates/blocks/signup-01/signup-01.html +332 -0
  24. package/dist/templates/blocks/signup-01/signup-01.ts +186 -0
  25. package/dist/templates/blocks/table-01/index.ts +1 -0
  26. package/dist/templates/blocks/table-01/table-01.html +214 -0
  27. package/dist/templates/blocks/table-01/table-01.ts +266 -0
  28. package/dist/templates/components/checkbox/checkbox.ts +14 -5
  29. package/dist/templates/components/command/command-texts.ts +65 -0
  30. package/dist/templates/components/command/command.ts +553 -0
  31. package/dist/templates/components/command/index.ts +13 -0
  32. package/dist/templates/components/data-table/data-table-texts.ts +110 -0
  33. package/dist/templates/components/data-table/data-table.ts +1153 -0
  34. package/dist/templates/components/data-table/index.ts +18 -0
  35. package/dist/templates/components/sortable-list/index.ts +9 -0
  36. package/dist/templates/components/sortable-list/sortable-list-texts.ts +71 -0
  37. package/dist/templates/components/sortable-list/sortable-list.ts +510 -0
  38. package/dist/templates/components/tabs/tabs.ts +88 -23
  39. package/dist/templates/components/tree/index.ts +9 -0
  40. package/dist/templates/components/tree/tree-texts.ts +45 -0
  41. package/dist/templates/components/tree/tree.ts +730 -0
  42. package/dist/templates/theme/provider.ts +4 -0
  43. package/dist/templates/theme/types.ts +84 -0
  44. package/dist/ui/index.js +323 -13
  45. package/dist/ui/schema.json +50 -2
  46. package/dist/utils/pages.js +138 -0
  47. package/dist/utils/project.js +59 -0
  48. package/dist/utils/tailwind.js +84 -0
  49. package/dist/utils/write-files.js +63 -8
  50. package/package.json +1 -1
@@ -101,6 +101,10 @@ const COMPONENT_PREFIX: Record<keyof MimiComponentTokens, string> = {
101
101
  date: 'date',
102
102
  fileUpload: 'upload',
103
103
  colorPicker: 'color-picker',
104
+ tree: 'tree',
105
+ sortable: 'sortable',
106
+ command: 'command',
107
+ dataTable: 'table',
104
108
  };
105
109
 
106
110
  /** Nombres de la 0.1.0 que siguen funcionando hasta la 1.0 (spec 12): ruta → qué usar. */
@@ -555,6 +555,86 @@ export interface MimiColorPickerTokens {
555
555
  swatchGap?: string;
556
556
  }
557
557
 
558
+ /**
559
+ * Tree. Fondo, hover y el nodo elegido van en el CSS (`--mimi-tree-bg`, `-hover`, `-selected`,
560
+ * `-selected-fg`).
561
+ */
562
+ export interface MimiTreeTokens {
563
+ /** Relleno del contenedor (6px). */
564
+ padding?: string;
565
+ /** Alto de cada nodo (32px). */
566
+ rowHeight?: string;
567
+ /** Radio del nodo (el global `radius-sm`). */
568
+ rowRadius?: string;
569
+ /** Sangría por nivel (18px). */
570
+ indent?: string;
571
+ /** Tamaño de texto (13.5px). */
572
+ fontSize?: string;
573
+ }
574
+
575
+ /**
576
+ * Sortable List. Fondo, el hover del asa y los colores del hueco van en el CSS
577
+ * (`--mimi-sortable-bg`, `-handle-hover`, `-drop`, `-drop-bg`).
578
+ */
579
+ export interface MimiSortableTokens {
580
+ /** Alto mínimo de cada elemento (56px). */
581
+ itemHeight?: string;
582
+ /** Separación entre elementos (8px). */
583
+ gap?: string;
584
+ /** Radio del elemento y del hueco (el global `radius`). */
585
+ radius?: string;
586
+ /** Giro y escala del elemento levantado (`rotate(-1.5deg) scale(1.03)`). */
587
+ lift?: string;
588
+ /** Curva del reacomodo (`cubic-bezier(.2, .8, .2, 1)`). */
589
+ easing?: string;
590
+ }
591
+
592
+ /**
593
+ * Command. El fondo del comando activo va en el CSS (`--mimi-command-active`).
594
+ */
595
+ export interface MimiCommandTokens {
596
+ /** Ancho del panel en el diálogo (560px). */
597
+ width?: string;
598
+ /** Distancia del panel al borde superior en el diálogo (36px). */
599
+ top?: string;
600
+ /** Alto de la fila del buscador (52px). */
601
+ inputHeight?: string;
602
+ /** Tamaño del texto del buscador (15px). */
603
+ inputFontSize?: string;
604
+ /** Alto máximo de la lista (332px). */
605
+ listHeight?: string;
606
+ /** Alto de cada comando (40px). */
607
+ itemHeight?: string;
608
+ /** Radio del comando (el global `radius-sm`). */
609
+ itemRadius?: string;
610
+ /** Caja del ícono del comando (26px). */
611
+ iconSize?: string;
612
+ }
613
+
614
+ /**
615
+ * Data Table. Los fondos (tabla, encabezado, hover, elegida, detalle) y la sombra de la columna
616
+ * fija van en el CSS (`--mimi-table-bg`, `-head-bg`, `-row-hover`, `-row-selected`,
617
+ * `-expanded-bg`, `-sticky-shadow`).
618
+ */
619
+ export interface MimiTableTokens {
620
+ /** Alto de fila en la densidad compacta (42px). */
621
+ rowHeight?: string;
622
+ /** Alto de fila en la densidad cómoda (60px). */
623
+ rowHeightComfortable?: string;
624
+ /** Alto del encabezado, compacta (38px). */
625
+ headHeight?: string;
626
+ /** Alto del encabezado, cómoda (44px). */
627
+ headHeightComfortable?: string;
628
+ /** Texto de las celdas, compacta (13px). */
629
+ fontSize?: string;
630
+ /** Texto de las celdas, cómoda (14px). */
631
+ fontSizeComfortable?: string;
632
+ /** Relleno horizontal de la celda (10px). */
633
+ cellPx?: string;
634
+ /** Alto mínimo de la barra (56px). */
635
+ toolbarHeight?: string;
636
+ }
637
+
558
638
  /**
559
639
  * Tokens por componente (spec 13). Solo los que no dependen del modo: los colores de los
560
640
  * componentes (`--mimi-<prefijo>-bg`, `-fg`, `-border`…) se cambian en el CSS (spec 6.2).
@@ -594,6 +674,10 @@ export interface MimiComponentTokens {
594
674
  date?: MimiDateTokens;
595
675
  fileUpload?: MimiFileUploadTokens;
596
676
  colorPicker?: MimiColorPickerTokens;
677
+ tree?: MimiTreeTokens;
678
+ sortable?: MimiSortableTokens;
679
+ command?: MimiCommandTokens;
680
+ dataTable?: MimiTableTokens;
597
681
  }
598
682
 
599
683
  export interface MimiThemePreset {
package/dist/ui/index.js CHANGED
@@ -8,13 +8,19 @@ const css_1 = require("../init/css");
8
8
  const items_1 = require("../init/items");
9
9
  const paths_1 = require("../init/paths");
10
10
  const registry_1 = require("../registry");
11
+ const pages_1 = require("../utils/pages");
11
12
  const project_1 = require("../utils/project");
13
+ const tailwind_1 = require("../utils/tailwind");
12
14
  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')
15
+ const namesOf = (type) => Object.entries(registry_1.registry.items)
16
+ .filter(([, item]) => item.type === type)
16
17
  .map(([name]) => name)
17
18
  .sort();
19
+ /**
20
+ * Lo que se puede agregar: los ítems `component` del registro y, después, los `block` (B.1-c;
21
+ * el mismo orden que el x-prompt).
22
+ */
23
+ exports.AVAILABLE = [...namesOf('component'), ...namesOf('block')];
18
24
  /**
19
25
  * `ng g mimi button input form-field`: copia los componentes y sus dependencias del registro a la
20
26
  * carpeta de mimi.json. Sin nombres, pregunta (x-prompt de selección múltiple).
@@ -34,12 +40,15 @@ function ui(options) {
34
40
  items: [...plan.items, ...plan.implicit],
35
41
  implicit: plan.implicit,
36
42
  componentsDir: plan.componentsDir,
43
+ blocksDir: plan.blocksDir,
44
+ pages: plan.pages,
37
45
  overwrite: options.overwrite ?? false,
38
46
  version: registry_1.registry.version,
39
47
  report,
40
48
  }),
41
49
  updateMimiConfig(plan, report),
42
50
  updateGlobalCss(plan),
51
+ addFolderSources(plan),
43
52
  ...Object.entries(plan.newPackages).map(([name, version]) => (0, utility_1.addDependency)(name, version, {
44
53
  type: utility_1.DependencyType.Default,
45
54
  existing: utility_1.ExistingBehavior.Skip,
@@ -56,6 +65,7 @@ function inspect(tree, options) {
56
65
  throw new schematics_1.SchematicsException('Mimi no está configurado en este proyecto (falta mimi.json). Ejecuta primero:\n' +
57
66
  ' ng add @mimi-ng/cli');
58
67
  }
68
+ (0, project_1.assertAliases)(config);
59
69
  const requested = [...new Set((options.components ?? []).map((name) => name.trim()))].filter(Boolean);
60
70
  if (requested.length === 0) {
61
71
  throw new schematics_1.SchematicsException(`Indica qué componentes agregar, por ejemplo: ng g mimi button\nDisponibles: ${exports.AVAILABLE.join(', ')}.`);
@@ -64,7 +74,7 @@ function inspect(tree, options) {
64
74
  if (unknown.length > 0) {
65
75
  const lines = unknown.map((name) => {
66
76
  const suggestion = closest(name);
67
- return `"${name}" no es un componente de Mimi.${suggestion ? ` ¿Quisiste decir ${suggestion}?` : ''}`;
77
+ return `"${name}" no es un componente ni un Block de Mimi.${suggestion ? ` ¿Quisiste decir ${suggestion}?` : ''}`;
68
78
  });
69
79
  throw new schematics_1.SchematicsException(`${lines.join('\n')}\nDisponibles: ${exports.AVAILABLE.join(', ')}.`);
70
80
  }
@@ -103,9 +113,170 @@ function inspect(tree, options) {
103
113
  }
104
114
  const implicit = [...new Set(items_1.INIT_ITEMS.flatMap((item) => (0, registry_1.resolveItems)(item)))].filter((item) => !items.includes(item));
105
115
  const componentsDir = (0, paths_1.normalizePath)(config.aliases?.components ?? 'src/app/components/ui');
116
+ const blocksDir = (0, paths_1.normalizePath)(config.aliases?.blocks ?? (0, project_1.defaultBlocksDir)(componentsDir));
117
+ const pagesDir = (0, paths_1.normalizePath)(config.aliases?.pages ?? (0, project_1.defaultPagesDir)(componentsDir));
118
+ const { pages, legacyPages } = planPages(tree, config, items, options.name, blocksDir, pagesDir);
106
119
  const outdated = outdatedPeers(tree, items, installed, newPackages);
107
120
  const updatable = PEERS_WITH_MINIMUM.filter((name) => installed[name] || newPackages[name]);
108
- return { componentsDir, config, items, implicit, newPackages, outdated, updatable };
121
+ const missingExclusion = mimiExclusion(tree, config, installed);
122
+ return {
123
+ componentsDir,
124
+ blocksDir,
125
+ pagesDir,
126
+ pages,
127
+ legacyPages,
128
+ config,
129
+ items,
130
+ implicit,
131
+ newPackages,
132
+ outdated,
133
+ updatable,
134
+ missingExclusion,
135
+ };
136
+ }
137
+ // ── Páginas (B.11) ────────────────────────────────────────────────────────────────────
138
+ /**
139
+ * Dónde va cada Block de tipo `page` de esta ejecución. Todo se comprueba antes de escribir: si
140
+ * algo no cuadra (un `--name` inválido, una carpeta que ya existe y no es esa página, dos páginas
141
+ * en la misma carpeta), falla sin cambiar nada.
142
+ *
143
+ * - Ya generada (`mimi.json → blocks.<ítem>.path`): se actualiza ahí, con su nombre (B.11-f).
144
+ * - Instalada con la 0.4.0 (`<aliases.blocks>/<ítem>/<ítem>.ts`, sin `path`): se queda donde está
145
+ * y se actualiza como antes (B.11-g).
146
+ * - Nueva: `<aliases.pages>/<nombre>/`, con `--name` o el nombre por defecto (B.11-c).
147
+ */
148
+ function planPages(tree, config, items, name, blocksDir, pagesDir) {
149
+ const pageItems = items.filter((item) => (0, registry_1.isPageBlock)(item));
150
+ if (name !== undefined) {
151
+ if (!pages_1.PAGE_NAME.test(name)) {
152
+ throw new schematics_1.SchematicsException(`--name ${JSON.stringify(name)} no sirve como nombre de página: usa minúsculas, números y ` +
153
+ 'guiones, empezando con una letra (por ejemplo, --name admin-login). No se cambió nada.');
154
+ }
155
+ if (pageItems.length !== 1) {
156
+ const pageNames = exports.AVAILABLE.filter((item) => (0, registry_1.isPageBlock)(item)).join(', ');
157
+ throw new schematics_1.SchematicsException((pageItems.length === 0
158
+ ? '--name es el nombre de la página que se genera, y no pediste ningún Block de tipo página'
159
+ : `--name sirve para una sola página, y pediste ${pageItems.join(', ')}: agrégalas por separado`) +
160
+ ` (son páginas: ${pageNames}). No se cambió nada.`);
161
+ }
162
+ if ((0, pages_1.isBlockLikeName)(pageItems[0], name)) {
163
+ throw new schematics_1.SchematicsException(`--name ${name} lleva el nombre del Block (${pageItems[0]}): elige el de tu página, por ` +
164
+ `ejemplo --name ${(0, pages_1.defaultPageName)(pageItems[0])} o --name admin-` +
165
+ `${(0, pages_1.defaultPageName)(pageItems[0])}. No se cambió nada.`);
166
+ }
167
+ }
168
+ const prefix = projectPrefix(tree, pagesDir);
169
+ const pages = {};
170
+ const legacyPages = [];
171
+ const taken = new Map();
172
+ for (const item of pageItems) {
173
+ const entry = config.blocks?.[item];
174
+ let target;
175
+ if (entry?.path) {
176
+ const dir = (0, paths_1.normalizePath)(entry.path);
177
+ const trackedName = entry.name && pages_1.PAGE_NAME.test(entry.name) ? entry.name : basename(dir);
178
+ if (name !== undefined && name !== trackedName) {
179
+ throw new schematics_1.SchematicsException(`${item} ya es tu página ${trackedName} (${dir}/): Mimi la sigue por su nombre del ` +
180
+ `registro, así que está en un solo sitio. Para actualizarla, ejecuta ng g mimi ${item} ` +
181
+ 'sin --name. Si quieres otra copia con otro nombre, renombra o mueve la tuya y cambia ' +
182
+ `"path" y "name" de blocks.${item} en mimi.json. No se cambió nada.`);
183
+ }
184
+ target = { item, dir, name: trackedName, prefix };
185
+ }
186
+ else if (tree.exists(`${blocksDir}/${item}/${item}.ts`)) {
187
+ if (name !== undefined) {
188
+ throw new schematics_1.SchematicsException(`${item} está instalado como en la 0.4.0, en ${blocksDir}/${item}/, y se sigue ` +
189
+ 'actualizando ahí. Para generarlo como página, guarda tus cambios (por ejemplo, en git), ' +
190
+ `borra esa carpeta y vuelve a ejecutar el comando. No se cambió nada.`);
191
+ }
192
+ legacyPages.push(item);
193
+ continue;
194
+ }
195
+ else {
196
+ const pageName = name ?? (0, pages_1.defaultPageName)(item);
197
+ const dir = `${pagesDir}/${pageName}`;
198
+ if (hasContent(tree, dir)) {
199
+ const owner = Object.entries(config.blocks ?? {}).find(([, other]) => other?.path && (0, paths_1.normalizePath)(other.path) === dir)?.[0];
200
+ throw new schematics_1.SchematicsException(`La carpeta ${dir}/ ya existe${owner ? ` (es tu página de ${owner})` : ''}, así que ` +
201
+ `${item} no se generó ahí: Mimi no sobrescribe tus archivos. Elige otro nombre con ` +
202
+ `--name, por ejemplo: ng g mimi ${item} --name ${pageName}-2. No se cambió nada.`);
203
+ }
204
+ target = { item, dir, name: pageName, prefix };
205
+ }
206
+ const other = taken.get(target.dir);
207
+ if (other) {
208
+ throw new schematics_1.SchematicsException(`${other} y ${item} irían a la misma carpeta (${target.dir}/). Agrégalos por separado, ` +
209
+ `uno de ellos con --name (por ejemplo: ng g mimi ${item} --name ${target.name}-2). ` +
210
+ 'No se cambió nada.');
211
+ }
212
+ taken.set(target.dir, item);
213
+ pages[item] = target;
214
+ }
215
+ return { pages, legacyPages };
216
+ }
217
+ const basename = (path) => path.slice(path.lastIndexOf('/') + 1);
218
+ /** ¿La carpeta existe y tiene algo dentro? (Un árbol de schematics no tiene carpetas vacías.) */
219
+ function hasContent(tree, dir) {
220
+ const entry = tree.getDir(dir);
221
+ return entry.subfiles.length + entry.subdirs.length > 0;
222
+ }
223
+ /**
224
+ * Prefijo de los selectores (`app`, de angular.json → projects → prefix): el del proyecto que
225
+ * contiene la carpeta de páginas o, si no, el de la única aplicación. Si no se sabe, `app`.
226
+ */
227
+ function projectPrefix(tree, dir) {
228
+ let workspace = null;
229
+ try {
230
+ workspace = (0, project_1.readJson)(tree, 'angular.json');
231
+ }
232
+ catch {
233
+ return 'app';
234
+ }
235
+ const projects = Object.values(workspace?.projects ?? {});
236
+ const sourceOf = (project) => (0, paths_1.normalizePath)(project.sourceRoot ?? `${project.root ?? ''}/src`);
237
+ const inside = projects
238
+ .filter((project) => `${dir}/`.startsWith(`${sourceOf(project)}/`))
239
+ .sort((a, b) => sourceOf(b).length - sourceOf(a).length);
240
+ const apps = projects.filter((project) => project.projectType === 'application');
241
+ const project = inside[0] ?? (apps.length === 1 ? apps[0] : undefined);
242
+ return typeof project?.prefix === 'string' && /^[a-z][a-z0-9-]*$|^$/.test(project.prefix)
243
+ ? project.prefix
244
+ : 'app';
245
+ }
246
+ /**
247
+ * Cómo importa `app.routes.ts` el archivo de rutas de una página: relativo a la carpeta del
248
+ * `app.routes.ts` que está por encima de la página (`src/app/pages/login` →
249
+ * `./pages/login/login.routes`). Se busca subiendo desde la carpeta de arriba de la página (nunca
250
+ * la suya: una página `app` tiene su propio `app.routes.ts`); si no hay ninguno, la carpeta `app/`
251
+ * más cercana por encima. Si tampoco, la ruta del workspace (y `appDir` null: hay que ajustarla).
252
+ */
253
+ function routesImportOf(tree, page) {
254
+ const routes = `${page.dir}/${page.name}.routes`;
255
+ const ancestors = [];
256
+ const parts = page.dir.split('/');
257
+ for (let length = parts.length - 1; length > 0; length--) {
258
+ ancestors.push(parts.slice(0, length).join('/'));
259
+ }
260
+ const appDir = ancestors.find((dir) => tree.exists(`${dir}/app.routes.ts`)) ??
261
+ ancestors.find((dir) => basename(dir) === 'app') ??
262
+ null;
263
+ return appDir ? { path: (0, paths_1.relativePath)(appDir, routes), appDir } : { path: routes, appDir: null };
264
+ }
265
+ /**
266
+ * Sin `@source not` hacia `.mimi/`, Tailwind escanea las copias originales (van en git) y genera
267
+ * sus clases aunque el usuario ya las haya cambiado en sus componentes. Proyectos de la 0.3.0 o
268
+ * anteriores no la tienen: se avisa para que no tengan que volver a ejecutar ng add. Con
269
+ * Tailwind 4.0 no se avisa aquí (no la entiende; init ya lo explicó).
270
+ */
271
+ function mimiExclusion(tree, config, installed) {
272
+ const cssPath = config.tailwind?.css;
273
+ if (!cssPath || !tree.exists(cssPath))
274
+ return null;
275
+ if (!(0, tailwind_1.tailwindSupportsSourceNot)(tree, installed['tailwindcss']))
276
+ return null;
277
+ if (!(0, tailwind_1.missingMimiExclusion)(tree.readText(cssPath), cssPath))
278
+ return null;
279
+ return { cssPath, line: (0, tailwind_1.mimiSourceNotLine)(cssPath) };
109
280
  }
110
281
  // ── Versiones mínimas de cdk y aria (M.4) ─────────────────────────────────────────────
111
282
  /**
@@ -252,16 +423,40 @@ function updateMimiConfig(plan, report) {
252
423
  const skipped = new Set(report.skipped);
253
424
  const blocked = new Set(report.blocked.map((b) => b.item));
254
425
  const components = { ...plan.config.components };
426
+ const blocks = { ...plan.config.blocks };
255
427
  for (const item of plan.items) {
256
- if (registry_1.registry.items[item].type !== 'component')
428
+ const { type } = registry_1.registry.items[item];
429
+ if (type !== 'component' && type !== 'block')
257
430
  continue;
258
431
  if (blocked.has(item))
259
432
  continue;
260
- if (registry_1.registry.items[item].files.some((file) => skipped.has(file)))
433
+ if ((0, write_files_1.itemFiles)(item, plan.pages).some((file) => skipped.has(file)))
261
434
  continue;
262
- components[item] = { version: registry_1.registry.version };
435
+ if (type === 'component') {
436
+ components[item] = { version: registry_1.registry.version };
437
+ continue;
438
+ }
439
+ // Una página guarda dónde está y con qué nombre se generó (B.11-f).
440
+ const page = plan.pages[item];
441
+ blocks[item] = page
442
+ ? { version: registry_1.registry.version, path: page.dir, name: page.name }
443
+ : { version: registry_1.registry.version };
263
444
  }
264
445
  const next = { ...plan.config, components };
446
+ if (Object.keys(blocks).length > 0) {
447
+ next.blocks = blocks;
448
+ // Proyectos anteriores: la carpeta queda escrita la primera vez que se copia un Block
449
+ // (B.1-a) o se genera una página (B.11-b).
450
+ const entries = Object.values(blocks);
451
+ const aliases = { ...plan.config.aliases };
452
+ if (!aliases.blocks && entries.some((entry) => !entry.path))
453
+ aliases.blocks = plan.blocksDir;
454
+ if (!aliases.pages && entries.some((entry) => entry.path))
455
+ aliases.pages = plan.pagesDir;
456
+ if (JSON.stringify(aliases) !== JSON.stringify(plan.config.aliases ?? {})) {
457
+ next.aliases = aliases;
458
+ }
459
+ }
265
460
  const text = `${JSON.stringify(next, null, 2)}\n`;
266
461
  if (tree.readText('mimi.json') !== text)
267
462
  tree.overwrite('mimi.json', text);
@@ -299,11 +494,70 @@ function updateGlobalCss(plan) {
299
494
  context.logger.info(` ${line}`);
300
495
  };
301
496
  }
497
+ /**
498
+ * G1: con `@import "tailwindcss" source(none)`, Tailwind no escanea la carpeta de los Blocks ni
499
+ * la de las páginas (B.11-i), y la pantalla queda sin sus estilos. Si se copia un Block o se
500
+ * genera una página, se agrega el `@source` de su carpeta (una vez).
501
+ */
502
+ function addFolderSources(plan) {
503
+ return (tree, context) => {
504
+ const dirs = [];
505
+ if (plan.items.some((item) => registry_1.registry.items[item].type === 'block' && !plan.pages[item])) {
506
+ dirs.push(plan.blocksDir);
507
+ }
508
+ for (const page of Object.values(plan.pages)) {
509
+ const dir = `${page.dir}/`.startsWith(`${plan.pagesDir}/`) ? plan.pagesDir : page.dir;
510
+ if (!dirs.includes(dir))
511
+ dirs.push(dir);
512
+ }
513
+ if (dirs.length === 0)
514
+ return;
515
+ const cssPath = plan.config.tailwind?.css;
516
+ if (!cssPath || !tree.exists(cssPath))
517
+ return;
518
+ let css = tree.readText(cssPath);
519
+ const lines = [];
520
+ for (const dir of dirs) {
521
+ const result = (0, tailwind_1.withFolderSource)(css, cssPath, [plan.componentsDir, plan.blocksDir, plan.pagesDir], dir);
522
+ if (!result)
523
+ continue;
524
+ css = result.css;
525
+ lines.push(result.line);
526
+ }
527
+ if (lines.length === 0)
528
+ return;
529
+ tree.overwrite(cssPath, css);
530
+ context.logger.info(`Agregado a ${cssPath} (tu @import de Tailwind usa source(none), así que Tailwind solo ` +
531
+ `escanea las carpetas que indicas):\n${lines.map((line) => ` ${line}`).join('\n')}`);
532
+ };
533
+ }
302
534
  // ── Mensaje final ─────────────────────────────────────────────────────────────────────
535
+ /**
536
+ * M1: un componente que se actualizó y que usa un Block instalado (en `mimi.json → blocks`) que
537
+ * no se actualizó en esta ejecución (porque el usuario lo modificó, o porque no se pidió). Solo
538
+ * avisa: la pantalla es del usuario y puede necesitar cambios con la API nueva.
539
+ */
540
+ function blocksUsingUpdated(plan, report, itemOf) {
541
+ const updated = new Set(report.updated.map(({ file }) => itemOf(file)));
542
+ const messages = [];
543
+ for (const block of Object.keys(plan.config.blocks ?? {}).sort()) {
544
+ if (updated.has(block) || registry_1.registry.items[block]?.type !== 'block')
545
+ continue;
546
+ const used = (0, registry_1.resolveItems)(block).filter((item) => updated.has(item) && registry_1.registry.items[item].type === 'component');
547
+ if (used.length === 0)
548
+ continue;
549
+ const list = used.join(', ');
550
+ messages.push(`Tu Block ${block} usa ${list}, que se actualizó a ${registry_1.registry.version}; revisa el ` +
551
+ 'CHANGELOG por si tu pantalla necesita algún cambio.');
552
+ }
553
+ return messages;
554
+ }
555
+ /** `pricing-01` → `Pricing01`: la clase de un Block copiado tal cual. */
556
+ const blockClassName = (item) => item.replace(/(^|-)([a-z0-9])/g, (_, _dash, c) => c.toUpperCase());
303
557
  function summary(plan, report) {
304
- return (_tree, context) => {
558
+ return (tree, context) => {
305
559
  const log = context.logger;
306
- const path = (file) => `${plan.componentsDir}/${(0, write_files_1.projectPathOf)(file)}`;
560
+ const path = (file) => (0, write_files_1.targetPathOf)(file, plan.componentsDir, plan.blocksDir, plan.pages);
307
561
  if (report.created.length > 0) {
308
562
  log.info('Agregado:');
309
563
  for (const file of report.created)
@@ -313,17 +567,68 @@ function summary(plan, report) {
313
567
  log.info('Reemplazado (--overwrite):');
314
568
  for (const file of report.overwritten)
315
569
  log.info(` ~ ${path(file)}`);
570
+ // Un Block es una pantalla del usuario: --overwrite la reemplaza entera (B.1, trampas).
571
+ const screens = new Map();
572
+ for (const file of report.overwritten) {
573
+ if ((0, pages_1.isPageFile)(file)) {
574
+ const item = (0, pages_1.pageItemOf)(file);
575
+ screens.set(item, `${plan.pages[item].dir}/`);
576
+ }
577
+ else if ((0, write_files_1.isBlockFile)(file)) {
578
+ const item = file.split('/')[1];
579
+ screens.set(item, `${plan.blocksDir}/${item}/`);
580
+ }
581
+ }
582
+ for (const [block, dir] of screens) {
583
+ log.warn(`--overwrite reemplazó tu pantalla ${block} (${dir}) por la de ` +
584
+ `Mimi ${registry_1.registry.version}. Si tenías cambios, recupéralos de git.`);
585
+ }
316
586
  }
317
- const itemOf = (file) => [...plan.items, ...plan.implicit].find((item) => registry_1.registry.items[item].files.includes(file)) ??
318
- file;
587
+ const itemOf = (file) => [...plan.items, ...plan.implicit].find((item) => (0, write_files_1.itemFiles)(item, plan.pages).includes(file)) ?? file;
319
588
  for (const message of (0, write_files_1.updatedMessages)(report, itemOf, registry_1.registry.version))
320
589
  log.info(message);
590
+ for (const message of blocksUsingUpdated(plan, report, itemOf))
591
+ log.warn(message);
321
592
  for (const file of report.skipped) {
322
593
  log.warn(`Omitido: ${path(file)} ya existe con otro contenido (usa --overwrite para reemplazarlo).`);
323
594
  }
324
- for (const message of (0, write_files_1.blockedMessages)(report, plan.componentsDir, registry_1.registry.version)) {
595
+ for (const message of (0, write_files_1.blockedMessages)(report, plan.componentsDir, registry_1.registry.version, plan.blocksDir, plan.pages)) {
325
596
  log.warn(message);
326
597
  }
598
+ // Una página nueva se enruta con la línea exacta (B.11-e); la CLI no edita app.routes.ts.
599
+ // Una sección nueva va dentro de una página.
600
+ const created = new Set(report.created);
601
+ for (const item of plan.items) {
602
+ if (registry_1.registry.items[item].type !== 'block' || plan.legacyPages.includes(item))
603
+ continue;
604
+ const page = plan.pages[item];
605
+ if (page) {
606
+ if (!created.has((0, pages_1.routesKey)(item)))
607
+ continue;
608
+ const { path: routes, appDir } = routesImportOf(tree, page);
609
+ const routesFile = appDir ? `${appDir}/app.routes.ts` : null;
610
+ log.info(`${item} es ahora tu página ${page.name} (${page.dir}/, clase ` +
611
+ `${(0, pages_1.pageClassName)(page.name)}). Para abrirla en /${page.name}, agrega esta línea a ` +
612
+ (routesFile && tree.exists(routesFile)
613
+ ? `las rutas de ${routesFile}:`
614
+ : 'tus rutas (la ruta del import es relativa a tu app.routes.ts):'));
615
+ log.info(` ${(0, pages_1.routeLine)(page, routes)}`);
616
+ continue;
617
+ }
618
+ if (!registry_1.registry.items[item].files.some((file) => created.has(file)))
619
+ continue;
620
+ log.info(`${item} es una sección (una parte de una página): ponla en una de tus páginas con ` +
621
+ `<mimi-${item} />, importando ${blockClassName(item)} de ${plan.blocksDir}/${item}.`);
622
+ }
623
+ // Pantallas de la 0.4.0: siguen donde estaban (B.11-g). Una bloqueada ya tiene su aviso.
624
+ const blockedItems = new Set(report.blocked.map((b) => b.item));
625
+ for (const item of plan.legacyPages.filter((legacy) => !blockedItems.has(legacy))) {
626
+ log.info(`${item} sigue en ${plan.blocksDir}/${item}/, donde lo instalaste con la 0.4.0, y se ` +
627
+ `actualiza ahí. Los Blocks de pantalla se generan ahora como páginas ` +
628
+ `(${plan.pagesDir}/${(0, pages_1.defaultPageName)(item)}/, ${(0, pages_1.pageClassName)((0, pages_1.defaultPageName)(item))}): ` +
629
+ 'para pasarlo, guarda tus cambios, borra esa carpeta y vuelve a ejecutar ' +
630
+ `ng g mimi ${item}.`);
631
+ }
327
632
  if (!(0, write_files_1.hasFileChanges)(report) && report.skipped.length === 0 && report.blocked.length === 0) {
328
633
  log.info('Nada que hacer: los archivos ya están al día.');
329
634
  }
@@ -340,6 +645,11 @@ function summary(plan, report) {
340
645
  `tienen errores conocidos con sus componentes) y tu proyecto tiene:\n${lines.join('\n')}\n` +
341
646
  `Actualízalos con:\n ng update ${plan.updatable.join(' ')}`);
342
647
  }
648
+ if (plan.missingExclusion) {
649
+ const { cssPath, line } = plan.missingExclusion;
650
+ log.warn(`Tailwind escanea ${write_files_1.BASE_DIR}/ (las copias originales de Mimi) y genera CSS de clases que ` +
651
+ `quizá ya no usas. Agrega a ${cssPath}, después de @import "tailwindcss":\n ${line}`);
652
+ }
343
653
  if ((0, write_files_1.hasFileChanges)(report)) {
344
654
  log.info(`Guarda la carpeta ${write_files_1.BASE_DIR.split('/')[0]}/ en git: Mimi la usa para actualizar tus ` +
345
655
  'componentes sin perder tus cambios.');
@@ -9,13 +9,13 @@
9
9
  "items": {
10
10
  "type": "string"
11
11
  },
12
- "description": "Componentes que se agregan (varios a la vez: button input form-field).",
12
+ "description": "Componentes o Blocks que se agregan (varios a la vez: button input form-field, o un Block: login-01).",
13
13
  "$default": {
14
14
  "$source": "argv",
15
15
  "index": 0
16
16
  },
17
17
  "x-prompt": {
18
- "message": "¿Qué componentes quieres agregar?",
18
+ "message": "¿Qué componentes o Blocks quieres agregar?",
19
19
  "type": "list",
20
20
  "multiselect": true,
21
21
  "items": [
@@ -51,6 +51,10 @@
51
51
  "value": "combobox",
52
52
  "label": "combobox"
53
53
  },
54
+ {
55
+ "value": "command",
56
+ "label": "command"
57
+ },
54
58
  {
55
59
  "value": "confirm",
56
60
  "label": "confirm"
@@ -59,6 +63,10 @@
59
63
  "value": "context-menu",
60
64
  "label": "context-menu"
61
65
  },
66
+ {
67
+ "value": "data-table",
68
+ "label": "data-table"
69
+ },
62
70
  {
63
71
  "value": "date-picker",
64
72
  "label": "date-picker"
@@ -131,6 +139,10 @@
131
139
  "value": "slider",
132
140
  "label": "slider"
133
141
  },
142
+ {
143
+ "value": "sortable-list",
144
+ "label": "sortable-list"
145
+ },
134
146
  {
135
147
  "value": "switch",
136
148
  "label": "switch"
@@ -162,6 +174,38 @@
162
174
  {
163
175
  "value": "tooltip",
164
176
  "label": "tooltip"
177
+ },
178
+ {
179
+ "value": "tree",
180
+ "label": "tree"
181
+ },
182
+ {
183
+ "value": "login-01",
184
+ "label": "login-01 (Block, página)"
185
+ },
186
+ {
187
+ "value": "login-02",
188
+ "label": "login-02 (Block, página)"
189
+ },
190
+ {
191
+ "value": "otp-01",
192
+ "label": "otp-01 (Block, página)"
193
+ },
194
+ {
195
+ "value": "pricing-01",
196
+ "label": "pricing-01 (Block, sección)"
197
+ },
198
+ {
199
+ "value": "settings-01",
200
+ "label": "settings-01 (Block, página)"
201
+ },
202
+ {
203
+ "value": "signup-01",
204
+ "label": "signup-01 (Block, página)"
205
+ },
206
+ {
207
+ "value": "table-01",
208
+ "label": "table-01 (Block, página)"
165
209
  }
166
210
  ]
167
211
  }
@@ -170,6 +214,10 @@
170
214
  "type": "boolean",
171
215
  "default": false,
172
216
  "description": "Reemplaza los archivos que ya existan con otro contenido."
217
+ },
218
+ "name": {
219
+ "type": "string",
220
+ "description": "Nombre de la página que genera un Block de tipo página (por defecto, el del Block sin el número: login-01 → login). En kebab-case: --name admin-login."
173
221
  }
174
222
  }
175
223
  }