@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.
- package/README.md +42 -1
- package/dist/init/css.js +1 -1
- package/dist/init/index.js +33 -0
- package/dist/init/paths.js +20 -0
- package/dist/registry.js +4 -1
- package/dist/registry.json +198 -1
- package/dist/templates/blocks/login-01/index.ts +1 -0
- package/dist/templates/blocks/login-01/login-01.html +174 -0
- package/dist/templates/blocks/login-01/login-01.ts +114 -0
- package/dist/templates/blocks/login-02/index.ts +1 -0
- package/dist/templates/blocks/login-02/login-02.html +186 -0
- package/dist/templates/blocks/login-02/login-02.ts +101 -0
- package/dist/templates/blocks/otp-01/index.ts +1 -0
- package/dist/templates/blocks/otp-01/otp-01.html +166 -0
- package/dist/templates/blocks/otp-01/otp-01.ts +221 -0
- package/dist/templates/blocks/pricing-01/index.ts +1 -0
- package/dist/templates/blocks/pricing-01/pricing-01.html +173 -0
- package/dist/templates/blocks/pricing-01/pricing-01.ts +178 -0
- package/dist/templates/blocks/settings-01/index.ts +1 -0
- package/dist/templates/blocks/settings-01/settings-01.html +424 -0
- package/dist/templates/blocks/settings-01/settings-01.ts +392 -0
- package/dist/templates/blocks/signup-01/index.ts +1 -0
- package/dist/templates/blocks/signup-01/signup-01.html +332 -0
- package/dist/templates/blocks/signup-01/signup-01.ts +186 -0
- package/dist/templates/blocks/table-01/index.ts +1 -0
- package/dist/templates/blocks/table-01/table-01.html +214 -0
- package/dist/templates/blocks/table-01/table-01.ts +266 -0
- package/dist/templates/components/checkbox/checkbox.ts +14 -5
- package/dist/templates/components/command/command-texts.ts +65 -0
- package/dist/templates/components/command/command.ts +553 -0
- package/dist/templates/components/command/index.ts +13 -0
- package/dist/templates/components/data-table/data-table-texts.ts +110 -0
- package/dist/templates/components/data-table/data-table.ts +1153 -0
- package/dist/templates/components/data-table/index.ts +18 -0
- package/dist/templates/components/sortable-list/index.ts +9 -0
- package/dist/templates/components/sortable-list/sortable-list-texts.ts +71 -0
- package/dist/templates/components/sortable-list/sortable-list.ts +510 -0
- package/dist/templates/components/tabs/tabs.ts +88 -23
- package/dist/templates/components/tree/index.ts +9 -0
- package/dist/templates/components/tree/tree-texts.ts +45 -0
- package/dist/templates/components/tree/tree.ts +730 -0
- package/dist/templates/theme/provider.ts +4 -0
- package/dist/templates/theme/types.ts +84 -0
- package/dist/ui/index.js +323 -13
- package/dist/ui/schema.json +50 -2
- package/dist/utils/pages.js +138 -0
- package/dist/utils/project.js +59 -0
- package/dist/utils/tailwind.js +84 -0
- package/dist/utils/write-files.js +63 -8
- 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,186 @@
|
|
|
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 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í:
|
|
55
|
+
*
|
|
56
|
+
* ```html
|
|
57
|
+
* <mimi-signup-01 [loading]="auth.loading()" [error]="auth.error()" (submitted)="auth.signUp($event)" />
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
@Component({
|
|
61
|
+
selector: 'mimi-signup-01',
|
|
62
|
+
imports: [FormField, MimiButton, MimiCheckbox, MimiFormField, MimiInput, MimiPassword],
|
|
63
|
+
templateUrl: './signup-01.html',
|
|
64
|
+
host: {
|
|
65
|
+
'[class]': 'classes()',
|
|
66
|
+
'[attr.data-state]': 'loading() ? "loading" : complete() ? "complete" : "incomplete"',
|
|
67
|
+
},
|
|
68
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
69
|
+
})
|
|
70
|
+
export class Signup01 {
|
|
71
|
+
/** Mientras tu app crea la cuenta: el botón dice «Creating account…» y no reenvía. */
|
|
72
|
+
readonly loading = input(false, { transform: booleanAttribute });
|
|
73
|
+
/** Error de tu servidor (correo ya registrado…), encima del botón. `null`: ninguno. */
|
|
74
|
+
readonly error = input<string | null>(null);
|
|
75
|
+
readonly termsHref = input('#');
|
|
76
|
+
readonly privacyHref = input('#');
|
|
77
|
+
readonly signinHref = input('#');
|
|
78
|
+
readonly userClass = input('', { alias: 'class' });
|
|
79
|
+
|
|
80
|
+
/** Formulario completo enviado. */
|
|
81
|
+
readonly submitted = output<Signup01Value>();
|
|
82
|
+
|
|
83
|
+
protected readonly id = `mimi-signup-01-${nextId++}`;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Las reglas del medidor: las cuatro de Password, con sus textos. El Block las pasa a
|
|
87
|
+
* `mimi-password` y las usa para el validador de «fuerte» (una sola lista).
|
|
88
|
+
*/
|
|
89
|
+
private readonly text = inject(MIMI_PASSWORD_TEXT);
|
|
90
|
+
protected readonly rules: readonly MimiPasswordRule[] = [
|
|
91
|
+
{ label: this.text.minLength, test: (v) => v.length >= 8 },
|
|
92
|
+
{ label: this.text.uppercase, test: (v) => /\p{Lu}/u.test(v) },
|
|
93
|
+
{ label: this.text.number, test: (v) => /\d/.test(v) },
|
|
94
|
+
{ label: this.text.symbol, test: (v) => /[^\p{L}\p{N}\s]/u.test(v) },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
protected readonly model = signal<Signup01Model>({
|
|
98
|
+
name: '',
|
|
99
|
+
email: '',
|
|
100
|
+
password: '',
|
|
101
|
+
confirm: '',
|
|
102
|
+
terms: false,
|
|
103
|
+
});
|
|
104
|
+
protected readonly signup = form(this.model, (path) => {
|
|
105
|
+
required(path.name, { message: 'Enter your name.' });
|
|
106
|
+
minLength(path.name, 2, { message: 'Enter your name.' });
|
|
107
|
+
required(path.email, { message: 'Enter your work email.' });
|
|
108
|
+
email(path.email, { message: 'Add the domain, for example name@example.com.' });
|
|
109
|
+
// El validador de Angular acepta «ana@empresa» (sin punto en el dominio); un correo de
|
|
110
|
+
// trabajo lo lleva.
|
|
111
|
+
validate(path.email, ({ value }) =>
|
|
112
|
+
/^[^\s@]+@[^\s@]+$/.test(value()) && !/@[^\s@]+\.[^\s@.]{2,}$/.test(value())
|
|
113
|
+
? { kind: 'email', message: 'Add the domain, for example name@example.com.' }
|
|
114
|
+
: undefined,
|
|
115
|
+
);
|
|
116
|
+
required(path.password, { message: 'Choose a password.' });
|
|
117
|
+
validate(path.password, ({ value }) =>
|
|
118
|
+
value() && mimiPasswordLevel(value(), this.rules) < 3
|
|
119
|
+
? { kind: 'strength', message: 'Meet the four rules for a strong password.' }
|
|
120
|
+
: undefined,
|
|
121
|
+
);
|
|
122
|
+
required(path.confirm, { message: 'Repeat your password.' });
|
|
123
|
+
validate(path.confirm, ({ value, valueOf }) =>
|
|
124
|
+
value() && value() !== valueOf(path.password)
|
|
125
|
+
? { kind: 'match', message: "Passwords don't match." }
|
|
126
|
+
: undefined,
|
|
127
|
+
);
|
|
128
|
+
validate(path.terms, ({ value }) =>
|
|
129
|
+
value() ? undefined : { kind: 'required', message: 'Accept the terms to continue.' },
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/** Lo que falta, en el orden del formulario (la nota del botón). */
|
|
134
|
+
protected readonly missing = computed(() => {
|
|
135
|
+
const f = this.signup;
|
|
136
|
+
return [
|
|
137
|
+
f.name().invalid() && 'your name',
|
|
138
|
+
f.email().invalid() && 'a valid email',
|
|
139
|
+
f.password().invalid() && 'a strong password',
|
|
140
|
+
f.confirm().invalid() && 'matching passwords',
|
|
141
|
+
f.terms().invalid() && 'accepting the terms',
|
|
142
|
+
].filter((item): item is string => !!item);
|
|
143
|
+
});
|
|
144
|
+
protected readonly complete = computed(() => this.missing().length === 0);
|
|
145
|
+
protected readonly hint = computed(() =>
|
|
146
|
+
this.complete() ? 'All set.' : `Missing: ${this.missing().join(', ')}.`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Error visible al salir del campo (B.4-a): Mimi lo muestra también al escribir (`dirty`), y
|
|
151
|
+
* el diseño espera al `touched`. Una vez tocado, cambia mientras se escribe (la confirmación
|
|
152
|
+
* deja de decir «no coinciden» en cuanto coinciden).
|
|
153
|
+
*/
|
|
154
|
+
protected blurred(field: 'name' | 'email' | 'password' | 'confirm'): boolean {
|
|
155
|
+
const state = this.signup[field]();
|
|
156
|
+
return state.touched() && state.invalid();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** «Válido» al salir de un campo bien escrito (el diseño; FormField solo tiene errores). */
|
|
160
|
+
protected valid(field: 'name' | 'email' | 'confirm'): boolean {
|
|
161
|
+
const state = this.signup[field]();
|
|
162
|
+
return state.touched() && state.valid();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected readonly okClasses =
|
|
166
|
+
'flex items-center gap-1.5 text-[12.5px] text-success-soft-foreground empty:hidden';
|
|
167
|
+
|
|
168
|
+
protected readonly classes = computed(() =>
|
|
169
|
+
cn('flex min-h-svh w-full bg-background text-foreground', this.userClass()),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
protected onSubmit(event: Event): void {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
if (this.loading()) return;
|
|
175
|
+
const state = this.signup();
|
|
176
|
+
state.markAsTouched();
|
|
177
|
+
if (state.invalid()) {
|
|
178
|
+
// El botón no usa disabled (no se enfocaría ni se leería la nota): marca todo y lleva el
|
|
179
|
+
// foco al primer campo con error.
|
|
180
|
+
state.errorSummary()[0]?.fieldTree().focusBoundControl();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const { name, email, password } = this.model();
|
|
184
|
+
this.submitted.emit({ name, email, password });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './table-01';
|