@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
package/README.md CHANGED
@@ -29,7 +29,7 @@ ng add @mimi-ng/cli
29
29
 
30
30
  Verifica Angular y Tailwind (si falta algo, se detiene sin modificar nada) y configura el proyecto:
31
31
 
32
- - importa el tema de Mimi en tu CSS global, justo después de `@import "tailwindcss"`;
32
+ - importa el tema de Mimi en tu CSS global, justo después de `@import "tailwindcss"`, y excluye `.mimi/` del escaneo de Tailwind (`@source not "../.mimi";`, con Tailwind 4.1 o superior);
33
33
  - agrega el alias `@/components/ui/*` al `tsconfig.json`;
34
34
  - registra la CLI en `angular.json`, para que funcione `ng g mimi`;
35
35
  - instala `clsx`, `tailwind-merge` y `class-variance-authority` con tu gestor de paquetes;
@@ -37,6 +37,14 @@ Verifica Angular y Tailwind (si falta algo, se detiene sin modificar nada) y con
37
37
 
38
38
  Con `--icons` instala también [`@lucide/angular`](https://lucide.dev) para tus íconos.
39
39
 
40
+ **Si configuraste el CSS a mano, o instalaste con la 0.3.0 o una anterior,** agrega después de `@import "tailwindcss"` (la ruta va de tu CSS global a la carpeta `.mimi/` de la raíz; con `src/styles.css`, así):
41
+
42
+ ```css
43
+ @source not "../.mimi";
44
+ ```
45
+
46
+ Sin esa línea, Tailwind escanea las copias originales de `.mimi/` (van en git) y tu CSS lleva sus clases aunque ya las hayas cambiado. `ng g mimi` te avisa si falta. Necesita Tailwind 4.1 o superior: la 4.0 no entiende `@source not`.
47
+
40
48
  ## Agregar componentes
41
49
 
42
50
  ```sh
@@ -100,6 +108,17 @@ export class App {}
100
108
  - **Toolbar**: `ng g mimi toolbar` · [documentación](https://ng.mimiworks.dev/docs/components/toolbar)
101
109
  - **Tooltip**: `ng g mimi tooltip` · [documentación](https://ng.mimiworks.dev/docs/components/tooltip)
102
110
 
111
+ ## Blocks
112
+
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).
114
+
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)
121
+
103
122
  ## Tus cambios
104
123
 
105
124
  - Guarda la carpeta `.mimi/` en git: tiene la copia original de cada archivo, y con ella Mimi sabe qué cambiaste tú.
package/dist/init/css.js CHANGED
@@ -42,7 +42,7 @@ function insertAfterTheme(css, lines) {
42
42
  const eol = eolOf(css);
43
43
  return css.slice(0, end) + lines.map((line) => eol + line).join('') + css.slice(end);
44
44
  }
45
- /** ¿Ya hay un `@import` o `@source` con esa ruta (con comillas simples o dobles)? */
45
+ /** ¿Ya hay un `@import`, `@source` o `@source not` con esa ruta (comillas simples o dobles)? */
46
46
  function hasDirective(css, directive, path) {
47
47
  const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
48
  return new RegExp(`${directive}\\s+(["'])${escaped}\\1`).test(css);
@@ -7,6 +7,7 @@ const utility_1 = require("@schematics/angular/utility");
7
7
  const json_file_1 = require("@schematics/angular/utility/json-file");
8
8
  const registry_1 = require("../registry");
9
9
  const project_1 = require("../utils/project");
10
+ const tailwind_1 = require("../utils/tailwind");
10
11
  const write_files_1 = require("../utils/write-files");
11
12
  const css_1 = require("./css");
12
13
  const items_1 = require("./items");
@@ -87,13 +88,17 @@ async function inspect(tree, options) {
87
88
  }
88
89
  const sourceRoot = (0, paths_1.normalizePath)(project.sourceRoot ?? `${project.root}/src`);
89
90
  const mimiConfig = (0, project_1.readJson)(tree, 'mimi.json');
91
+ if (mimiConfig)
92
+ (0, project_1.assertAliases)(mimiConfig);
90
93
  const componentsDir = (0, paths_1.normalizePath)(mimiConfig?.aliases?.components ?? `${sourceRoot}/app/components/ui`);
91
94
  const tsConfig = build?.options?.['tsConfig'];
92
95
  return {
93
96
  projectName: name,
94
97
  cssPath: withTailwind,
95
98
  sourceNone: (0, css_1.findTailwindImport)(tree.readText(withTailwind)).sourceNone,
99
+ sourceNot: (0, tailwind_1.tailwindSupportsSourceNot)(tree, deps['tailwindcss']),
96
100
  componentsDir,
101
+ blocksDir: (0, paths_1.normalizePath)(mimiConfig?.aliases?.blocks ?? (0, project_1.defaultBlocksDir)(componentsDir)),
97
102
  buildTsConfig: typeof tsConfig === 'string' ? (0, paths_1.normalizePath)(tsConfig) : undefined,
98
103
  mimiConfig,
99
104
  sourceRoot,
@@ -134,6 +139,24 @@ function updateGlobalCss(plan) {
134
139
  context.logger.warn(`El @import de Tailwind en ${plan.cssPath} usa source(none): Tailwind no escanea nada por su ` +
135
140
  `cuenta. Se agregó @source "${sourcePath}"; para que genere las clases de los componentes.`);
136
141
  }
142
+ // G1: y la carpeta de los Blocks (sin él, un Block queda sin estilos). Tailwind no se queja
143
+ // si la carpeta todavía no existe.
144
+ const blocksSourcePath = (0, paths_1.relativePath)(cssDir, plan.blocksDir);
145
+ if (plan.sourceNone && !(0, css_1.hasDirective)(css, '@source', blocksSourcePath)) {
146
+ lines.push(`@source "${blocksSourcePath}";`);
147
+ }
148
+ // Sin source(none), Tailwind escanea todo el workspace salvo lo que ignora git, y .mimi/ va
149
+ // en git: las clases de las copias originales acabarían en el CSS aunque el usuario ya las
150
+ // haya cambiado o quitado en sus componentes.
151
+ // Con Tailwind 4.0 (no entiende `@source not`) no se escribe: solo se avisa.
152
+ if ((0, tailwind_1.missingMimiExclusion)(css, plan.cssPath)) {
153
+ const line = (0, tailwind_1.mimiSourceNotLine)(plan.cssPath);
154
+ if (plan.sourceNot)
155
+ lines.push(line);
156
+ else
157
+ context.logger.warn(`Tailwind 4.0 escanea ${write_files_1.BASE_DIR}/ y genera CSS de más. Con Tailwind 4.1 o superior, ` +
158
+ `agrega ${line} en ${plan.cssPath} (init lo hace si lo vuelves a ejecutar).`);
159
+ }
137
160
  if (lines.length === 0)
138
161
  return;
139
162
  css = (0, css_1.insertAfterTailwind)(css, lines);
@@ -224,6 +247,8 @@ function writeMimiConfig(plan) {
224
247
  components: plan.componentsDir,
225
248
  utils: `${plan.componentsDir}/utils`,
226
249
  theme: `${plan.sourceRoot}/app/mimi.preset.ts`,
250
+ // Desde la 0.4.0 (B.1-a). Los proyectos anteriores lo reciben con el primer Block.
251
+ blocks: plan.blocksDir,
227
252
  },
228
253
  components: {},
229
254
  };
@@ -6,6 +6,7 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.normalizePath = normalizePath;
9
+ exports.invalidAliasReason = invalidAliasReason;
9
10
  exports.dirnameOf = dirnameOf;
10
11
  exports.relativePath = relativePath;
11
12
  /** `src\app\ui\`, `./src/app/ui` o `/src/app/ui` → `src/app/ui`. Resuelve `.` y `..`. */
@@ -21,6 +22,25 @@ function normalizePath(path) {
21
22
  }
22
23
  return out.join('/');
23
24
  }
25
+ /**
26
+ * M2: un alias de mimi.json (`components`, `blocks`) tiene que ser una ruta relativa a la raíz del
27
+ * workspace y quedar dentro de él. Devuelve el motivo si no lo es (o null): absoluta (`/x`,
28
+ * `C:\x`, `\\servidor`, `~/x`), con `..` o vacía. Se comprueba antes de normalizar: normalizePath
29
+ * convertiría `C:\x` en una carpeta «C:» y quitaría el `/` inicial.
30
+ */
31
+ function invalidAliasReason(value) {
32
+ if (typeof value !== 'string' || value.trim() === '')
33
+ return 'está vacío';
34
+ const path = value.trim().replace(/\\/g, '/');
35
+ if (/^[a-zA-Z]:/.test(path) || path.startsWith('/') || path.startsWith('~')) {
36
+ return 'es una ruta absoluta';
37
+ }
38
+ if (path.split('/').includes('..'))
39
+ return 'tiene «..» (saldría del proyecto)';
40
+ if (normalizePath(path) === '')
41
+ return 'apunta a la raíz del workspace';
42
+ return null;
43
+ }
24
44
  /** Carpeta de un archivo: `src/styles.css` → `src`. */
25
45
  function dirnameOf(path) {
26
46
  const parts = normalizePath(path).split('/');
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.0",
2
+ "version": "0.4.0",
3
3
  "suggestedDependencies": {
4
4
  "@lucide/angular": "^1.47.0"
5
5
  },
@@ -670,6 +670,117 @@
670
670
  "utils/control-styles",
671
671
  "utils/field-state"
672
672
  ]
673
+ },
674
+ "login-01": {
675
+ "type": "block",
676
+ "files": [
677
+ "blocks/login-01/login-01.ts",
678
+ "blocks/login-01/login-01.html",
679
+ "blocks/login-01/index.ts"
680
+ ],
681
+ "peerDependencies": {
682
+ "@angular/forms": "^22.0.0"
683
+ },
684
+ "registryDependencies": [
685
+ "button",
686
+ "card",
687
+ "checkbox",
688
+ "form-field",
689
+ "input",
690
+ "password",
691
+ "separator",
692
+ "utils/cn"
693
+ ]
694
+ },
695
+ "login-02": {
696
+ "type": "block",
697
+ "files": [
698
+ "blocks/login-02/login-02.ts",
699
+ "blocks/login-02/login-02.html",
700
+ "blocks/login-02/index.ts"
701
+ ],
702
+ "peerDependencies": {
703
+ "@angular/forms": "^22.0.0"
704
+ },
705
+ "registryDependencies": [
706
+ "avatar",
707
+ "button",
708
+ "form-field",
709
+ "input",
710
+ "utils/cn"
711
+ ]
712
+ },
713
+ "signup-01": {
714
+ "type": "block",
715
+ "files": [
716
+ "blocks/signup-01/signup-01.ts",
717
+ "blocks/signup-01/signup-01.html",
718
+ "blocks/signup-01/index.ts"
719
+ ],
720
+ "peerDependencies": {
721
+ "@angular/forms": "^22.0.0"
722
+ },
723
+ "registryDependencies": [
724
+ "button",
725
+ "checkbox",
726
+ "form-field",
727
+ "input",
728
+ "password",
729
+ "utils/cn"
730
+ ]
731
+ },
732
+ "otp-01": {
733
+ "type": "block",
734
+ "files": [
735
+ "blocks/otp-01/otp-01.ts",
736
+ "blocks/otp-01/otp-01.html",
737
+ "blocks/otp-01/index.ts"
738
+ ],
739
+ "registryDependencies": [
740
+ "button",
741
+ "card",
742
+ "input-otp",
743
+ "utils/cn"
744
+ ]
745
+ },
746
+ "settings-01": {
747
+ "type": "block",
748
+ "files": [
749
+ "blocks/settings-01/settings-01.ts",
750
+ "blocks/settings-01/settings-01.html",
751
+ "blocks/settings-01/index.ts"
752
+ ],
753
+ "peerDependencies": {
754
+ "@angular/forms": "^22.0.0"
755
+ },
756
+ "registryDependencies": [
757
+ "avatar",
758
+ "badge",
759
+ "button",
760
+ "card",
761
+ "form-field",
762
+ "input",
763
+ "progress",
764
+ "separator",
765
+ "switch",
766
+ "tabs",
767
+ "textarea",
768
+ "utils/cn"
769
+ ]
770
+ },
771
+ "pricing-01": {
772
+ "type": "block",
773
+ "files": [
774
+ "blocks/pricing-01/pricing-01.ts",
775
+ "blocks/pricing-01/pricing-01.html",
776
+ "blocks/pricing-01/index.ts"
777
+ ],
778
+ "registryDependencies": [
779
+ "badge",
780
+ "button",
781
+ "toggle-group",
782
+ "utils/cn"
783
+ ]
673
784
  }
674
785
  }
