@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
@@ -0,0 +1,332 @@
1
+ <!-- El formulario va primero en el DOM (su h1 antes del h2 de los beneficios); a la vista, a la
2
+ derecha (los beneficios llevan order-first). -->
3
+ <div class="flex min-w-0 flex-1 items-center justify-center px-[22px] py-7 md:p-10">
4
+ <form
5
+ novalidate
6
+ class="flex w-full max-w-[400px] flex-col gap-4"
7
+ [attr.aria-labelledby]="id + '-title'"
8
+ (submit)="onSubmit($event)"
9
+ >
10
+ <span
11
+ aria-hidden="true"
12
+ class="inline-flex size-9 items-center justify-center rounded-[11px] bg-primary text-xl leading-none font-extrabold text-primary-foreground md:hidden"
13
+ >m</span
14
+ >
15
+ <div class="flex flex-col gap-1">
16
+ <h1 [id]="id + '-title'" class="text-[26px] font-extrabold tracking-[-0.025em]">
17
+ Create your account
18
+ </h1>
19
+ <p class="text-sm text-muted-foreground">It takes less than a minute.</p>
20
+ </div>
21
+
22
+ <mimi-form-field label="Full name">
23
+ <input
24
+ mimiInput
25
+ autocomplete="name"
26
+ [attr.aria-describedby]="id + '-name-ok'"
27
+ [formField]="signup.name"
28
+ [showError]="blurred('name')"
29
+ />
30
+ <!-- «Válido»: texto del Block (FormField solo tiene errores). -->
31
+ <p [id]="id + '-name-ok'" [class]="okClasses">
32
+ @if (valid('name')) {
33
+ <svg
34
+ xmlns="http://www.w3.org/2000/svg"
35
+ width="13"
36
+ height="13"
37
+ viewBox="0 0 24 24"
38
+ fill="none"
39
+ stroke="currentColor"
40
+ stroke-width="2"
41
+ stroke-linecap="round"
42
+ stroke-linejoin="round"
43
+ aria-hidden="true"
44
+ >
45
+ <!-- Lucide circle-check (ISC) -->
46
+ <circle cx="12" cy="12" r="10" />
47
+ <path d="m9 12 2 2 4-4" />
48
+ </svg>
49
+ Looks good.
50
+ }
51
+ </p>
52
+ </mimi-form-field>
53
+
54
+ <mimi-form-field label="Work email">
55
+ <input
56
+ mimiInput
57
+ type="email"
58
+ autocomplete="email"
59
+ [attr.aria-describedby]="id + '-email-ok'"
60
+ [formField]="signup.email"
61
+ [showError]="blurred('email')"
62
+ />
63
+ <p [id]="id + '-email-ok'" [class]="okClasses">
64
+ @if (valid('email')) {
65
+ <svg
66
+ xmlns="http://www.w3.org/2000/svg"
67
+ width="13"
68
+ height="13"
69
+ viewBox="0 0 24 24"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ stroke-width="2"
73
+ stroke-linecap="round"
74
+ stroke-linejoin="round"
75
+ aria-hidden="true"
76
+ >
77
+ <!-- Lucide circle-check (ISC) -->
78
+ <circle cx="12" cy="12" r="10" />
79
+ <path d="m9 12 2 2 4-4" />
80
+ </svg>
81
+ Valid email.
82
+ }
83
+ </p>
84
+ </mimi-form-field>
85
+
86
+ <mimi-form-field label="Password">
87
+ <mimi-password meter [rules]="rules">
88
+ <input
89
+ mimiInput
90
+ type="password"
91
+ autocomplete="new-password"
92
+ [formField]="signup.password"
93
+ [showError]="blurred('password')"
94
+ />
95
+ </mimi-password>
96
+ </mimi-form-field>
97
+
98
+ <mimi-form-field label="Confirm password">
99
+ <input
100
+ mimiInput
101
+ type="password"
102
+ autocomplete="new-password"
103
+ [attr.aria-describedby]="id + '-confirm-ok'"
104
+ [formField]="signup.confirm"
105
+ [showError]="blurred('confirm')"
106
+ />
107
+ <p [id]="id + '-confirm-ok'" [class]="okClasses">
108
+ @if (valid('confirm')) {
109
+ <svg
110
+ xmlns="http://www.w3.org/2000/svg"
111
+ width="13"
112
+ height="13"
113
+ viewBox="0 0 24 24"
114
+ fill="none"
115
+ stroke="currentColor"
116
+ stroke-width="2"
117
+ stroke-linecap="round"
118
+ stroke-linejoin="round"
119
+ aria-hidden="true"
120
+ >
121
+ <!-- Lucide circle-check (ISC) -->
122
+ <circle cx="12" cy="12" r="10" />
123
+ <path d="m9 12 2 2 4-4" />
124
+ </svg>
125
+ They match.
126
+ }
127
+ </p>
128
+ </mimi-form-field>
129
+
130
+ <!-- En un mimi-form-field para que su error se vea (al pulsar el botón incompleto). -->
131
+ <mimi-form-field>
132
+ <div class="flex items-start gap-2.5">
133
+ <mimi-checkbox class="mt-0.5" [id]="id + '-terms'" [formField]="signup.terms" />
134
+ <label [attr.for]="id + '-terms'" class="cursor-pointer text-[13.5px] leading-[1.45]">
135
+ I accept the
136
+ <a
137
+ [href]="termsHref()"
138
+ class="rounded-sm font-medium underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
139
+ >Terms</a
140
+ >
141
+ and the
142
+ <a
143
+ [href]="privacyHref()"
144
+ class="rounded-sm font-medium underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
145
+ >Privacy Notice</a
146
+ >.
147
+ </label>
148
+ </div>
149
+ </mimi-form-field>
150
+
151
+ <div role="alert" class="empty:hidden">
152
+ @if (error(); as message) {
153
+ <p
154
+ 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"
155
+ >
156
+ <svg
157
+ xmlns="http://www.w3.org/2000/svg"
158
+ width="16"
159
+ height="16"
160
+ viewBox="0 0 24 24"
161
+ fill="none"
162
+ stroke="currentColor"
163
+ stroke-width="2"
164
+ stroke-linecap="round"
165
+ stroke-linejoin="round"
166
+ aria-hidden="true"
167
+ class="mt-0.5 shrink-0"
168
+ >
169
+ <!-- Lucide circle-alert (ISC) -->
170
+ <circle cx="12" cy="12" r="10" />
171
+ <line x1="12" x2="12" y1="8" y2="12" />
172
+ <line x1="12" x2="12.01" y1="16" y2="16" />
173
+ </svg>
174
+ {{ message }}
175
+ </p>
176
+ }
177
+ </div>
178
+
179
+ <!-- aria-disabled y no disabled: el botón se enfoca y su nota (lo que falta) se lee. -->
180
+ <button
181
+ mimiBtn
182
+ type="submit"
183
+ class="h-[calc(var(--mimi-control-height,2.5rem)+4px)] text-[15px] font-semibold aria-disabled:cursor-not-allowed aria-disabled:opacity-[var(--mimi-disabled-opacity,0.5)]"
184
+ [attr.aria-disabled]="loading() || !complete() ? 'true' : null"
185
+ [attr.aria-busy]="loading() ? 'true' : null"
186
+ [attr.aria-describedby]="id + '-hint'"
187
+ >
188
+ @if (loading()) {
189
+ <svg
190
+ xmlns="http://www.w3.org/2000/svg"
191
+ width="16"
192
+ height="16"
193
+ viewBox="0 0 24 24"
194
+ fill="none"
195
+ stroke="currentColor"
196
+ stroke-width="2"
197
+ stroke-linecap="round"
198
+ stroke-linejoin="round"
199
+ aria-hidden="true"
200
+ class="motion-safe:animate-[mimi-spin_0.8s_linear_infinite]"
201
+ >
202
+ <!-- Lucide loader-circle (ISC) -->
203
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
204
+ </svg>
205
+ Creating account…
206
+ } @else {
207
+ Create account
208
+ }
209
+ </button>
210
+ <p [id]="id + '-hint'" class="-mt-1 text-center text-[12.5px] text-muted-foreground">
211
+ {{ hint() }}
212
+ </p>
213
+
214
+ <p class="text-center text-sm text-muted-foreground">
215
+ Already have an account?
216
+ <a
217
+ [href]="signinHref()"
218
+ class="rounded-sm font-semibold text-foreground underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
219
+ >Sign in</a
220
+ >
221
+ </p>
222
+ </form>
223
+ </div>
224
+
225
+ <!-- Beneficios (solo escritorio), a la izquierda con order-first. -->
226
+ <aside
227
+ [attr.aria-labelledby]="id + '-perks'"
228
+ class="order-first hidden w-[44%] shrink-0 flex-col justify-between border-r bg-muted px-12 py-10 md:flex"
229
+ >
230
+ <div class="flex items-center gap-2.5">
231
+ <span
232
+ aria-hidden="true"
233
+ class="inline-flex size-8 items-center justify-center rounded-[10px] bg-primary text-lg leading-none font-extrabold text-primary-foreground"
234
+ >m</span
235
+ >
236
+ <span class="text-base font-bold">Mimi Sales</span>
237
+ </div>
238
+ <div class="flex flex-col gap-[22px]">
239
+ <h2
240
+ [id]="id + '-perks'"
241
+ class="text-[30px] leading-[1.15] font-extrabold tracking-[-0.025em] text-balance"
242
+ >
243
+ Your whole sales team in one place.
244
+ </h2>
245
+ <ul class="flex flex-col gap-3.5">
246
+ <li class="flex items-start gap-3">
247
+ <span
248
+ class="inline-flex size-[30px] shrink-0 items-center justify-center rounded-[9px] border bg-card"
249
+ >
250
+ <svg
251
+ xmlns="http://www.w3.org/2000/svg"
252
+ width="15"
253
+ height="15"
254
+ viewBox="0 0 24 24"
255
+ fill="none"
256
+ stroke="currentColor"
257
+ stroke-width="2"
258
+ stroke-linecap="round"
259
+ stroke-linejoin="round"
260
+ aria-hidden="true"
261
+ >
262
+ <!-- Lucide git-merge (ISC) -->
263
+ <circle cx="18" cy="18" r="3" />
264
+ <circle cx="6" cy="6" r="3" />
265
+ <path d="M6 21V9a9 9 0 0 0 9 9" />
266
+ </svg>
267
+ </span>
268
+ <span class="flex flex-col leading-[1.4]">
269
+ <span class="text-[14.5px] font-semibold">Sync without losing changes</span>
270
+ <span class="text-[13.5px] text-muted-foreground">Your team works on the same data.</span>
271
+ </span>
272
+ </li>
273
+ <li class="flex items-start gap-3">
274
+ <span
275
+ class="inline-flex size-[30px] shrink-0 items-center justify-center rounded-[9px] border bg-card"
276
+ >
277
+ <svg
278
+ xmlns="http://www.w3.org/2000/svg"
279
+ width="15"
280
+ height="15"
281
+ viewBox="0 0 24 24"
282
+ fill="none"
283
+ stroke="currentColor"
284
+ stroke-width="2"
285
+ stroke-linecap="round"
286
+ stroke-linejoin="round"
287
+ aria-hidden="true"
288
+ >
289
+ <!-- Lucide shield-check (ISC) -->
290
+ <path
291
+ d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"
292
+ />
293
+ <path d="m9 12 2 2 4-4" />
294
+ </svg>
295
+ </span>
296
+ <span class="flex flex-col leading-[1.4]">
297
+ <span class="text-[14.5px] font-semibold">Secure by default</span>
298
+ <span class="text-[13.5px] text-muted-foreground">Encryption and 2FA for everyone.</span>
299
+ </span>
300
+ </li>
301
+ <li class="flex items-start gap-3">
302
+ <span
303
+ class="inline-flex size-[30px] shrink-0 items-center justify-center rounded-[9px] border bg-card"
304
+ >
305
+ <svg
306
+ xmlns="http://www.w3.org/2000/svg"
307
+ width="15"
308
+ height="15"
309
+ viewBox="0 0 24 24"
310
+ fill="none"
311
+ stroke="currentColor"
312
+ stroke-width="2"
313
+ stroke-linecap="round"
314
+ stroke-linejoin="round"
315
+ aria-hidden="true"
316
+ >
317
+ <!-- Lucide headset (ISC) -->
318
+ <path
319
+ d="M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5Zm0 0a9 9 0 1 1 18 0m0 0v5a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3Z"
320
+ />
321
+ <path d="M21 16v2a4 4 0 0 1-4 4h-5" />
322
+ </svg>
323
+ </span>
324
+ <span class="flex flex-col leading-[1.4]">
325
+ <span class="text-[14.5px] font-semibold">Support that answers</span>
326
+ <span class="text-[13.5px] text-muted-foreground">A reply in under 4 hours.</span>
327
+ </span>
328
+ </li>
329
+ </ul>
330
+ </div>
331
+ <span class="text-[12.5px] text-muted-foreground">14-day free trial · no card required</span>
332
+ </aside>
@@ -0,0 +1,176 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ booleanAttribute,
5
+ computed,
6
+ inject,
7
+ input,
8
+ output,
9
+ signal,
10
+ } from '@angular/core';
11
+ import { FormField, email, form, minLength, required, validate } from '@angular/forms/signals';
12
+ import { MimiButton } from '@/components/ui/button';
13
+ import { MimiCheckbox } from '@/components/ui/checkbox';
14
+ import { MimiFormField } from '@/components/ui/form-field';
15
+ import { MimiInput } from '@/components/ui/input';
16
+ import {
17
+ MIMI_PASSWORD_TEXT,
18
+ MimiPassword,
19
+ type MimiPasswordRule,
20
+ mimiPasswordLevel,
21
+ } from '@/components/ui/password';
22
+ import { cn } from '@/components/ui/utils/cn';
23
+
24
+ /** Lo que sale al enviar un formulario completo. */
25
+ export interface Signup01Value {
26
+ name: string;
27
+ email: string;
28
+ password: string;
29
+ }
30
+
31
+ interface Signup01Model extends Signup01Value {
32
+ confirm: string;
33
+ terms: boolean;
34
+ }
35
+
36
+ let nextId = 0;
37
+
38
+ /**
39
+ * signup-01 (Block de Mimi): registro con validación al salir de cada campo, medidor de
40
+ * contraseña con reglas, confirmación y un botón que dice qué falta.
41
+ * Diseño: docs/design/Block signup-01.dc.html.
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í.
45
+ *
46
+ * ```html
47
+ * <mimi-signup-01 [loading]="auth.loading()" [error]="auth.error()" (submitted)="auth.signUp($event)" />
48
+ * ```
49
+ */
50
+ @Component({
51
+ selector: 'mimi-signup-01',
52
+ imports: [FormField, MimiButton, MimiCheckbox, MimiFormField, MimiInput, MimiPassword],
53
+ templateUrl: './signup-01.html',
54
+ host: {
55
+ '[class]': 'classes()',
56
+ '[attr.data-state]': 'loading() ? "loading" : complete() ? "complete" : "incomplete"',
57
+ },
58
+ changeDetection: ChangeDetectionStrategy.OnPush,
59
+ })
60
+ export class Signup01 {
61
+ /** Mientras tu app crea la cuenta: el botón dice «Creating account…» y no reenvía. */
62
+ readonly loading = input(false, { transform: booleanAttribute });
63
+ /** Error de tu servidor (correo ya registrado…), encima del botón. `null`: ninguno. */
64
+ readonly error = input<string | null>(null);
65
+ readonly termsHref = input('#');
66
+ readonly privacyHref = input('#');
67
+ readonly signinHref = input('#');
68
+ readonly userClass = input('', { alias: 'class' });
69
+
70
+ /** Formulario completo enviado. */
71
+ readonly submitted = output<Signup01Value>();
72
+
73
+ protected readonly id = `mimi-signup-01-${nextId++}`;
74
+
75
+ /**
76
+ * Las reglas del medidor: las cuatro de Password, con sus textos. El Block las pasa a
77
+ * `mimi-password` y las usa para el validador de «fuerte» (una sola lista).
78
+ */
79
+ private readonly text = inject(MIMI_PASSWORD_TEXT);
80
+ protected readonly rules: readonly MimiPasswordRule[] = [
81
+ { label: this.text.minLength, test: (v) => v.length >= 8 },
82
+ { label: this.text.uppercase, test: (v) => /\p{Lu}/u.test(v) },
83
+ { label: this.text.number, test: (v) => /\d/.test(v) },
84
+ { label: this.text.symbol, test: (v) => /[^\p{L}\p{N}\s]/u.test(v) },
85
+ ];
86
+
87
+ protected readonly model = signal<Signup01Model>({
88
+ name: '',
89
+ email: '',
90
+ password: '',
91
+ confirm: '',
92
+ terms: false,
93
+ });
94
+ protected readonly signup = form(this.model, (path) => {
95
+ required(path.name, { message: 'Enter your name.' });
96
+ minLength(path.name, 2, { message: 'Enter your name.' });
97
+ required(path.email, { message: 'Enter your work email.' });
98
+ email(path.email, { message: 'Add the domain, for example name@example.com.' });
99
+ // El validador de Angular acepta «ana@empresa» (sin punto en el dominio); un correo de
100
+ // trabajo lo lleva.
101
+ validate(path.email, ({ value }) =>
102
+ /^[^\s@]+@[^\s@]+$/.test(value()) && !/@[^\s@]+\.[^\s@.]{2,}$/.test(value())
103
+ ? { kind: 'email', message: 'Add the domain, for example name@example.com.' }
104
+ : undefined,
105
+ );
106
+ required(path.password, { message: 'Choose a password.' });
107
+ validate(path.password, ({ value }) =>
108
+ value() && mimiPasswordLevel(value(), this.rules) < 3
109
+ ? { kind: 'strength', message: 'Meet the four rules for a strong password.' }
110
+ : undefined,
111
+ );
112
+ required(path.confirm, { message: 'Repeat your password.' });
113
+ validate(path.confirm, ({ value, valueOf }) =>
114
+ value() && value() !== valueOf(path.password)
115
+ ? { kind: 'match', message: "Passwords don't match." }
116
+ : undefined,
117
+ );
118
+ validate(path.terms, ({ value }) =>
119
+ value() ? undefined : { kind: 'required', message: 'Accept the terms to continue.' },
120
+ );
121
+ });
122
+
123
+ /** Lo que falta, en el orden del formulario (la nota del botón). */
124
+ protected readonly missing = computed(() => {
125
+ const f = this.signup;
126
+ return [
127
+ f.name().invalid() && 'your name',
128
+ f.email().invalid() && 'a valid email',
129
+ f.password().invalid() && 'a strong password',
130
+ f.confirm().invalid() && 'matching passwords',
131
+ f.terms().invalid() && 'accepting the terms',
132
+ ].filter((item): item is string => !!item);
133
+ });
134
+ protected readonly complete = computed(() => this.missing().length === 0);
135
+ protected readonly hint = computed(() =>
136
+ this.complete() ? 'All set.' : `Missing: ${this.missing().join(', ')}.`,
137
+ );
138
+
139
+ /**
140
+ * Error visible al salir del campo (B.4-a): Mimi lo muestra también al escribir (`dirty`), y
141
+ * el diseño espera al `touched`. Una vez tocado, cambia mientras se escribe (la confirmación
142
+ * deja de decir «no coinciden» en cuanto coinciden).
143
+ */
144
+ protected blurred(field: 'name' | 'email' | 'password' | 'confirm'): boolean {
145
+ const state = this.signup[field]();
146
+ return state.touched() && state.invalid();
147
+ }
148
+
149
+ /** «Válido» al salir de un campo bien escrito (el diseño; FormField solo tiene errores). */
150
+ protected valid(field: 'name' | 'email' | 'confirm'): boolean {
151
+ const state = this.signup[field]();
152
+ return state.touched() && state.valid();
153
+ }
154
+
155
+ protected readonly okClasses =
156
+ 'flex items-center gap-1.5 text-[12.5px] text-success-soft-foreground empty:hidden';
157
+
158
+ protected readonly classes = computed(() =>
159
+ cn('flex min-h-svh w-full bg-background text-foreground', this.userClass()),
160
+ );
161
+
162
+ protected onSubmit(event: Event): void {
163
+ event.preventDefault();
164
+ if (this.loading()) return;
165
+ const state = this.signup();
166
+ state.markAsTouched();
167
+ if (state.invalid()) {
168
+ // El botón no usa disabled (no se enfocaría ni se leería la nota): marca todo y lleva el
169
+ // foco al primer campo con error.
170
+ state.errorSummary()[0]?.fieldTree().focusBoundControl();
171
+ return;
172
+ }
173
+ const { name, email, password } = this.model();
174
+ this.submitted.emit({ name, email, password });
175
+ }
176
+ }