@mimi-ng/cli 0.3.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 (30) hide show
  1. package/README.md +20 -1
  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 +112 -1
  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/tabs/tabs.ts +88 -23
  25. package/dist/ui/index.js +128 -10
  26. package/dist/ui/schema.json +26 -2
  27. package/dist/utils/project.js +35 -0
  28. package/dist/utils/tailwind.js +80 -0
  29. package/dist/utils/write-files.js +37 -7
  30. package/package.json +1 -1
@@ -97,37 +97,75 @@ export class MimiTabsOutlet {
97
97
  });
98
98
  }
99
99
 
100
+ /**
101
+ * Orientación de la fila de pestañas (B.10): horizontal (por defecto) o vertical, para una
102
+ * navegación lateral. En vertical, las flechas son ↑/↓ (las pone aria) y el panel va al lado.
103
+ */
104
+ export type MimiTabsOrientation = 'horizontal' | 'vertical';
105
+
100
106
  /** El contenedor visible de las pestañas (recibe el `class` del usuario). */
101
107
  const containerVariants = cva('relative font-sans', {
102
108
  variants: {
103
109
  variant: {
104
- underline: 'border-b border-border',
110
+ underline: 'border-border',
105
111
  // 4px fijos (no rem): el indicador resta los mismos 8px.
106
- pill: 'max-w-[520px] rounded-[calc(var(--mimi-radius)_+_4px)] bg-[color:var(--mimi-tabs-pill-bg,var(--mimi-muted))] p-[4px]',
112
+ pill: 'rounded-[calc(var(--mimi-radius)_+_4px)] bg-[color:var(--mimi-tabs-pill-bg,var(--mimi-muted))] p-[4px]',
107
113
  },
114
+ orientation: { horizontal: '', vertical: 'self-start' },
108
115
  },
109
- defaultVariants: { variant: 'underline' },
116
+ compoundVariants: [
117
+ { variant: 'underline', orientation: 'horizontal', class: 'border-b' },
118
+ { variant: 'underline', orientation: 'vertical', class: 'border-s' },
119
+ { variant: 'pill', orientation: 'horizontal', class: 'max-w-[520px]' },
120
+ ],
121
+ defaultVariants: { variant: 'underline', orientation: 'horizontal' },
110
122
  });
111
123
 
112
124
  /**
113
- * El indicador: columnas iguales, así que su ancho es el de una columna y se desplaza su propio
114
- * ancho por cada pestaña (sin medir nada). En derecha a izquierda, al revés.
125
+ * El indicador: columnas (o filas) iguales, así que su tamaño es el de una pestaña y se desplaza
126
+ * su propio ancho (o alto) por cada una (sin medir nada). En derecha a izquierda, al revés.
115
127
  */
