@mimi-ng/cli 0.4.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 (35) hide show
  1. package/README.md +29 -7
  2. package/dist/init/index.js +8 -0
  3. package/dist/registry.js +4 -1
  4. package/dist/registry.json +87 -1
  5. package/dist/templates/blocks/login-01/login-01.ts +12 -3
  6. package/dist/templates/blocks/login-02/login-02.ts +12 -3
  7. package/dist/templates/blocks/otp-01/otp-01.ts +12 -3
  8. package/dist/templates/blocks/pricing-01/pricing-01.ts +1 -1
  9. package/dist/templates/blocks/settings-01/settings-01.ts +13 -3
  10. package/dist/templates/blocks/signup-01/signup-01.ts +12 -2
  11. package/dist/templates/blocks/table-01/index.ts +1 -0
  12. package/dist/templates/blocks/table-01/table-01.html +214 -0
  13. package/dist/templates/blocks/table-01/table-01.ts +266 -0
  14. package/dist/templates/components/checkbox/checkbox.ts +14 -5
  15. package/dist/templates/components/command/command-texts.ts +65 -0
  16. package/dist/templates/components/command/command.ts +553 -0
  17. package/dist/templates/components/command/index.ts +13 -0
  18. package/dist/templates/components/data-table/data-table-texts.ts +110 -0
  19. package/dist/templates/components/data-table/data-table.ts +1153 -0
  20. package/dist/templates/components/data-table/index.ts +18 -0
  21. package/dist/templates/components/sortable-list/index.ts +9 -0
  22. package/dist/templates/components/sortable-list/sortable-list-texts.ts +71 -0
  23. package/dist/templates/components/sortable-list/sortable-list.ts +510 -0
  24. package/dist/templates/components/tree/index.ts +9 -0
  25. package/dist/templates/components/tree/tree-texts.ts +45 -0
  26. package/dist/templates/components/tree/tree.ts +730 -0
  27. package/dist/templates/theme/provider.ts +4 -0
  28. package/dist/templates/theme/types.ts +84 -0
  29. package/dist/ui/index.js +232 -40
  30. package/dist/ui/schema.json +30 -6
  31. package/dist/utils/pages.js +138 -0
  32. package/dist/utils/project.js +33 -9
  33. package/dist/utils/tailwind.js +15 -11
  34. package/dist/utils/write-files.js +32 -7
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -79,8 +79,10 @@ export class App {}
79
79
  - **Checkbox**: `ng g mimi checkbox` · [documentación](https://ng.mimiworks.dev/docs/components/switch)
80
80
  - **Color Picker**: `ng g mimi color-picker` · [documentación](https://ng.mimiworks.dev/docs/components/color-picker)
81
81
  - **Combobox**: `ng g mimi combobox` · [documentación](https://ng.mimiworks.dev/docs/components/combobox)
82
+ - **Command**: `ng g mimi command` · [documentación](https://ng.mimiworks.dev/docs/components/command)
82
83
  - **Confirm**: `ng g mimi confirm` · [documentación](https://ng.mimiworks.dev/docs/components/confirm)
83
84
  - **Context Menu**: `ng g mimi context-menu` · [documentación](https://ng.mimiworks.dev/docs/components/context-menu)
85
+ - **Data Table**: `ng g mimi data-table` · [documentación](https://ng.mimiworks.dev/docs/components/data-table)
84
86
  - **Date Picker**: `ng g mimi date-picker` · [documentación](https://ng.mimiworks.dev/docs/components/date-picker)
85
87
  - **Date Range**: `ng g mimi date-range` · [documentación](https://ng.mimiworks.dev/docs/components/date-range)
86
88
  - **Dialog**: `ng g mimi dialog` · [documentación](https://ng.mimiworks.dev/docs/components/dialog)
@@ -99,6 +101,7 @@ export class App {}
99
101
  - **Separator**: `ng g mimi separator` · [documentación](https://ng.mimiworks.dev/docs/components/separator)
100
102
  - **Skeleton**: `ng g mimi skeleton` · [documentación](https://ng.mimiworks.dev/docs/components/skeleton)
101
103
  - **Slider**: `ng g mimi slider` · [documentación](https://ng.mimiworks.dev/docs/components/slider)
104
+ - **Sortable List**: `ng g mimi sortable-list` · [documentación](https://ng.mimiworks.dev/docs/components/sortable-list)
102
105
  - **Switch**: `ng g mimi switch` · [documentación](https://ng.mimiworks.dev/docs/components/switch)
103
106
  - **Tabs**: `ng g mimi tabs` · [documentación](https://ng.mimiworks.dev/docs/components/tabs)
104
107
  - **Textarea**: `ng g mimi textarea` · [documentación](https://ng.mimiworks.dev/docs/components/input)
@@ -107,17 +110,24 @@ export class App {}
107
110
  - **Toggle Group**: `ng g mimi toggle-group` · [documentación](https://ng.mimiworks.dev/docs/components/toggle-group)
108
111
  - **Toolbar**: `ng g mimi toolbar` · [documentación](https://ng.mimiworks.dev/docs/components/toolbar)
109
112
  - **Tooltip**: `ng g mimi tooltip` · [documentación](https://ng.mimiworks.dev/docs/components/tooltip)
113
+ - **Tree**: `ng g mimi tree` · [documentación](https://ng.mimiworks.dev/docs/components/tree)
110
114
 
111
115
  ## Blocks
112
116
 
113
- Pantallas enteras (inicio de sesión, registro, ajustes…) hechas con los componentes de Mimi. Se agregan con el mismo comando, por ejemplo `ng g mimi` seguido del nombre del Block: la pantalla se copia a `src/app/blocks/<nombre>` (`aliases.blocks` en `mimi.json`) y los componentes que usa, a la carpeta de siempre. Es un componente de página: agrégalo a tus rutas. La pantalla es tuya; si la modificas, `ng g mimi` no la vuelve a tocar al actualizar (te avisa y `--overwrite` la reemplaza entera).
117
+ Pantallas y secciones (inicio de sesión, registro, ajustes, precios…) hechas con los componentes de Mimi. Se agregan con el mismo comando, por ejemplo `ng g mimi` seguido del nombre del Block, y los componentes que usan van a la carpeta de siempre.
114
118
 
115
- - **login-01** (inicio de sesión centrado, con Google y GitHub): `ng g mimi login-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/login-01)
116
- - **login-02** (inicio de sesión en dos columnas, con imagen y testimonio): `ng g mimi login-02` · [documentación](https://ng.mimiworks.dev/docs/blocks/login-02)
117
- - **otp-01** (verificación con un código de 6 dígitos, reenvío y cuenta regresiva): `ng g mimi otp-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/otp-01)
118
- - **pricing-01** (tres planes con mensual/anual y el plan destacado): `ng g mimi pricing-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/pricing-01)
119
- - **settings-01** (ajustes de cuenta con secciones, interruptores, sesiones y facturación): `ng g mimi settings-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/settings-01)
120
- - **signup-01** (registro con validación, medidor de contraseña y confirmación): `ng g mimi signup-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/signup-01)
119
+ - **Las páginas** (`login-01`, `login-02`, `otp-01`, `settings-01`, `signup-01`, `table-01`) se generan como una página tuya: `ng g mimi login-01` crea `src/app/pages/login/` (`aliases.pages` en `mimi.json`) con `login.ts`, `login.html` y `login.routes.ts`, y la clase `LoginPage`. Al terminar te da la línea para `app.routes.ts` (la CLI no toca tus rutas). Otro nombre: `--name admin-login`. Si la carpeta ya existe, no la toca y te pide otro nombre.
120
+ - **Las secciones** (`pricing-01`) se copian a `src/app/blocks/<nombre>` (`aliases.blocks`): ponlas dentro de una de tus páginas.
121
+
122
+ El Block es tuyo; si lo modificas, `ng g mimi` no lo vuelve a tocar al actualizar (te avisa y `--overwrite` lo reemplaza entero). Mimi lo sigue por su nombre (`login-01`) aunque le pongas otro. Si instalaste una pantalla con la 0.4.0, sigue en `src/app/blocks/` y se actualiza ahí.
123
+
124
+ - **login-01** (página: inicio de sesión centrado, con Google y GitHub): `ng g mimi login-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/login-01)
125
+ - **login-02** (página: inicio de sesión en dos columnas, con imagen y testimonio): `ng g mimi login-02` · [documentación](https://ng.mimiworks.dev/docs/blocks/login-02)
126
+ - **otp-01** (página: verificación con un código de 6 dígitos, reenvío y cuenta regresiva): `ng g mimi otp-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/otp-01)
127
+ - **pricing-01** (sección: tres planes con mensual/anual y el plan destacado): `ng g mimi pricing-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/pricing-01)
128
+ - **settings-01** (página: ajustes de cuenta con secciones, interruptores, sesiones y facturación): `ng g mimi settings-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/settings-01)
129
+ - **table-01** (página: clientes con la Data Table en escritorio y una lista de tarjetas en móvil): `ng g mimi table-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/table-01)
130
+ - **signup-01** (página: registro con validación, medidor de contraseña y confirmación): `ng g mimi signup-01` · [documentación](https://ng.mimiworks.dev/docs/blocks/signup-01)
121
131
 
122
132
  ## Tus cambios
123
133
 
@@ -130,6 +140,18 @@ Pantallas enteras (inicio de sesión, registro, ajustes…) hechas con los compo
130
140
 
131
141
  Desde pnpm 10, los scripts de instalación de las dependencias están bloqueados hasta que los apruebes, y la instalación puede fallar con `ERR_PNPM_IGNORED_BUILDS`. Apruébalos con `pnpm approve-builds`. Más detalles en la [página de Instalación](https://ng.mimiworks.dev/docs/installation).
132
142
 
143
+ ## Si los componentes salen sin estilos
144
+
145
+ Si `ng build` termina bien pero los componentes salen sin estilos, y el CSS compilado pesa mucho menos de lo normal (en una prueba, unos 33 kB en vez de unos 100 kB), puede que tu sistema bloquee el binario nativo de Tailwind. Lo vimos en Windows, con el Control de aplicaciones, que bloqueó el binario de Tailwind 4.3.5: Tailwind usa entonces un respaldo que no escanea tus archivos, y no da ningún error. No es de Mimi.
146
+
147
+ Para comprobarlo, busca una clase de tus componentes (por ejemplo `bg-background`) en el `styles-*.css` de `dist/<proyecto>/browser`: si no está, Tailwind no escaneó nada. Para ver si es el bloqueo, carga el binario desde la raíz del proyecto (Windows x64):
148
+
149
+ ```sh
150
+ node -e "require('./node_modules/@tailwindcss/oxide-win32-x64-msvc/tailwindcss-oxide.win32-x64-msvc.node')"
151
+ ```
152
+
153
+ Si falla con `ERR_DLOPEN_FAILED`, permite ese archivo en la política de tu sistema (con quien la administre) o fija una versión de Tailwind cuyo binario cargue; en la prueba donde lo vimos, la 4.3.3: `npm i -D --save-exact tailwindcss@4.3.3 @tailwindcss/postcss@4.3.3`. No desactives la política de seguridad para esto. Con otra versión, o en otro equipo, el resultado puede ser distinto.
154
+
133
155
  ## Licencia
134
156
 
135
157
  MIT. Incluye íconos de Lucide (ISC); ver `THIRD_PARTY_NOTICES.md`.
@@ -99,6 +99,7 @@ async function inspect(tree, options) {
99
99
  sourceNot: (0, tailwind_1.tailwindSupportsSourceNot)(tree, deps['tailwindcss']),
100
100
  componentsDir,
101
101
  blocksDir: (0, paths_1.normalizePath)(mimiConfig?.aliases?.blocks ?? (0, project_1.defaultBlocksDir)(componentsDir)),
102
+ pagesDir: (0, paths_1.normalizePath)(mimiConfig?.aliases?.pages ?? (0, project_1.defaultPagesDir)(componentsDir)),
102
103
  buildTsConfig: typeof tsConfig === 'string' ? (0, paths_1.normalizePath)(tsConfig) : undefined,
103
104
  mimiConfig,
104
105
  sourceRoot,
@@ -145,6 +146,11 @@ function updateGlobalCss(plan) {
145
146
  if (plan.sourceNone && !(0, css_1.hasDirective)(css, '@source', blocksSourcePath)) {
146
147
  lines.push(`@source "${blocksSourcePath}";`);
147
148
  }
149
+ // B.11-i: y la de las páginas (los Blocks de tipo página).
150
+ const pagesSourcePath = (0, paths_1.relativePath)(cssDir, plan.pagesDir);
151
+ if (plan.sourceNone && !(0, css_1.hasDirective)(css, '@source', pagesSourcePath)) {
152
+ lines.push(`@source "${pagesSourcePath}";`);
153
+ }
148
154
  // Sin source(none), Tailwind escanea todo el workspace salvo lo que ignora git, y .mimi/ va
149
155
  // en git: las clases de las copias originales acabarían en el CSS aunque el usuario ya las
150
156
  // haya cambiado o quitado en sus componentes.
@@ -249,6 +255,8 @@ function writeMimiConfig(plan) {
249
255
  theme: `${plan.sourceRoot}/app/mimi.preset.ts`,
250
256
  // Desde la 0.4.0 (B.1-a). Los proyectos anteriores lo reciben con el primer Block.
251
257
  blocks: plan.blocksDir,
258
+ // Desde la B.11. Los proyectos anteriores lo reciben con la primera página.
259
+ pages: plan.pagesDir,
252
260
  },
253
261
  components: {},
254
262
  };
package/dist/registry.js CHANGED
@@ -3,10 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.registry = void 0;
6
+ exports.isPageBlock = exports.registry = void 0;
7
7
  exports.resolveItems = resolveItems;
8
8
  const registry_json_1 = __importDefault(require("./registry.json"));
9
9
  exports.registry = registry_json_1.default;
10
+ /** ¿El ítem es un Block que se genera como página (B.11)? */
11
+ const isPageBlock = (name, reg = exports.registry) => reg.items[name]?.type === 'block' && reg.items[name].kind === 'page';
12
+ exports.isPageBlock = isPageBlock;
10
13
  /**
11
14
  * El ítem y todas sus `registryDependencies`, sin repetir, dependencias primero.
12
15
  * Lanza un error si el nombre no existe.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.0",
2
+ "version": "0.5.0",
3
3
  "suggestedDependencies": {
4
4
  "@lucide/angular": "^1.47.0"
5
5
  },
@@ -265,6 +265,23 @@
265
265
  "utils/cn"
266
266
  ]
267
267
  },
268
+ "data-table": {
269
+ "type": "component",
270
+ "files": [
271
+ "components/data-table/data-table.ts",
272
+ "components/data-table/data-table-texts.ts",
273
+ "components/data-table/index.ts"
274
+ ],
275
+ "registryDependencies": [
276
+ "checkbox",
277
+ "chip",
278
+ "dropdown-menu",
279
+ "input",
280
+ "pagination",
281
+ "toggle-group",
282
+ "utils/cn"
283
+ ]
284
+ },
268
285
  "input-mask": {
269
286
  "type": "component",
270
287
  "files": [
@@ -334,6 +351,20 @@
334
351
  "utils/cn"
335
352
  ]
336
353
  },
354
+ "sortable-list": {
355
+ "type": "component",
356
+ "files": [
357
+ "components/sortable-list/sortable-list.ts",
358
+ "components/sortable-list/sortable-list-texts.ts",
359
+ "components/sortable-list/index.ts"
360
+ ],
361
+ "peerDependencies": {
362
+ "@angular/cdk": "^22.2.4"
363
+ },
364
+ "registryDependencies": [
365
+ "utils/cn"
366
+ ]
367
+ },
337
368
  "slider": {
338
369
  "type": "component",
339
370
  "files": [
@@ -374,6 +405,22 @@
374
405
  "utils/overlay"
375
406
  ]
376
407
  },
408
+ "command": {
409
+ "type": "component",
410
+ "files": [
411
+ "components/command/command.ts",
412
+ "components/command/command-texts.ts",
413
+ "components/command/index.ts"
414
+ ],
415
+ "peerDependencies": {
416
+ "@angular/aria": "^22.2.4"
417
+ },
418
+ "registryDependencies": [
419
+ "dialog",
420
+ "option-list",
421
+ "utils/cn"
422
+ ]
423
+ },
377
424
  "calendar": {
378
425
  "type": "component",
379
426
  "files": [
@@ -655,6 +702,22 @@
655
702
  "utils/overlay"
656
703
  ]
657
704
  },
705
+ "tree": {
706
+ "type": "component",
707
+ "files": [
708
+ "components/tree/tree.ts",
709
+ "components/tree/tree-texts.ts",
710
+ "components/tree/index.ts"
711
+ ],
712
+ "peerDependencies": {
713
+ "@angular/cdk": "^22.2.4",
714
+ "@angular/forms": "^22.0.0"
715
+ },
716
+ "registryDependencies": [
717
+ "checkbox",
718
+ "utils/cn"
719
+ ]
720
+ },
658
721
  "textarea": {
659
722
  "type": "component",
660
723
  "files": [
@@ -673,6 +736,7 @@
673
736
  },
674
737
  "login-01": {
675
738
  "type": "block",
739
+ "kind": "page",
676
740
  "files": [
677
741
  "blocks/login-01/login-01.ts",
678
742
  "blocks/login-01/login-01.html",
@@ -694,6 +758,7 @@
694
758
  },
695
759
  "login-02": {
696
760
  "type": "block",
761
+ "kind": "page",
697
762
  "files": [
698
763
  "blocks/login-02/login-02.ts",
699
764
  "blocks/login-02/login-02.html",
@@ -712,6 +777,7 @@
712
777
  },
713
778
  "signup-01": {
714
779
  "type": "block",
780
+ "kind": "page",
715
781
  "files": [
716
782
  "blocks/signup-01/signup-01.ts",
717
783
  "blocks/signup-01/signup-01.html",
@@ -731,6 +797,7 @@
731
797
  },
732
798
  "otp-01": {
733
799
  "type": "block",
800
+ "kind": "page",
734
801
  "files": [
735
802
  "blocks/otp-01/otp-01.ts",
736
803
  "blocks/otp-01/otp-01.html",
@@ -745,6 +812,7 @@
745
812
  },
746
813
  "settings-01": {
747
814
  "type": "block",
815
+ "kind": "page",
748
816
  "files": [
749
817
  "blocks/settings-01/settings-01.ts",
750
818
  "blocks/settings-01/settings-01.html",
@@ -768,8 +836,26 @@
768
836
  "utils/cn"
769
837
  ]
770
838
  },
839
+ "table-01": {
840
+ "type": "block",
841
+ "kind": "page",
842
+ "files": [
843
+ "blocks/table-01/table-01.ts",
844
+ "blocks/table-01/table-01.html",
845
+ "blocks/table-01/index.ts"
846
+ ],
847
+ "registryDependencies": [
848
+ "badge",
849
+ "button",
850
+ "checkbox",
851
+ "data-table",
852
+ "input",
853
+ "utils/cn"
854
+ ]
855
+ },
771
856
  "pricing-01": {
772
857
  "type": "block",
858
+ "kind": "section",
773
859
  "files": [
774
860
  "blocks/pricing-01/pricing-01.ts",
775
861
  "blocks/pricing-01/pricing-01.html",
@@ -33,9 +33,18 @@ let nextId = 0;
33
33
  * login-01 (Block de Mimi): inicio de sesión centrado con correo, contraseña, «recordarme» y
34
34
  * botones de Google y GitHub. Diseño: docs/design/Block login-01.dc.html.
35
35
  *
36
- * Es una pantalla: ponla en tus rutas (`{ path: 'login', component: Login01 }`) y conéctala con
37
- * tu autenticación por las salidas. El archivo es tuyo: cambia los textos, quita las redes o
38
- * agrega campos aquí mismo.
36
+ * Es una página: `ng g mimi login-01` la genera en tu carpeta de páginas (`aliases.pages` de
37
+ * mimi.json; por defecto, `src/app/pages/login/`, u otro nombre con `--name`) junto con su
38
+ * archivo de rutas (`login.routes.ts`, con la página en carga diferida), y te da la línea para
39
+ * `app.routes.ts`:
40
+ *
41
+ * ```ts
42
+ * { path: 'login', loadChildren: () => import('./pages/login/login.routes').then((m) => m.LOGIN_ROUTES) },
43
+ * ```
44
+ *
45
+ * El archivo es tuyo: conecta tu autenticación aquí mismo (en `onSubmit`, inyectando tu
46
+ * servicio), cambia los textos, quita las redes o agrega campos. Si la usas dentro de otra
47
+ * página, sus entradas y salidas siguen ahí:
39
48
  *
40
49
  * ```html
41
50
  * <mimi-login-01 [loading]="auth.loading()" [error]="auth.error()" (submitted)="auth.signIn($event)" />
@@ -26,9 +26,18 @@ let nextId = 0;
26
26
  * login-02 (Block de Mimi): inicio de sesión dividido en dos, con el formulario a un lado y una
27
27
  * imagen con un testimonio en vidrio al otro. Diseño: docs/design/Block login-02.dc.html.
28
28
  *
29
- * Es una pantalla: ponla en tus rutas (`{ path: 'login', component: Login02 }`). Da tu imagen con
30
- * `imageSrc` (y `imageAlt` si no es decorativa). El testimonio está escrito en la plantilla:
31
- * cámbialo ahí.
29
+ * Es una página: `ng g mimi login-02` la genera en tu carpeta de páginas (`aliases.pages` de
30
+ * mimi.json; por defecto, `src/app/pages/login/`, u otro nombre con `--name` si ya tienes la de
31
+ * login-01) junto con su archivo de rutas (`login.routes.ts`, con la página en carga diferida),
32
+ * y te da la línea para `app.routes.ts`:
33
+ *
34
+ * ```ts
35
+ * { path: 'login', loadChildren: () => import('./pages/login/login.routes').then((m) => m.LOGIN_ROUTES) },
36
+ * ```
37
+ *
38
+ * Da tu imagen con `imageSrc` (y `imageAlt` si no es decorativa). El testimonio está escrito en
39
+ * la plantilla: cámbialo ahí. Si la usas dentro de otra página, sus entradas y salidas siguen
40
+ * ahí:
32
41
  *
33
42
  * ```html
34
43
  * <mimi-login-02 imageSrc="/img/login.jpg" [loading]="auth.loading()" (submitted)="auth.signIn($event)" />
@@ -29,9 +29,18 @@ let nextId = 0;
29
29
  * otp-01 (Block de Mimi): verificación con un código de 6 dígitos, con reenvío y cuenta
30
30
  * regresiva. Diseño: docs/design/Block otp-01.dc.html.
31
31
  *
32
- * Es una pantalla: va después del registro o del inicio de sesión. `verify` sale con el código
33
- * (al completar los dígitos o con el botón, una sola vez por código); tu app lo comprueba y
34
- * responde con `status`.
32
+ * Es una página, que va después del registro o del inicio de sesión: `ng g mimi otp-01` la
33
+ * genera en tu carpeta de páginas (`aliases.pages` de mimi.json; por defecto, `src/app/pages/otp/`,
34
+ * u otro nombre con `--name`) junto con su archivo de rutas (`otp.routes.ts`, con la página en
35
+ * carga diferida), y te da la línea para `app.routes.ts`:
36
+ *
37
+ * ```ts
38
+ * { path: 'otp', loadChildren: () => import('./pages/otp/otp.routes').then((m) => m.OTP_ROUTES) },
39
+ * ```
40
+ *
41
+ * `verify` sale con el código (al completar los dígitos o con el botón, una sola vez por
42
+ * código); tu app lo comprueba y responde con `status`: conéctalo aquí mismo o, si la usas dentro
43
+ * de otra página, por sus entradas y salidas:
35
44
  *
36
45
  * ```html
37
46
  * <mimi-otp-01 destination="a••••@example.com" [status]="otp.status()" [loading]="otp.checking()"
@@ -167,7 +167,7 @@ export class Pricing01 {
167
167
 
168
168
  protected readonly classes = computed(() =>
169
169
  cn(
170
- 'flex min-h-svh w-full flex-col items-center gap-7 bg-background px-[18px] pt-9 pb-8 text-foreground md:px-10 md:py-14',
170
+ 'flex w-full flex-col items-center gap-7 bg-background px-[18px] pt-9 pb-8 text-foreground md:px-10 md:py-14',
171
171
  this.userClass(),
172
172
  ),
173
173
  );
@@ -146,9 +146,19 @@ let nextId = 0;
146
146
  * de peligro, facturación y una barra de «cambios sin guardar».
147
147
  * Diseño: docs/design/Block settings-01.dc.html.
148
148
  *
149
- * Es una pantalla: ponla en tus rutas y dale tus datos (`profile`, `preferences`, `sessions`,
150
- * `plan`). Sin datos, muestra los del ejemplo. «Guardar» emite `saved` con el formulario; las
151
- * acciones (foto, sesiones, borrar la cuenta, cambiar de plan) son salidas: tu app decide.
149
+ * Es una página: `ng g mimi settings-01` la genera en tu carpeta de páginas (`aliases.pages` de
150
+ * mimi.json; por defecto, `src/app/pages/settings/`, u otro nombre con `--name`) junto con su
151
+ * archivo de rutas (`settings.routes.ts`, con la página en carga diferida), y te da la línea
152
+ * para `app.routes.ts`:
153
+ *
154
+ * ```ts
155
+ * { path: 'settings', loadChildren: () => import('./pages/settings/settings.routes').then((m) => m.SETTINGS_ROUTES) },
156
+ * ```
157
+ *
158
+ * Dale tus datos (`profile`, `preferences`, `sessions`, `plan`) aquí mismo o, si la usas dentro
159
+ * de otra página, por sus entradas. Sin datos, muestra los del ejemplo. «Guardar» emite `saved`
160
+ * con el formulario; las acciones (foto, sesiones, borrar la cuenta, cambiar de plan) son
161
+ * salidas: tu app decide.
152
162
  *
153
163
  * ```html
154
164
  * <mimi-settings-01 [profile]="me()" [saving]="saving()" (saved)="save($event)" (deleteAccount)="confirmDelete()" />
@@ -40,8 +40,18 @@ let nextId = 0;
40
40
  * contraseña con reglas, confirmación y un botón que dice qué falta.
41
41
  * Diseño: docs/design/Block signup-01.dc.html.
42
42
  *
43
- * Es una pantalla: ponla en tus rutas (`{ path: 'signup', component: Signup01 }`). El texto de
44
- * los beneficios y los mensajes están en la plantilla y en este archivo: cámbialos aquí.
43
+ * Es una página: `ng g mimi signup-01` la genera en tu carpeta de páginas (`aliases.pages` de
44
+ * mimi.json; por defecto, `src/app/pages/signup/`, u otro nombre con `--name`) junto con su
45
+ * archivo de rutas (`signup.routes.ts`, con la página en carga diferida), y te da la línea para
46
+ * `app.routes.ts`:
47
+ *
48
+ * ```ts
49
+ * { path: 'signup', loadChildren: () => import('./pages/signup/signup.routes').then((m) => m.SIGNUP_ROUTES) },
50
+ * ```
51
+ *
52
+ * El texto de los beneficios y los mensajes están en la plantilla y en este archivo: cámbialos
53
+ * aquí, y conecta tu registro en `onSubmit`. Si la usas dentro de otra página, sus entradas y
54
+ * salidas siguen ahí:
45
55
  *
46
56
  * ```html
47
57
  * <mimi-signup-01 [loading]="auth.loading()" [error]="auth.error()" (submitted)="auth.signUp($event)" />
@@ -0,0 +1 @@
1
+ export * from './table-01';
@@ -0,0 +1,214 @@
1
+ <header class="flex items-center justify-between gap-3 px-4 pt-5 pb-3.5 md:px-8 md:pt-7 md:pb-5">
2
+ <div class="min-w-0">
3
+ <h1 class="text-2xl font-extrabold tracking-[-0.03em] md:text-[28px]">Customers</h1>
4
+ <p class="text-sm text-muted-foreground tabular-nums">{{ subtitle() }}</p>
5
+ </div>
6
+ <div class="flex shrink-0 items-center gap-2">
7
+ <button
8
+ type="button"
9
+ mimiBtn
10
+ variant="outline"
11
+ class="max-md:hidden"
12
+ (click)="importCsv.emit()"
13
+ >
14
+ <svg
15
+ aria-hidden="true"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ width="16"
18
+ height="16"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ stroke-width="2"
23
+ stroke-linecap="round"
24
+ stroke-linejoin="round"
25
+ >
26
+ <!-- Lucide upload (ISC) -->
27
+ <path d="M12 3v12" />
28
+ <path d="m17 8-5-5-5 5" />
29
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
30
+ </svg>
31
+ Import CSV
32
+ </button>
33
+ <button type="button" mimiBtn aria-label="New customer" (click)="newCustomer.emit()">
34
+ <svg
35
+ aria-hidden="true"
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ width="16"
38
+ height="16"
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ stroke-width="2"
43
+ stroke-linecap="round"
44
+ stroke-linejoin="round"
45
+ >
46
+ <!-- Lucide plus (ISC) -->
47
+ <path d="M5 12h14" />
48
+ <path d="M12 5v14" />
49
+ </svg>
50
+ <span class="max-md:hidden">New customer</span>
51
+ </button>
52
+ </div>
53
+ </header>
54
+
55
+ <!-- Escritorio: el resumen y la Data Table. -->
56
+ <div class="flex flex-col gap-4 px-8 pb-8 max-md:hidden">
57
+ <ul class="flex flex-wrap gap-x-7 gap-y-2" aria-label="Customers by status">
58
+ @for (item of counts(); track item.id) {
59
+ <li class="flex items-center gap-2 text-sm">
60
+ <span aria-hidden="true" [class]="'size-2 rounded-full ' + dotClass(item.tone)"></span>
61
+ <span class="text-muted-foreground">{{ item.plural }}</span>
62
+ <span class="font-bold tabular-nums">{{ item.count }}</span>
63
+ </li>
64
+ }
65
+ </ul>
66
+ <mimi-data-table
67
+ aria-label="Customers"
68
+ searchPlaceholder="Search customer, email or ID…"
69
+ selectable
70
+ [data]="customers()"
71
+ [columns]="columns()"
72
+ [(selection)]="selection"
73
+ >
74
+ <ng-template mimiTableCell="name" let-row>
75
+ <span class="flex min-w-0 flex-col leading-[1.3]">
76
+ <span class="truncate font-semibold">{{ row.name }}</span>
77
+ <span class="truncate text-[12.5px] text-muted-foreground">{{ row.email }}</span>
78
+ </span>
79
+ </ng-template>
80
+ <ng-template mimiTableCell="status" let-row>
81
+ <span mimiBadge variant="soft" [tone]="statusOf(row.status).tone">{{
82
+ statusOf(row.status).label
83
+ }}</span>
84
+ </ng-template>
85
+ <ng-template mimiTableCell="mrr" let-row>
86
+ <span class="font-semibold">{{ format(row.mrr) }}</span>
87
+ </ng-template>
88
+ <ng-template mimiTableBulk let-ids>
89
+ <button type="button" mimiBtn variant="outline" size="sm" (click)="exported.emit(ids)">
90
+ Export
91
+ </button>
92
+ <button type="button" mimiBtn tone="danger" size="sm" (click)="deleted.emit(ids)">
93
+ Delete
94
+ </button>
95
+ </ng-template>
96
+ </mimi-data-table>
97
+ <p class="text-[12.5px] text-muted-foreground">
98
+ Select rows to see the bulk actions (Export, Delete). Filter by Status or Plan from the header.
99
+ </p>
100
+ </div>
101
+
102
+ <!-- Móvil: búsqueda, píldoras de estado y la lista de tarjetas. -->
103
+ <div class="flex flex-col gap-3 px-4 pb-24 md:hidden">
104
+ <input
105
+ mimiInput
106
+ type="search"
107
+ aria-label="Search customers"
108
+ placeholder="Search customer…"
109
+ [value]="query()"
110
+ (input)="query.set($any($event.target).value)"
111
+ />
112
+ <div role="group" aria-label="Filter by status" class="-mx-4 flex gap-2 overflow-x-auto px-4">
113
+ <button
114
+ type="button"
115
+ class="inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border border-border bg-card px-3 text-[13px] font-medium mimi-transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring aria-pressed:border-primary aria-pressed:bg-primary aria-pressed:text-primary-foreground"
116
+ [attr.aria-pressed]="statusFilter() === null"
117
+ (click)="statusFilter.set(null)"
118
+ >
119
+ All <span class="tabular-nums opacity-70">{{ customers().length }}</span>
120
+ </button>
121
+ @for (item of counts(); track item.id) {
122
+ <button
123
+ type="button"
124
+ class="inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border border-border bg-card px-3 text-[13px] font-medium mimi-transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring aria-pressed:border-primary aria-pressed:bg-primary aria-pressed:text-primary-foreground"
125
+ [attr.aria-pressed]="statusFilter() === item.id"
126
+ (click)="pickStatus(item.id)"
127
+ >
128
+ {{ item.plural }} <span class="tabular-nums opacity-70">{{ item.count }}</span>
129
+ </button>
130
+ }
131
+ </div>
132
+ <div class="flex items-center justify-between text-[13px] text-muted-foreground">
133
+ <label class="flex items-center gap-2.5">
134
+ <mimi-checkbox
135
+ aria-label="Select all"
136
+ [checked]="allState() === 'all'"
137
+ [indeterminate]="allState() === 'some'"
138
+ (checkedChange)="toggleAll($event)"
139
+ />
140
+ Select all
141
+ </label>
142
+ <span class="tabular-nums" role="status">{{ list().length }} results</span>
143
+ </div>
144
+ <ul class="flex flex-col gap-2">
145
+ @for (customer of list(); track customer.id) {
146
+ <li
147
+ class="flex items-center gap-3 rounded-[var(--mimi-radius-card)] border bg-card p-3 data-[selected]:border-info/45 data-[selected]:bg-[color-mix(in_oklab,var(--mimi-info)_8%,var(--mimi-card))]"
148
+ [attr.data-selected]="isSelected(customer.id) ? '' : null"
149
+ >
150
+ <mimi-checkbox
151
+ [aria-label]="'Select ' + customer.name"
152
+ [checked]="isSelected(customer.id)"
153
+ (checkedChange)="toggle(customer.id)"
154
+ />
155
+ <span class="flex min-w-0 flex-1 flex-col leading-[1.3]">
156
+ <span class="truncate text-sm font-semibold">{{ customer.name }}</span>
157
+ <span class="truncate text-[12.5px] text-muted-foreground tabular-nums"
158
+ >{{ customer.plan }} · {{ format(customer.mrr) }}</span
159
+ >
160
+ </span>
161
+ <span mimiBadge variant="soft" [tone]="statusOf(customer.status).tone">{{
162
+ statusOf(customer.status).label
163
+ }}</span>
164
+ </li>
165
+ } @empty {
166
+ <li
167
+ class="rounded-[var(--mimi-radius-card)] border border-dashed p-6 text-center text-sm text-muted-foreground"
168
+ >
169
+ No customers match.
170
+ </li>
171
+ }
172
+ </ul>
173
+ @if (selection().length) {
174
+ <div
175
+ role="toolbar"
176
+ aria-label="Bulk actions"
177
+ class="fixed inset-x-3 bottom-3 z-10 flex items-center gap-2 rounded-[var(--mimi-radius-card)] bg-foreground py-2 pr-2 pl-4 text-background shadow-[shadow:var(--mimi-shadow-popover)]"
178
+ >
179
+ <span class="flex-1 text-sm font-semibold tabular-nums" role="status">{{
180
+ selectedText(selection().length)
181
+ }}</span>
182
+ <button
183
+ type="button"
184
+ mimiBtn
185
+ variant="ghost"
186
+ size="sm"
187
+ class="text-background hover:bg-background/15 hover:text-background"
188
+ (click)="exported.emit(selection())"
189
+ >
190
+ Export
191
+ </button>
192
+ <button
193
+ type="button"
194
+ mimiBtn
195
+ variant="ghost"
196
+ size="sm"
197
+ class="text-background hover:bg-background/15 hover:text-background"
198
+ (click)="deleted.emit(selection())"
199
+ >
200
+ Delete
201
+ </button>
202
+ <button
203
+ type="button"
204
+ mimiBtn
205
+ variant="ghost"
206
+ size="sm"
207
+ class="text-background hover:bg-background/15 hover:text-background"
208
+ (click)="selection.set([])"
209
+ >
210
+ Clear
211
+ </button>
212
+ </div>
213
+ }
214
+ </div>