675
786
  }
@@ -0,0 +1 @@
1
+ export * from './login-01';
@@ -0,0 +1,174 @@
1
+ <!-- Trama de puntos de fondo (solo escritorio). La máscara usa solo el canal alfa. -->
2
+ <div
3
+ aria-hidden="true"
4
+ class="pointer-events-none absolute inset-0 -z-10 hidden bg-[radial-gradient(color-mix(in_oklab,var(--mimi-foreground)_14%,transparent)_1px,transparent_1px)] bg-size-[18px_18px] mask-[radial-gradient(ellipse_at_50%_40%,var(--mimi-foreground)_30%,transparent_75%)] sm:block"
5
+ ></div>
6
+
7
+ <mimi-card
8
+ class="w-full gap-[18px] border-0 bg-transparent p-0 shadow-none sm:max-w-[400px] sm:border sm:bg-card sm:p-8 sm:shadow-card"
9
+ >
10
+ <form
11
+ novalidate
12
+ class="flex flex-col gap-[18px]"
13
+ [attr.aria-labelledby]="id + '-title'"
14
+ (submit)="onSubmit($event)"
15
+ >
16
+ <div class="flex flex-col items-center gap-2.5 text-center">
17
+ <span
18
+ aria-hidden="true"
19
+ class="inline-flex size-10 items-center justify-center rounded-[12px] bg-primary text-[22px] leading-none font-extrabold text-primary-foreground shadow-primary"
20
+ >m</span
21
+ >
22
+ <h1 [id]="id + '-title'" class="text-2xl font-extrabold tracking-[-0.02em]">Sign in</h1>
23
+ <p class="text-sm text-muted-foreground">Welcome back to Mimi Sales.</p>
24
+ </div>
25
+
26
+ <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
27
+ <button
28
+ mimiBtn
29
+ type="button"
30
+ variant="outline"
31
+ tone="secondary"
32
+ (click)="provider.emit('google')"
33
+ >
34
+ <!-- La «G» del diseño, una letra en un círculo (no el logotipo de Google). -->
35
+ <span
36
+ aria-hidden="true"
37
+ class="inline-flex size-[18px] items-center justify-center rounded-full border-[1.5px] border-current text-[11px] font-extrabold"
38
+ >G</span
39
+ >
40
+ Google
41
+ </button>
42
+ <button
43
+ mimiBtn
44
+ type="button"
45
+ variant="outline"
46
+ tone="secondary"
47
+ (click)="provider.emit('github')"
48
+ >
49
+ <svg
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ width="17"
52
+ height="17"
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ stroke-linecap="round"
58
+ stroke-linejoin="round"
59
+ aria-hidden="true"
60
+ >
61
+ <!-- Lucide github (ISC) -->
62
+ <path
63
+ d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
64
+ />
65
+ <path d="M9 18c-4.51 2-5-2-7-2" />
66
+ </svg>
67
+ GitHub
68
+ </button>
69
+ </div>
70
+
71
+ <div class="flex items-center gap-3 text-xs text-muted-foreground">
72
+ <mimi-separator class="w-auto flex-1" />
73
+ or with your email
74
+ <mimi-separator class="w-auto flex-1" />
75
+ </div>
76
+
77
+ <mimi-form-field label="Email">
78
+ <input mimiInput type="email" autocomplete="email" [formField]="login.email" />
79
+ </mimi-form-field>
80
+
81
+ <mimi-form-field class="relative">
82
+ <label [attr.for]="id + '-password'" class="text-sm font-medium">Password</label>
83
+ <a
84
+ [href]="forgotHref()"
85
+ class="absolute top-0 right-0 rounded-sm text-[13px] font-medium text-muted-foreground underline underline-offset-3 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
86
+ >Forgot it?</a
87
+ >
88
+ <mimi-password>
89
+ <input
90
+ mimiInput
91
+ type="password"
92
+ autocomplete="current-password"
93
+ [id]="id + '-password'"
94
+ [formField]="login.password"
95
+ />
96
+ </mimi-password>
97
+ </mimi-form-field>
98
+
99
+ <div class="flex items-center gap-2.5">
100
+ <mimi-checkbox [id]="id + '-remember'" [formField]="login.remember" />
101
+ <label [attr.for]="id + '-remember'" class="cursor-pointer text-sm"
102
+ >Remember me for 30 days</label
103
+ >
104
+ </div>
105
+
106
+ <!-- Siempre en la página: así el lector anuncia el error cuando aparece. -->
107
+ <div role="alert" class="empty:hidden">
108
+ @if (error(); as message) {
109
+ <p
110
+ class="flex items-start gap-2 rounded-[var(--mimi-radius)] bg-destructive-soft-bg px-3 py-2.5 text-sm text-destructive-soft-foreground"
111
+ >
112
+ <svg
113
+ xmlns="http://www.w3.org/2000/svg"
114
+ width="16"
115
+ height="16"
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ stroke="currentColor"
119
+ stroke-width="2"
120
+ stroke-linecap="round"
121
+ stroke-linejoin="round"
122
+ aria-hidden="true"
123
+ class="mt-0.5 shrink-0"
124
+ >
125
+ <!-- Lucide circle-alert (ISC) -->
126
+ <circle cx="12" cy="12" r="10" />
127
+ <line x1="12" x2="12" y1="8" y2="12" />
128
+ <line x1="12" x2="12.01" y1="16" y2="16" />
129
+ </svg>
130
+ {{ message }}
131
+ </p>
132
+ }
133
+ </div>
134
+
135
+ <button
136
+ mimiBtn
137
+ type="submit"
138
+ class="h-[calc(var(--mimi-control-height,2.5rem)+4px)] text-[15px] font-semibold"
139
+ [attr.aria-disabled]="loading() ? 'true' : null"
140
+ [attr.aria-busy]="loading() ? 'true' : null"
141
+ >
142
+ @if (loading()) {
143
+ <svg
144
+ xmlns="http://www.w3.org/2000/svg"
145
+ width="16"
146
+ height="16"
147
+ viewBox="0 0 24 24"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ stroke-width="2"
151
+ stroke-linecap="round"
152
+ stroke-linejoin="round"
153
+ aria-hidden="true"
154
+ class="motion-safe:animate-[mimi-spin_0.8s_linear_infinite]"
155
+ >
156
+ <!-- Lucide loader-circle (ISC) -->
157
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
158
+ </svg>
159
+ Signing in…
160
+ } @else {
161
+ Sign in
162
+ }
163
+ </button>
164
+
165
+ <p class="text-center text-sm text-muted-foreground">
166
+ Don't have an account?
167
+ <a
168
+ [href]="signupHref()"
169
+ class="rounded-sm font-semibold text-foreground underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
170
+ >Sign up</a
171
+ >
172
+ </p>
173
+ </form>
174
+ </mimi-card>
@@ -0,0 +1,105 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ booleanAttribute,
5
+ computed,
6
+ input,
7
+ output,
8
+ signal,
9
+ } from '@angular/core';
10
+ import { FormField, email, form, required } from '@angular/forms/signals';
11
+ import { MimiButton } from '@/components/ui/button';
12
+ import { MimiCard } from '@/components/ui/card';
13
+ import { MimiCheckbox } from '@/components/ui/checkbox';
14
+ import { MimiFormField } from '@/components/ui/form-field';
15
+ import { MimiInput } from '@/components/ui/input';
16
+ import { MimiPassword } from '@/components/ui/password';
17
+ import { MimiSeparator } from '@/components/ui/separator';
18
+ import { cn } from '@/components/ui/utils/cn';
19
+
20
+ /** Lo que sale al enviar un formulario válido. */
21
+ export interface Login01Value {
22
+ email: string;
23
+ password: string;
24
+ remember: boolean;
25
+ }
26
+
27
+ /** Proveedores de los botones de redes. */
28
+ export type Login01Provider = 'google' | 'github';
29
+
30
+ let nextId = 0;
31
+
32
+ /**
33
+ * login-01 (Block de Mimi): inicio de sesión centrado con correo, contraseña, «recordarme» y
34
+ * botones de Google y GitHub. Diseño: docs/design/Block login-01.dc.html.
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.
39
+ *
40
+ * ```html
41
+ * <mimi-login-01 [loading]="auth.loading()" [error]="auth.error()" (submitted)="auth.signIn($event)" />
42
+ * ```
43
+ */
44
+ @Component({
45
+ selector: 'mimi-login-01',
46
+ imports: [
47
+ FormField,
48
+ MimiButton,
49
+ MimiCard,
50
+ MimiCheckbox,
51
+ MimiFormField,
52
+ MimiInput,
53
+ MimiPassword,
54
+ MimiSeparator,
55
+ ],
56
+ templateUrl: './login-01.html',
57
+ host: {
58
+ '[class]': 'classes()',
59
+ '[attr.data-state]': 'loading() ? "loading" : null',
60
+ },
61
+ changeDetection: ChangeDetectionStrategy.OnPush,
62
+ })
63
+ export class Login01 {
64
+ /** Mientras tu app comprueba las credenciales: el botón dice «Signing in…» y no reenvía. */
65
+ readonly loading = input(false, { transform: booleanAttribute });
66
+ /** Error de tu servidor (credenciales incorrectas…), encima del botón. `null`: ninguno. */
67
+ readonly error = input<string | null>(null);
68
+ readonly forgotHref = input('#');
69
+ readonly signupHref = input('#');
70
+ readonly userClass = input('', { alias: 'class' });
71
+
72
+ /** Formulario válido enviado. */
73
+ readonly submitted = output<Login01Value>();
74
+ /** Botón de una red pulsado. */
75
+ readonly provider = output<Login01Provider>();
76
+
77
+ protected readonly id = `mimi-login-01-${nextId++}`;
78
+
79
+ protected readonly model = signal<Login01Value>({ email: '', password: '', remember: true });
80
+ protected readonly login = form(this.model, (path) => {
81
+ required(path.email);
82
+ email(path.email);
83
+ required(path.password);
84
+ });
85
+
86
+ protected readonly classes = computed(() =>
87
+ cn(
88
+ 'relative isolate flex min-h-svh w-full items-center justify-center bg-background px-[22px] py-8 text-foreground sm:bg-muted sm:p-10',
89
+ this.userClass(),
90
+ ),
91
+ );
92
+
93
+ protected onSubmit(event: Event): void {
94
+ event.preventDefault();
95
+ if (this.loading()) return;
96
+ const state = this.login();
97
+ state.markAsTouched();
98
+ if (state.invalid()) {
99
+ // El foco va al primer campo con error (el orden del formulario).
100
+ state.errorSummary()[0]?.fieldTree().focusBoundControl();
101
+ return;
102
+ }
103
+ this.submitted.emit({ ...this.model() });
104
+ }
105
+ }
@@ -0,0 +1 @@
1
+ export * from './login-02';