116
128
  const indicatorVariants = cva(
117
129
  [
118
130
  'pointer-events-none absolute',
119
- 'translate-x-[calc(100%_*_var(--mimi-tabs-index))] rtl:translate-x-[calc(-100%_*_var(--mimi-tabs-index))]',
120
131
  'transition-[translate] duration-[450ms] ease-[var(--mimi-tabs-spring,cubic-bezier(0.3,1.35,0.5,1))] motion-reduce:transition-none',
121
132
  ],
122
133
  {
123
134
  variants: {
124
135
  variant: {
125
- underline:
126
- '-bottom-px start-0 h-0.5 w-[calc(100%_/_var(--mimi-tabs-count))] rounded-[2px] bg-[color:var(--mimi-tabs-indicator,var(--mimi-foreground))]',
127
- pill: 'start-[4px] top-[4px] bottom-[4px] w-[calc((100%_-_8px)_/_var(--mimi-tabs-count))] rounded-[var(--mimi-radius)] bg-[color:var(--mimi-tabs-pill-active,var(--mimi-card))] shadow-neutral',
136
+ underline: 'rounded-[2px] bg-[color:var(--mimi-tabs-indicator,var(--mimi-foreground))]',
137
+ pill: 'rounded-[var(--mimi-radius)] bg-[color:var(--mimi-tabs-pill-active,var(--mimi-card))] shadow-neutral',
138
+ },
139
+ orientation: {
140
+ horizontal:
141
+ 'translate-x-[calc(100%_*_var(--mimi-tabs-index))] rtl:translate-x-[calc(-100%_*_var(--mimi-tabs-index))]',
142
+ vertical: 'translate-y-[calc(100%_*_var(--mimi-tabs-index))]',
128
143
  },
129
144
  },
130
- defaultVariants: { variant: 'underline' },
145
+ compoundVariants: [
146
+ {
147
+ variant: 'underline',
148
+ orientation: 'horizontal',
149
+ class: '-bottom-px start-0 h-0.5 w-[calc(100%_/_var(--mimi-tabs-count))]',
150
+ },
151
+ {
152
+ // La barra va a la izquierda (al inicio), sobre el borde del contenedor.
153
+ variant: 'underline',
154
+ orientation: 'vertical',
155
+ class: '-start-px top-0 w-0.5 h-[calc(100%_/_var(--mimi-tabs-count))]',
156
+ },
157
+ {
158
+ variant: 'pill',
159
+ orientation: 'horizontal',
160
+ class: 'start-[4px] top-[4px] bottom-[4px] w-[calc((100%_-_8px)_/_var(--mimi-tabs-count))]',
161
+ },
162
+ {
163
+ variant: 'pill',
164
+ orientation: 'vertical',
165
+ class: 'start-[4px] end-[4px] top-[4px] h-[calc((100%_-_8px)_/_var(--mimi-tabs-count))]',
166
+ },
167
+ ],
168
+ defaultVariants: { variant: 'underline', orientation: 'horizontal' },
131
169
  },
132
170
  );
133
171
 
@@ -148,8 +186,10 @@ const tabVariants = cva(
148
186
  // 34px sin token (el token de alto es el de la subrayada): plan D6.
149
187
  pill: 'h-[34px] gap-1.5 rounded-[var(--mimi-radius)] text-[13.5px] [&_svg]:size-[15px] [&_svg]:shrink-0',
150
188
  },
189
+ // En vertical, el texto al inicio, como una navegación lateral.
190
+ orientation: { horizontal: '', vertical: 'justify-start px-3 text-start' },
151
191
  },
152
- defaultVariants: { variant: 'underline' },
192
+ defaultVariants: { variant: 'underline', orientation: 'horizontal' },
153
193
  },
154
194
  );
155
195
 
@@ -196,11 +236,12 @@ const COUNT_CLASSES =
196
236
  <div
197
237
  ngTabList
198
238
  data-slot="tablist"
239
+ [orientation]="orientation()"
199
240
  [selectedTab]="painted()"
200
241
  (selectedTabChange)="fromAria($event)"
201
242
  [attr.aria-label]="ariaLabel() || null"
202
243
  [attr.aria-labelledby]="ariaLabelledby() || null"
203
- [class]="tablistClasses"
244
+ [class]="tablistClasses()"
204
245
  (click)="choose($event)"
205
246
  (keydown.enter)="choose($event)"
206
247
  (keydown.space)="choose($event)"
@@ -248,12 +289,19 @@ const COUNT_CLASSES =
248
289
  </div>
