@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
@@ -0,0 +1,114 @@
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 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í:
48
+ *
49
+ * ```html
50
+ * <mimi-login-01 [loading]="auth.loading()" [error]="auth.error()" (submitted)="auth.signIn($event)" />
51
+ * ```
52
+ */
53
+ @Component({
54
+ selector: 'mimi-login-01',
55
+ imports: [
56
+ FormField,
57
+ MimiButton,
58
+ MimiCard,
59
+ MimiCheckbox,
60
+ MimiFormField,
61
+ MimiInput,
62
+ MimiPassword,
63
+ MimiSeparator,
64
+ ],
65
+ templateUrl: './login-01.html',
66
+ host: {
67
+ '[class]': 'classes()',
68
+ '[attr.data-state]': 'loading() ? "loading" : null',
69
+ },
70
+ changeDetection: ChangeDetectionStrategy.OnPush,
71
+ })
72
+ export class Login01 {
73
+ /** Mientras tu app comprueba las credenciales: el botón dice «Signing in…» y no reenvía. */
74
+ readonly loading = input(false, { transform: booleanAttribute });
75
+ /** Error de tu servidor (credenciales incorrectas…), encima del botón. `null`: ninguno. */
76
+ readonly error = input<string | null>(null);
77
+ readonly forgotHref = input('#');
78
+ readonly signupHref = input('#');
79
+ readonly userClass = input('', { alias: 'class' });
80
+
81
+ /** Formulario válido enviado. */
82
+ readonly submitted = output<Login01Value>();
83
+ /** Botón de una red pulsado. */
84
+ readonly provider = output<Login01Provider>();
85
+
86
+ protected readonly id = `mimi-login-01-${nextId++}`;
87
+
88
+ protected readonly model = signal<Login01Value>({ email: '', password: '', remember: true });
89
+ protected readonly login = form(this.model, (path) => {
90
+ required(path.email);
91
+ email(path.email);
92
+ required(path.password);
93
+ });
94
+
95
+ protected readonly classes = computed(() =>
96
+ cn(
97
+ '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',
98
+ this.userClass(),
99
+ ),
100
+ );
101
+
102
+ protected onSubmit(event: Event): void {
103
+ event.preventDefault();
104
+ if (this.loading()) return;
105
+ const state = this.login();
106
+ state.markAsTouched();
107
+ if (state.invalid()) {
108
+ // El foco va al primer campo con error (el orden del formulario).
109
+ state.errorSummary()[0]?.fieldTree().focusBoundControl();
110
+ return;
111
+ }
112
+ this.submitted.emit({ ...this.model() });
113
+ }
114
+ }
@@ -0,0 +1 @@
1
+ export * from './login-02';
@@ -0,0 +1,186 @@
1
+ <!-- Imagen: arriba como banner en móvil, la mitad derecha en escritorio. -->
2
+ <div
3
+ class="relative h-[200px] shrink-0 overflow-hidden rounded-b-[24px] bg-muted md:order-2 md:h-auto md:w-1/2 md:rounded-none"
4
+ >
5
+ @if (imageSrc(); as src) {
6
+ <img [src]="src" [alt]="imageAlt()" class="absolute inset-0 size-full object-cover" />
7
+ } @else {
8
+ <!-- PROVISIONAL (B.3-a): degradado neutro hasta tener una imagen en el diseño. Pon la tuya con imageSrc. -->
9
+ <div
10
+ aria-hidden="true"
11
+ data-slot="placeholder"
12
+ class="absolute inset-0 bg-linear-to-br from-secondary via-muted to-accent"
13
+ ></div>
14
+ }
15
+
16
+ <figure
17
+ class="pointer-events-none absolute inset-x-7 bottom-7 hidden flex-col gap-3 rounded-card border border-glass-border bg-glass p-5 backdrop-blur-glass md:flex [@media(prefers-reduced-transparency:reduce)]:bg-card [@media(prefers-reduced-transparency:reduce)]:backdrop-blur-none"
18
+ >
19
+ <!-- Texto de relleno: pon aquí la cita de un cliente real (con su permiso), o quita el figure. -->
20
+ <blockquote class="text-[17px] leading-[1.45] font-semibold text-pretty">
21
+ Add a short quote from a customer here.
22
+ </blockquote>
23
+ <figcaption class="flex items-center gap-2.5">
24
+ <mimi-avatar size="sm">
25
+ <mimi-avatar-fallback class="bg-info-soft font-bold text-info-soft-foreground"
26
+ >CN</mimi-avatar-fallback
27
+ >
28
+ </mimi-avatar>
29
+ <span class="flex flex-col leading-[1.3]">
30
+ <span class="text-[13.5px] font-bold">Customer name</span>
31
+ <span class="text-[12.5px] text-muted-foreground">Role · Company</span>
32
+ </span>
33
+ </figcaption>
34
+ </figure>
35
+ </div>
36
+
37
+ <div class="flex min-w-0 flex-1 flex-col px-[22px] pt-5 pb-6 md:px-10 md:py-8">
38
+ <div class="flex items-center gap-2.5">
39
+ <span
40
+ aria-hidden="true"
41
+ class="inline-flex size-8 items-center justify-center rounded-[10px] bg-primary text-lg leading-none font-extrabold text-primary-foreground"
42
+ >m</span
43
+ >
44
+ <span class="text-base font-bold">Mimi Sales</span>
45
+ </div>
46
+
47
+ <div class="flex flex-1 items-center justify-center py-6 md:py-0">
48
+ <form
49
+ novalidate
50
+ class="flex w-full max-w-[360px] flex-col gap-[18px]"
51
+ [attr.aria-labelledby]="id + '-title'"
52
+ (submit)="onSubmit($event)"
53
+ >
54
+ <div class="flex flex-col gap-1.5">
55
+ <h1
56
+ [id]="id + '-title'"
57
+ class="text-[26px] font-extrabold tracking-[-0.025em] md:text-[32px]"
58
+ >
59
+ Welcome back
60
+ </h1>
61
+ <p class="text-[14.5px] text-muted-foreground">Sign in with your work email.</p>
62
+ </div>
63
+
64
+ <mimi-form-field label="Email">
65
+ <input
66
+ mimiInput
67
+ type="email"
68
+ autocomplete="email"
69
+ placeholder="name@example.com"
70
+ [formField]="login.email"
71
+ />
72
+ </mimi-form-field>
73
+
74
+ <mimi-form-field class="relative">
75
+ <label [attr.for]="id + '-password'" class="text-sm font-medium">Password</label>
76
+ <a
77
+ [href]="forgotHref()"
78
+ class="absolute top-0 right-0 rounded-sm text-[13px] text-muted-foreground underline underline-offset-3 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
79
+ >Forgot it?</a
80
+ >
81
+ <input
82
+ mimiInput
83
+ type="password"
84
+ autocomplete="current-password"
85
+ placeholder="••••••••"
86
+ [id]="id + '-password'"
87
+ [formField]="login.password"
88
+ />
89
+ </mimi-form-field>
90
+
91
+ <!-- Siempre en la página: así el lector anuncia el error cuando aparece. -->
92
+ <div role="alert" class="empty:hidden">
93
+ @if (error(); as message) {
94
+ <p
95
+ 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"
96
+ >
97
+ <svg
98
+ xmlns="http://www.w3.org/2000/svg"
99
+ width="16"
100
+ height="16"
101
+ viewBox="0 0 24 24"
102
+ fill="none"
103
+ stroke="currentColor"
104
+ stroke-width="2"
105
+ stroke-linecap="round"
106
+ stroke-linejoin="round"
107
+ aria-hidden="true"
108
+ class="mt-0.5 shrink-0"
109
+ >
110
+ <!-- Lucide circle-alert (ISC) -->
111
+ <circle cx="12" cy="12" r="10" />
112
+ <line x1="12" x2="12" y1="8" y2="12" />
113
+ <line x1="12" x2="12.01" y1="16" y2="16" />
114
+ </svg>
115
+ {{ message }}
116
+ </p>
117
+ }
118
+ </div>
119
+
120
+ <button
121
+ mimiBtn
122
+ type="submit"
123
+ class="h-[calc(var(--mimi-control-height,2.5rem)+4px)] text-[15px] font-semibold"
124
+ [attr.aria-disabled]="loading() ? 'true' : null"
125
+ [attr.aria-busy]="loading() ? 'true' : null"
126
+ >
127
+ @if (loading()) {
128
+ <svg
129
+ xmlns="http://www.w3.org/2000/svg"
130
+ width="16"
131
+ height="16"
132
+ viewBox="0 0 24 24"
133
+ fill="none"
134
+ stroke="currentColor"
135
+ stroke-width="2"
136
+ stroke-linecap="round"
137
+ stroke-linejoin="round"
138
+ aria-hidden="true"
139
+ class="motion-safe:animate-[mimi-spin_0.8s_linear_infinite]"
140
+ >
141
+ <!-- Lucide loader-circle (ISC) -->
142
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
143
+ </svg>
144
+ Signing in…
145
+ } @else {
146
+ Sign in
147
+ }
148
+ </button>
149
+
150
+ <button mimiBtn type="button" variant="outline" tone="secondary" (click)="sso.emit()">
151
+ <svg
152
+ xmlns="http://www.w3.org/2000/svg"
153
+ width="16"
154
+ height="16"
155
+ viewBox="0 0 24 24"
156
+ fill="none"
157
+ stroke="currentColor"
158
+ stroke-width="2"
159
+ stroke-linecap="round"
160
+ stroke-linejoin="round"
161
+ aria-hidden="true"
162
+ >
163
+ <!-- Lucide key-round (ISC) -->
164
+ <path
165
+ d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"
166
+ />
167
+ <circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
168
+ </svg>
169
+ Sign in with SSO
170
+ </button>
171
+
172
+ <p class="text-sm text-muted-foreground">
173
+ First time here?
174
+ <a
175
+ [href]="signupHref()"
176
+ class="rounded-sm font-semibold text-foreground underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
177
+ >Create an account</a
178
+ >
179
+ </p>
180
+ </form>
181
+ </div>
182
+
183
+ <p class="text-xs text-muted-foreground">
184
+ By continuing you agree to the Terms and the Privacy Notice.
185
+ </p>
186
+ </div>
@@ -0,0 +1,101 @@
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 { MimiAvatar, MimiAvatarFallback } from '@/components/ui/avatar';
12
+ import { MimiButton } from '@/components/ui/button';
13
+ import { MimiFormField } from '@/components/ui/form-field';
14
+ import { MimiInput } from '@/components/ui/input';
15
+ import { cn } from '@/components/ui/utils/cn';
16
+
17
+ /** Lo que sale al enviar un formulario válido. */
18
+ export interface Login02Value {
19
+ email: string;
20
+ password: string;
21
+ }
22
+
23
+ let nextId = 0;
24
+
25
+ /**
26
+ * login-02 (Block de Mimi): inicio de sesión dividido en dos, con el formulario a un lado y una
27
+ * imagen con un testimonio en vidrio al otro. Diseño: docs/design/Block login-02.dc.html.
28
+ *
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í:
41
+ *
42
+ * ```html
43
+ * <mimi-login-02 imageSrc="/img/login.jpg" [loading]="auth.loading()" (submitted)="auth.signIn($event)" />
44
+ * ```
45
+ */
46
+ @Component({
47
+ selector: 'mimi-login-02',
48
+ imports: [FormField, MimiAvatar, MimiAvatarFallback, MimiButton, MimiFormField, MimiInput],
49
+ templateUrl: './login-02.html',
50
+ host: {
51
+ '[class]': 'classes()',
52
+ '[attr.data-state]': 'loading() ? "loading" : null',
53
+ },
54
+ changeDetection: ChangeDetectionStrategy.OnPush,
55
+ })
56
+ export class Login02 {
57
+ /** Mientras tu app comprueba las credenciales: el botón dice «Signing in…» y no reenvía. */
58
+ readonly loading = input(false, { transform: booleanAttribute });
59
+ /** Error de tu servidor, encima del botón. `null`: ninguno. */
60
+ readonly error = input<string | null>(null);
61
+ /** Tu imagen. Sin ella, un degradado neutro (provisional, B.3-a). */
62
+ readonly imageSrc = input<string | null>(null);
63
+ /** Texto alternativo. Vacío por defecto: la imagen es decorativa. */
64
+ readonly imageAlt = input('');
65
+ readonly forgotHref = input('#');
66
+ readonly signupHref = input('#');
67
+ readonly userClass = input('', { alias: 'class' });
68
+
69
+ /** Formulario válido enviado. */
70
+ readonly submitted = output<Login02Value>();
71
+ /** Se pulsó «Sign in with SSO». */
72
+ readonly sso = output<void>();
73
+
74
+ protected readonly id = `mimi-login-02-${nextId++}`;
75
+
76
+ protected readonly model = signal<Login02Value>({ email: '', password: '' });
77
+ protected readonly login = form(this.model, (path) => {
78
+ required(path.email);
79
+ email(path.email);
80
+ required(path.password);
81
+ });
82
+
83
+ protected readonly classes = computed(() =>
84
+ cn(
85
+ 'flex min-h-svh w-full flex-col bg-background text-foreground md:flex-row',
86
+ this.userClass(),
87
+ ),
88
+ );
89
+
90
+ protected onSubmit(event: Event): void {
91
+ event.preventDefault();
92
+ if (this.loading()) return;
93
+ const state = this.login();
94
+ state.markAsTouched();
95
+ if (state.invalid()) {
96
+ state.errorSummary()[0]?.fieldTree().focusBoundControl();
97
+ return;
98
+ }
99
+ this.submitted.emit({ ...this.model() });
100
+ }
101
+ }
@@ -0,0 +1 @@
1
+ export * from './otp-01';
@@ -0,0 +1,166 @@
1
+ <mimi-card
2
+ class="w-full items-center gap-[22px] border-0 bg-transparent p-0 text-center shadow-none sm:max-w-[440px] sm:border sm:bg-card sm:px-8 sm:py-9 sm:shadow-card"
3
+ >
4
+ <span
5
+ aria-hidden="true"
6
+ class="inline-flex size-14 items-center justify-center rounded-full bg-info-soft text-info-soft-foreground shadow-[0_0_0_8px_color-mix(in_oklab,var(--mimi-info)_8%,transparent)]"
7
+ >
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="26"
11
+ height="26"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ >
19
+ <!-- Lucide mail-check (ISC) -->
20
+ <path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8" />
21
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
22
+ <path d="m16 19 2 2 4-4" />
23
+ </svg>
24
+ </span>
25
+
26
+ <div class="flex flex-col gap-1.5">
27
+ <h1 [id]="id + '-title'" class="text-2xl font-extrabold tracking-[-0.02em]">
28
+ Check your email
29
+ </h1>
30
+ <p class="text-sm leading-normal text-muted-foreground">
31
+ We sent a {{ length() }}-digit code to
32
+ <b class="font-bold text-foreground">{{ destination() }}</b
33
+ >.
34
+ </p>
35
+ </div>
36
+
37
+ <div class="flex flex-col items-center gap-2.5">
38
+ <!-- Casillas más pequeñas en móvil, con los tokens de Input OTP. -->
39
+ <mimi-input-otp
40
+ class="[--mimi-otp-font-size:20px] [--mimi-otp-gap:6px] [--mimi-otp-height:50px] [--mimi-otp-width:42px] sm:[--mimi-otp-font-size:22px] sm:[--mimi-otp-gap:8px] sm:[--mimi-otp-height:56px] sm:[--mimi-otp-width:48px]"
41
+ aria-label="Verification code"
42
+ [aria-describedby]="id + '-message'"
43
+ [length]="length()"
44
+ [separatorAfter]="length() === 6 ? 3 : undefined"
45
+ [invalid]="incomplete() || shownStatus() === 'error'"
46
+ [touched]="incomplete() || shownStatus() === 'error'"
47
+ [success]="shownStatus() === 'success'"
48
+ [value]="code()"
49
+ (valueChange)="onCodeChange($event)"
50
+ />
51
+ <!-- El mensaje del código (Input OTP no trae uno propio). -->
52
+ <span role="status" [id]="id + '-message'" [class]="messageClasses()">
53
+ @if (incomplete() || shownStatus() === 'error') {
54
+ <svg
55
+ xmlns="http://www.w3.org/2000/svg"
56
+ width="14"
57
+ height="14"
58
+ viewBox="0 0 24 24"
59
+ fill="none"
60
+ stroke="currentColor"
61
+ stroke-width="2"
62
+ stroke-linecap="round"
63
+ stroke-linejoin="round"
64
+ aria-hidden="true"
65
+ >
66
+ <!-- Lucide circle-alert (ISC) -->
67
+ <circle cx="12" cy="12" r="10" />
68
+ <line x1="12" x2="12" y1="8" y2="12" />
69
+ <line x1="12" x2="12.01" y1="16" y2="16" />
70
+ </svg>
71
+ } @else if (shownStatus() === 'success') {
72
+ <svg
73
+ xmlns="http://www.w3.org/2000/svg"
74
+ width="14"
75
+ height="14"
76
+ viewBox="0 0 24 24"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ stroke-width="2"
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ aria-hidden="true"
83
+ >
84
+ <!-- Lucide circle-check (ISC) -->
85
+ <circle cx="12" cy="12" r="10" />
86
+ <path d="m9 12 2 2 4-4" />
87
+ </svg>
88
+ }
89
+ {{ message() }}
90
+ </span>
91
+ </div>
92
+
93
+ <button
94
+ mimiBtn
95
+ type="button"
96
+ class="h-[calc(var(--mimi-control-height,2.5rem)+4px)] w-full text-[15px] font-semibold"
97
+ [attr.aria-disabled]="loading() ? 'true' : null"
98
+ [attr.aria-busy]="loading() ? 'true' : null"
99
+ (click)="onVerify()"
100
+ >
101
+ @if (loading()) {
102
+ <svg
103
+ xmlns="http://www.w3.org/2000/svg"
104
+ width="16"
105
+ height="16"
106
+ viewBox="0 0 24 24"
107
+ fill="none"
108
+ stroke="currentColor"
109
+ stroke-width="2"
110
+ stroke-linecap="round"
111
+ stroke-linejoin="round"
112
+ aria-hidden="true"
113
+ class="motion-safe:animate-[mimi-spin_0.8s_linear_infinite]"
114
+ >
115
+ <!-- Lucide loader-circle (ISC) -->
116
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
117
+ </svg>
118
+ Verifying…
119
+ } @else {
120
+ Verify
121
+ }
122
+ </button>
123
+
124
+ <div class="flex flex-col gap-2 text-[13.5px] text-muted-foreground">
125
+ <span>
126
+ Didn't get it?
127
+ <button
128
+ type="button"
129
+ class="rounded-sm font-semibold text-foreground underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:cursor-not-allowed disabled:text-muted-foreground"
130
+ [disabled]="left() > 0"
131
+ (click)="onResend()"
132
+ >
133
+ @if (left() > 0) {
134
+ Resend in {{ countdown() }}
135
+ } @else {
136
+ Resend code
137
+ }
138
+ </button>
139
+ </span>
140
+ <!-- Anuncia solo cuando se puede reenviar (no cada segundo). -->
141
+ <span class="sr-only" aria-live="polite">{{ resendAnnouncement() }}</span>
142
+ <button
143
+ type="button"
144
+ class="inline-flex items-center justify-center gap-1.5 rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
145
+ (click)="changeDestination.emit()"
146
+ >
147
+ <svg
148
+ xmlns="http://www.w3.org/2000/svg"
149
+ width="14"
150
+ height="14"
151
+ viewBox="0 0 24 24"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ stroke-width="2"
155
+ stroke-linecap="round"
156
+ stroke-linejoin="round"
157
+ aria-hidden="true"
158
+ >
159
+ <!-- Lucide arrow-left (ISC) -->
160
+ <path d="m12 19-7-7 7-7" />
161
+ <path d="M19 12H5" />
162
+ </svg>
163
+ Use another email
164
+ </button>
165
+ </div>
166
+ </mimi-card>