249
290
  `,
250
291
  host: {
251
- style: 'display: flex; flex-direction: column; gap: 14px',
292
+ // flex-direction va en su binding (B.10), no aquí.
293
+ style: 'display: flex; gap: 14px',
252
294
  // El class es del contenedor de las pestañas; aria-label y aria-labelledby, del tablist.
253
295
  '[attr.class]': 'null',
254
296
  '[attr.aria-label]': 'null',
255
297
  '[attr.aria-labelledby]': 'null',
256
298
  '[attr.data-variant]': 'variant()',
299
+ '[attr.data-orientation]': 'orientation()',
300
+ // En vertical, las pestañas a un lado y el panel al otro. Bindings sueltos (no [style] con
301
+ // un objeto, que sumaba ɵɵstyleMap al JS inicial: spec 10).
302
+ // Siempre con valor: con null, Angular quita también el flex-direction del style estático.
303
+ '[style.flex-direction]': 'orientation() === "vertical" ? "row" : "column"',
304
+ '[style.align-items]': 'orientation() === "vertical" ? "flex-start" : null',
257
305
  },
258
306
  changeDetection: ChangeDetectionStrategy.OnPush,
259
307
  })
@@ -264,6 +312,8 @@ export class MimiTabs {
264
312
  readonly value = model<string | undefined>(undefined);
265
313
  /** Por defecto, `underline`. */
266
314
  readonly variant = input<MimiTabsVariant>('underline');
315
+ /** Por defecto, `horizontal`. Puede cambiar en vivo (por ejemplo, con el ancho). */
316
+ readonly orientation = input<MimiTabsOrientation>('horizontal');
267
317
  /** Conserva el contenido de los paneles al cambiar de pestaña (por defecto se destruye). */
268
318
  readonly preserveContent = input(false, { transform: booleanAttribute });
269
319
  /** Nombre accesible de la fila de pestañas. */
@@ -298,22 +348,37 @@ export class MimiTabs {
298
348
  this.tabs().findIndex((tab) => tab.value() === this.painted()),
299
349
  );
300
350
 
351
+ private readonly variants = computed(() => ({
352
+ variant: this.variant(),
353
+ orientation: this.orientation(),
354
+ }));
301
355
  protected readonly containerClasses = computed(() =>
302
- cn(containerVariants({ variant: this.variant() }), this.userClass()),
356
+ cn(containerVariants(this.variants()), this.userClass()),
303
357
  );
304
- protected readonly indicatorClasses = computed(() =>
305
- indicatorVariants({ variant: this.variant() }),
306
- );
307
- protected readonly tabClasses = computed(() => tabVariants({ variant: this.variant() }));
358
+ protected readonly indicatorClasses = computed(() => indicatorVariants(this.variants()));
359
+ // Con cn(): la vertical cambia `justify-center` por `justify-start` (sin él, quedaban las dos y
360
+ // ganaba la que va después en el CSS).
361
+ protected readonly tabClasses = computed(() => cn(tabVariants(this.variants())));
308
362
  // Sin outline-none: en Tailwind 4 pone --tw-outline-style en none y anula el anillo de foco.
309
- protected readonly tablistClasses = cn(
310
- 'grid grid-cols-[repeat(var(--mimi-tabs-count),minmax(0,1fr))] rounded-[inherit]',
311
- // Con todas deshabilitadas, el foco queda en la fila (aria).
312
- buttonFocusStyles,
363
+ // Las clases de cada orientación, completas (no variantes encadenadas: selectores largos, G2.1).
364
+ protected readonly tablistClasses = computed(() =>
365
+ cn(
366
+ this.orientation() === 'vertical'
367
+ ? 'grid grid-rows-[repeat(var(--mimi-tabs-count),minmax(0,1fr))]'
368
+ : 'grid grid-cols-[repeat(var(--mimi-tabs-count),minmax(0,1fr))]',
369
+ 'rounded-[inherit]',
370
+ // Con todas deshabilitadas, el foco queda en la fila (aria).
371
+ buttonFocusStyles,
372
+ ),
313
373
  );
314
374
  protected readonly countClasses = COUNT_CLASSES;
315
375
  protected readonly panelClasses = computed(() =>
316
- cn('px-0.5 py-1', buttonFocusStyles, this.panelClass()),
376
+ cn(
377
+ 'px-0.5 py-1',
378
+ this.orientation() === 'vertical' && 'min-w-0 flex-1',
379
+ buttonFocusStyles,
380
+ this.panelClass(),
381
+ ),
317
382
  );
318
383
 
319
384
  /** El último valor que puso Mimi (no viene de fuera). */
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,9 +111,37 @@ 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');
114
+ const blocksDir = (0, paths_1.normalizePath)(config.aliases?.blocks ?? (0, project_1.defaultBlocksDir)(componentsDir));
106
115
  const outdated = outdatedPeers(tree, items, installed, newPackages);
107
116
  const updatable = PEERS_WITH_MINIMUM.filter((name) => installed[name] || newPackages[name]);
108
- return { componentsDir, config, items, implicit, newPackages, outdated, updatable };
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) };
109
145
  }
110
146
  // ── Versiones mínimas de cdk y aria (M.4) ─────────────────────────────────────────────
111
147
  /**
@@ -252,16 +288,25 @@ function updateMimiConfig(plan, report) {
252
288
  const skipped = new Set(report.skipped);
253
289
  const blocked = new Set(report.blocked.map((b) => b.item));
254
290
  const components = { ...plan.config.components };
291
+ const blocks = { ...plan.config.blocks };
255
292
  for (const item of plan.items) {
256
- if (registry_1.registry.items[item].type !== 'component')
293
+ const { type, files } = registry_1.registry.items[item];
294
+ if (type !== 'component' && type !== 'block')
257
295
  continue;
258
296
  if (blocked.has(item))
259
297
  continue;
260
- if (registry_1.registry.items[item].files.some((file) => skipped.has(file)))
298
+ if (files.some((file) => skipped.has(file)))
261
299
  continue;
262
- components[item] = { version: registry_1.registry.version };
300
+ (type === 'block' ? blocks : components)[item] = { version: registry_1.registry.version };
263
301
  }
264
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
+ }
265
310
  const text = `${JSON.stringify(next, null, 2)}\n`;
266
311
  if (tree.readText('mimi.json') !== text)
267
312
  tree.overwrite('mimi.json', text);
@@ -299,11 +344,58 @@ function updateGlobalCss(plan) {
299
344
  context.logger.info(` ${line}`);
300
345
  };
301
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
+ }
302
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
+ }
303
395
  function summary(plan, report) {
304
396
  return (_tree, context) => {
305
397
  const log = context.logger;
306
- 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);
307
399
  if (report.created.length > 0) {
308
400
  log.info('Agregado:');
309
401
  for (const file of report.created)
@@ -313,17 +405,38 @@ function summary(plan, report) {
313
405
  log.info('Reemplazado (--overwrite):');
314
406
  for (const file of report.overwritten)
315
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
+ }
316
416
  }
317
417
  const itemOf = (file) => [...plan.items, ...plan.implicit].find((item) => registry_1.registry.items[item].files.includes(file)) ??
318
418
  file;
319
419
  for (const message of (0, write_files_1.updatedMessages)(report, itemOf, registry_1.registry.version))
320
420
  log.info(message);
421
+ for (const message of blocksUsingUpdated(plan, report, itemOf))
422
+ log.warn(message);
321
423
  for (const file of report.skipped) {
322
424
  log.warn(`Omitido: ${path(file)} ya existe con otro contenido (usa --overwrite para reemplazarlo).`);
323
425
  }
324
- 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)) {
325
427
  log.warn(message);
326
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
+ }
327
440
  if (!(0, write_files_1.hasFileChanges)(report) && report.skipped.length === 0 && report.blocked.length === 0) {
328
441
  log.info('Nada que hacer: los archivos ya están al día.');
329
442
  }
@@ -340,6 +453,11 @@ function summary(plan, report) {
340
453
  `tienen errores conocidos con sus componentes) y tu proyecto tiene:\n${lines.join('\n')}\n` +
341
454
  `Actualízalos con:\n ng update ${plan.updatable.join(' ')}`);
342
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
+ }
343
461
  if ((0, write_files_1.hasFileChanges)(report)) {
344
462
  log.info(`Guarda la carpeta ${write_files_1.BASE_DIR.split('/')[0]}/ en git: Mimi la usa para actualizar tus ` +
345
463
  '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": [
@@ -162,6 +162,30 @@
162
162
  {
163
163
  "value": "tooltip",
164
164
  "label": "tooltip"
165
+ },
166
+ {
167
+ "value": "login-01",
168
+ "label": "login-01 (Block)"
169
+ },
170
+ {
171
+ "value": "login-02",
172
+ "label": "login-02 (Block)"
173
+ },
174
+ {
175
+ "value": "otp-01",
176
+ "label": "otp-01 (Block)"
177
+ },
178
+ {
179
+ "value": "pricing-01",
180
+ "label": "pricing-01 (Block)"
181
+ },
182
+ {
183
+ "value": "settings-01",
184
+ "label": "settings-01 (Block)"
185
+ },
186
+ {
187
+ "value": "signup-01",
188
+ "label": "signup-01 (Block)"
165
189
  }
166
190
  ]
167
191
  }
@@ -1,12 +1,47 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIMI_COLLECTION = void 0;
4
+ exports.defaultBlocksDir = defaultBlocksDir;
5
+ exports.assertAliases = assertAliases;
4
6
  exports.readJson = readJson;
5
7
  exports.allDependencies = allDependencies;
6
8
  exports.majorOf = majorOf;
7
9
  const schematics_1 = require("@angular-devkit/schematics");
10
+ const paths_1 = require("../init/paths");
8
11
  /** Nombre de esta colección (el paquete). */
9
12
  exports.MIMI_COLLECTION = '@mimi-ng/cli';
13
+ /**
14
+ * Carpeta de los Blocks si mimi.json no la dice: la hermana de `components/` cuando la de
15
+ * componentes es la de siempre (`src/app/components/ui` → `src/app/blocks`), o si no, la hermana
16
+ * de la carpeta de componentes (`src/app/ui` → `src/app/blocks`).
17
+ */
18
+ function defaultBlocksDir(componentsDir) {
19
+ const parts = componentsDir.split('/').filter(Boolean);
20
+ if (parts.length >= 2 && parts.at(-1) === 'ui' && parts.at(-2) === 'components') {
21
+ parts.splice(-2, 2);
22
+ }
23
+ else {
24
+ parts.pop();
25
+ }
26
+ return [...parts, 'blocks'].join('/');
27
+ }
28
+ /**
29
+ * M2: falla con un error claro (sin escribir nada) si `aliases.components` o `aliases.blocks` de
30
+ * mimi.json es absoluto, tiene `..` o sale del proyecto.
31
+ */
32
+ function assertAliases(config) {
33
+ for (const name of ['components', 'blocks']) {
34
+ const value = config.aliases?.[name];
35
+ if (value === undefined)
36
+ continue;
37
+ const reason = (0, paths_1.invalidAliasReason)(value);
38
+ if (!reason)
39
+ continue;
40
+ const example = name === 'blocks' ? 'src/app/blocks' : 'src/app/components/ui';
41
+ throw new schematics_1.SchematicsException(`mimi.json → aliases.${name} ${reason}: ${JSON.stringify(value)}. Usa una ruta relativa a ` +
42
+ `la raíz del workspace y dentro de ella (por ejemplo "${example}"). No se cambió nada.`);
43
+ }
44
+ }
10
45
  function readJson(tree, path) {
11
46
  if (!tree.exists(path))
12
47
  return null;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tailwindSupportsSourceNot = tailwindSupportsSourceNot;
4
+ exports.withBlocksSource = withBlocksSource;
5
+ exports.mimiSourceNotLine = mimiSourceNotLine;
6
+ exports.mimiSourceNotPath = mimiSourceNotPath;
7
+ exports.missingMimiExclusion = missingMimiExclusion;
8
+ const css_1 = require("../init/css");
9
+ const paths_1 = require("../init/paths");
10
+ /** La carpeta de las copias originales (`.mimi/base/`), en la raíz del workspace. */
11
+ const MIMI_DIR = '.mimi';
12
+ /**
13
+ * ¿El Tailwind del proyecto entiende `@source not` (4.1 o superior)? Manda la versión instalada
14
+ * (`node_modules`, la que corre de verdad); si no está instalado, el rango de package.json: solo
15
+ * un rango que no puede pasar de la 4.0 (`4.0.5`, `~4.0.17`) cuenta como 4.0. `^4.0.17` instala
16
+ * la última 4.x, así que sí lo entiende.
17
+ */
18
+ function tailwindSupportsSourceNot(tree, declared) {
19
+ const file = tree.read('node_modules/tailwindcss/package.json');
20
+ const installed = file
21
+ ? JSON.parse(file.toString('utf-8')).version
22
+ : undefined;
23
+ if (installed)
24
+ return atLeast41(installed);
25
+ if (!declared)
26
+ return true;
27
+ // Solo un rango exacto o con `~` fija el minor; `^4.0.17`, `>=4`, `^4`, `latest`: la última 4.x.
28
+ const match = /^\s*(?:~|=|v)?\s*(\d+)\.(\d+)/.exec(declared);
29
+ if (!match)
30
+ return true;
31
+ const [major, minor] = [Number(match[1]), Number(match[2])];
32
+ return major > 4 || (major === 4 && minor >= 1);
33
+ }
34
+ function atLeast41(version) {
35
+ const [major, minor] = version.split('.').map((n) => Number.parseInt(n, 10) || 0);
36
+ return major > 4 || (major === 4 && minor >= 1);
37
+ }
38
+ /**
39
+ * Con `source(none)`, Tailwind solo escanea lo que dice un `@source`: la carpeta de los Blocks
40
+ * necesita el suyo (G1), como la de componentes. Devuelve el CSS con la línea agregada (después
41
+ * del `@source` de componentes, o del tema), o `null` si no hace falta: sin `source(none)`, o si
42
+ * ya está (comillas simples o dobles).
43
+ */
44
+ function withBlocksSource(css, cssPath, componentsDir, blocksDir) {
45
+ const tailwind = (0, css_1.findTailwindImport)(css);
46
+ if (!tailwind?.sourceNone)
47
+ return null;
48
+ const cssDir = (0, paths_1.dirnameOf)(cssPath);
49
+ const path = (0, paths_1.relativePath)(cssDir, blocksDir);
50
+ if ((0, css_1.hasDirective)(css, '@source', path))
51
+ return null;
52
+ const line = `@source "${path}";`;
53
+ const after = new RegExp(String.raw `^[ \t]*@source\s+(["'])` +
54
+ (0, paths_1.relativePath)(cssDir, componentsDir).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
55
+ String.raw `\1[^;\n]*;[^\n]*$`, 'm').exec(css);
56
+ if (after) {
57
+ const end = after.index + after[0].length;
58
+ const eol = /\r\n/.test(css) ? '\r\n' : '\n';
59
+ return { css: css.slice(0, end) + eol + line + css.slice(end), line };
60
+ }
61
+ const updated = (0, css_1.insertAfterTheme)(css, [line]);
62
+ return updated === null ? null : { css: updated, line };
63
+ }
64
+ /** La línea que excluye `.mimi/` del escaneo de Tailwind, relativa al CSS global. */
65
+ function mimiSourceNotLine(cssPath) {
66
+ return `@source not "${mimiSourceNotPath(cssPath)}";`;
67
+ }
68
+ function mimiSourceNotPath(cssPath) {
69
+ return (0, paths_1.relativePath)((0, paths_1.dirnameOf)(cssPath), MIMI_DIR);
70
+ }
71
+ /**
72
+ * ¿Falta la exclusión de `.mimi/` en este CSS? No hace falta con `source(none)` (Tailwind no
73
+ * escanea nada por su cuenta) ni si ya está, con comillas simples o dobles.
74
+ */
75
+ function missingMimiExclusion(css, cssPath) {
76
+ const tailwind = (0, css_1.findTailwindImport)(css);
77
+ if (!tailwind || tailwind.sourceNone)
78
+ return false;
79
+ return !(0, css_1.hasDirective)(css, '@source not', mimiSourceNotPath(cssPath));
80
+ }