@mimi-ng/cli 0.1.1 → 0.3.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 +28 -1
- package/dist/init/css.js +17 -0
- package/dist/init/index.js +7 -5
- package/dist/init/items.js +9 -0
- package/dist/registry.json +482 -1
- package/dist/templates/components/avatar/avatar.ts +1 -1
- package/dist/templates/components/avatar/avatar.variants.ts +4 -4
- package/dist/templates/components/badge/badge.ts +41 -6
- package/dist/templates/components/badge/badge.variants.ts +105 -11
- package/dist/templates/components/button/button.ts +74 -7
- package/dist/templates/components/button/button.variants.ts +145 -21
- package/dist/templates/components/calendar/calendar-texts.ts +72 -0
- package/dist/templates/components/calendar/calendar.ts +588 -0
- package/dist/templates/components/calendar/dates.ts +181 -0
- package/dist/templates/components/calendar/index.ts +3 -0
- package/dist/templates/components/card/card.ts +8 -5
- package/dist/templates/components/checkbox/checkbox.ts +3 -3
- package/dist/templates/components/chip/chip-texts.ts +40 -0
- package/dist/templates/components/chip/chip.ts +116 -0
- package/dist/templates/components/chip/index.ts +2 -0
- package/dist/templates/components/color-picker/color-picker-texts.ts +57 -0
- package/dist/templates/components/color-picker/color-picker.ts +718 -0
- package/dist/templates/components/color-picker/color.ts +157 -0
- package/dist/templates/components/color-picker/index.ts +3 -0
- package/dist/templates/components/combobox/combobox-texts.ts +70 -0
- package/dist/templates/components/combobox/combobox.ts +798 -0
- package/dist/templates/components/combobox/index.ts +2 -0
- package/dist/templates/components/confirm/confirm-texts.ts +46 -0
- package/dist/templates/components/confirm/confirm.ts +190 -0
- package/dist/templates/components/confirm/index.ts +2 -0
- package/dist/templates/components/context-menu/context-menu.ts +378 -0
- package/dist/templates/components/context-menu/index.ts +14 -0
- package/dist/templates/components/date-picker/date-picker.ts +240 -0
- package/dist/templates/components/date-picker/index.ts +3 -0
- package/dist/templates/components/date-range/date-range.ts +308 -0
- package/dist/templates/components/date-range/index.ts +1 -0
- package/dist/templates/components/dialog/dialog-texts.ts +41 -0
- package/dist/templates/components/dialog/dialog.ts +701 -0
- package/dist/templates/components/dialog/index.ts +18 -0
- package/dist/templates/components/dropdown-menu/dropdown-menu.ts +252 -0
- package/dist/templates/components/dropdown-menu/index.ts +14 -0
- package/dist/templates/components/file-upload/file-upload-texts.ts +77 -0
- package/dist/templates/components/file-upload/file-upload.ts +698 -0
- package/dist/templates/components/file-upload/index.ts +2 -0
- package/dist/templates/components/form-field/form-error.ts +1 -1
- package/dist/templates/components/form-field/form-field.ts +3 -2
- package/dist/templates/components/input/input.variants.ts +6 -4
- package/dist/templates/components/input-mask/index.ts +1 -0
- package/dist/templates/components/input-mask/input-mask.ts +319 -0
- package/dist/templates/components/input-otp/index.ts +1 -0
- package/dist/templates/components/input-otp/input-otp.ts +212 -0
- package/dist/templates/components/menu/index.ts +15 -0
- package/dist/templates/components/menu/menu.ts +656 -0
- package/dist/templates/components/number-input/index.ts +2 -0
- package/dist/templates/components/number-input/number-input-texts.ts +46 -0
- package/dist/templates/components/number-input/number-input.ts +450 -0
- package/dist/templates/components/option-list/index.ts +11 -0
- package/dist/templates/components/option-list/option-list.ts +518 -0
- package/dist/templates/components/pagination/index.ts +2 -0
- package/dist/templates/components/pagination/pagination-page-size.ts +74 -0
- package/dist/templates/components/pagination/pagination-texts.ts +69 -0
- package/dist/templates/components/pagination/pagination.ts +443 -0
- package/dist/templates/components/password/index.ts +2 -0
- package/dist/templates/components/password/password-texts.ts +68 -0
- package/dist/templates/components/password/password.ts +363 -0
- package/dist/templates/components/popover/index.ts +12 -0
- package/dist/templates/components/popover/popover.ts +407 -0
- package/dist/templates/components/progress/index.ts +1 -0
- package/dist/templates/components/progress/progress.ts +217 -0
- package/dist/templates/components/select/index.ts +12 -0
- package/dist/templates/components/select/select-texts.ts +62 -0
- package/dist/templates/components/select/select.ts +1086 -0
- package/dist/templates/components/separator/separator.variants.ts +10 -7
- package/dist/templates/components/skeleton/skeleton.ts +4 -1
- package/dist/templates/components/slider/index.ts +2 -0
- package/dist/templates/components/slider/slider-texts.ts +26 -0
- package/dist/templates/components/slider/slider.ts +425 -0
- package/dist/templates/components/switch/switch.ts +6 -6
- package/dist/templates/components/tabs/index.ts +2 -0
- package/dist/templates/components/tabs/tabs.ts +426 -0
- package/dist/templates/components/tags-input/index.ts +2 -0
- package/dist/templates/components/tags-input/tags-input-texts.ts +44 -0
- package/dist/templates/components/tags-input/tags-input.ts +303 -0
- package/dist/templates/components/textarea/textarea.variants.ts +14 -11
- package/dist/templates/components/toast/index.ts +3 -0
- package/dist/templates/components/toast/toast-config.ts +45 -0
- package/dist/templates/components/toast/toast-texts.ts +46 -0
- package/dist/templates/components/toast/toast.ts +1000 -0
- package/dist/templates/components/toggle-group/index.ts +1 -0
- package/dist/templates/components/toggle-group/toggle-group.ts +406 -0
- package/dist/templates/components/toolbar/index.ts +1 -0
- package/dist/templates/components/toolbar/toolbar.ts +377 -0
- package/dist/templates/components/tooltip/index.ts +7 -0
- package/dist/templates/components/tooltip/tooltip.ts +407 -0
- package/dist/templates/theme/provider.ts +75 -6
- package/dist/templates/theme/theme-base.css +410 -9
- package/dist/templates/theme/types.ts +494 -4
- package/dist/templates/utils/cn.ts +15 -1
- package/dist/templates/utils/control-styles.ts +2 -2
- package/dist/templates/utils/overlay.ts +153 -0
- package/dist/ui/index.js +184 -8
- package/dist/ui/schema.json +100 -0
- package/dist/utils/write-files.js +166 -59
- package/package.json +1 -1
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
DestroyRef,
|
|
6
|
+
afterNextRender,
|
|
7
|
+
afterRenderEffect,
|
|
8
|
+
booleanAttribute,
|
|
9
|
+
computed,
|
|
10
|
+
contentChild,
|
|
11
|
+
inject,
|
|
12
|
+
input,
|
|
13
|
+
signal,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import { MimiInput } from '@/components/ui/input';
|
|
16
|
+
import { cn } from '@/components/ui/utils/cn';
|
|
17
|
+
import { buttonFocusStyles } from '@/components/ui/utils/control-styles';
|
|
18
|
+
import { MIMI_PASSWORD_TEXT } from './password-texts';
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Password (docs/design/Mimi F3 Slider OTP Numero Mascara.dc.html, «8 · Password»; prefijo de
|
|
22
|
+
* tokens `password`, spec 13). Un contenedor que proyecta el `<input mimiInput>` del usuario
|
|
23
|
+
* (CLAUDE.md, regla 6: el valor y el formulario son del input nativo) y le agrega el botón de
|
|
24
|
+
* mostrar u ocultar y, si se pide, el medidor.
|
|
25
|
+
*
|
|
26
|
+
* El botón cambia el `type` del mismo input (no lo reemplaza: el gestor de contraseñas y el
|
|
27
|
+
* formulario conservan la referencia). El medidor es orientativo, no una medida de seguridad.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** 0: vacío; 1: débil; 2: media; 3: fuerte. */
|
|
31
|
+
export type MimiPasswordLevel = 0 | 1 | 2 | 3;
|
|
32
|
+
|
|
33
|
+
/** Una regla del medidor: su texto y si la contraseña la cumple. */
|
|
34
|
+
export interface MimiPasswordRule {
|
|
35
|
+
label: string;
|
|
36
|
+
test: (value: string) => boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* La heurística del diseño (`strength()`), generalizada a cualquier lista de reglas: todas,
|
|
41
|
+
* fuerte; al menos la mitad, media; menos, débil. Con las cuatro por defecto: 0–1 débil, 2–3
|
|
42
|
+
* media, 4 fuerte.
|
|
43
|
+
*/
|
|
44
|
+
export function mimiPasswordLevel(
|
|
45
|
+
value: string,
|
|
46
|
+
rules: readonly MimiPasswordRule[],
|
|
47
|
+
): MimiPasswordLevel {
|
|
48
|
+
if (!value) return 0;
|
|
49
|
+
if (!rules.length) return 1;
|
|
50
|
+
const passed = rules.filter((rule) => rule.test(value)).length;
|
|
51
|
+
if (passed === rules.length) return 3;
|
|
52
|
+
return passed >= rules.length / 2 ? 2 : 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const BOX_CLASSES = cn(
|
|
56
|
+
'relative flex w-full min-w-0 items-center gap-1.5 font-sans text-foreground mimi-transition',
|
|
57
|
+
'h-[var(--mimi-input-height,var(--mimi-control-height,2.5rem))] rounded-[var(--mimi-input-radius,var(--mimi-radius,0.75rem))] ps-3 pe-1',
|
|
58
|
+
'border border-input bg-input-background',
|
|
59
|
+
'not-data-[disabled]:hover:border-input-hover',
|
|
60
|
+
'focus-within:border-ring focus-within:ring-3 focus-within:ring-ring-soft',
|
|
61
|
+
'data-[invalid]:border-destructive data-[invalid]:focus-within:border-destructive data-[invalid]:focus-within:ring-destructive-ring',
|
|
62
|
+
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[var(--mimi-disabled-opacity,0.5)]',
|
|
63
|
+
// El input del usuario (mimiInput) va sin su caja: la caja es esta. Sin el ojo de Edge.
|
|
64
|
+
'[&_input]:h-full [&_input]:min-w-0 [&_input]:flex-1 [&_input]:rounded-none [&_input]:border-0 [&_input]:bg-transparent [&_input]:p-0 [&_input]:shadow-none',
|
|
65
|
+
'[&_input]:focus-visible:ring-0 [&_input]:disabled:opacity-100 [&_input::-ms-reveal]:hidden',
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const TOGGLE_CLASSES = cn(
|
|
69
|
+
'inline-flex shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-muted-foreground mimi-transition',
|
|
70
|
+
'size-[var(--mimi-password-toggle-size,32px)] rounded-[var(--mimi-password-toggle-radius,var(--mimi-radius-sm))]',
|
|
71
|
+
'[&_svg]:size-4 [&_svg]:shrink-0',
|
|
72
|
+
'enabled:hover:bg-[color:var(--mimi-password-toggle-hover,var(--mimi-accent))] enabled:hover:text-foreground enabled:active:scale-(--mimi-press-scale-sm)',
|
|
73
|
+
'disabled:cursor-not-allowed',
|
|
74
|
+
buttonFocusStyles,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/** Barra vacía y barras con el color del nivel (globales del Grupo 2 con respaldo, G2.0b). */
|
|
78
|
+
const BAR_EMPTY = 'bg-[color:var(--mimi-password-meter-track,var(--mimi-track,var(--mimi-input)))]';
|
|
79
|
+
const BAR_FILL: Record<1 | 2 | 3, string> = {
|
|
80
|
+
1: 'bg-[color:var(--mimi-strength-weak,var(--mimi-destructive))]',
|
|
81
|
+
2: 'bg-[color:var(--mimi-strength-medium,var(--mimi-warning))]',
|
|
82
|
+
3: 'bg-[color:var(--mimi-strength-strong,var(--mimi-success))]',
|
|
83
|
+
};
|
|
84
|
+
const LEVEL_TEXT: Record<1 | 2 | 3, string> = {
|
|
85
|
+
1: 'text-destructive-soft-foreground',
|
|
86
|
+
2: 'text-warning-soft-foreground',
|
|
87
|
+
3: 'text-success-soft-foreground',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let nextId = 0;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Password:
|
|
94
|
+
*
|
|
95
|
+
* ```html
|
|
96
|
+
* <mimi-password meter>
|
|
97
|
+
* <input mimiInput type="password" autocomplete="new-password" formControlName="password" />
|
|
98
|
+
* </mimi-password>
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* El `type`, el `autocomplete` y el nombre del campo son del usuario.
|
|
102
|
+
*/
|
|
103
|
+
@Component({
|
|
104
|
+
selector: 'mimi-password',
|
|
105
|
+
template: `
|
|
106
|
+
<div
|
|
107
|
+
data-slot="box"
|
|
108
|
+
[class]="boxClasses()"
|
|
109
|
+
[attr.data-invalid]="invalid() ? '' : null"
|
|
110
|
+
[attr.data-disabled]="isDisabled() ? '' : null"
|
|
111
|
+
>
|
|
112
|
+
<ng-content />
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
data-slot="toggle"
|
|
116
|
+
[class]="toggleClasses"
|
|
117
|
+
[attr.aria-label]="text.show"
|
|
118
|
+
[attr.aria-pressed]="shown()"
|
|
119
|
+
[disabled]="isDisabled()"
|
|
120
|
+
(click)="toggle()"
|
|
121
|
+
>
|
|
122
|
+
@if (shown()) {
|
|
123
|
+
<svg
|
|
124
|
+
viewBox="0 0 24 24"
|
|
125
|
+
fill="none"
|
|
126
|
+
stroke="currentColor"
|
|
127
|
+
stroke-width="2"
|
|
128
|
+
stroke-linecap="round"
|
|
129
|
+
stroke-linejoin="round"
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
>
|
|
132
|
+
<!-- Lucide eye-off (ISC) -->
|
|
133
|
+
<path
|
|
134
|
+
d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"
|
|
135
|
+
/>
|
|
136
|
+
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
|
137
|
+
<path
|
|
138
|
+
d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"
|
|
139
|
+
/>
|
|
140
|
+
<path d="m2 2 20 20" />
|
|
141
|
+
</svg>
|
|
142
|
+
} @else {
|
|
143
|
+
<svg
|
|
144
|
+
viewBox="0 0 24 24"
|
|
145
|
+
fill="none"
|
|
146
|
+
stroke="currentColor"
|
|
147
|
+
stroke-width="2"
|
|
148
|
+
stroke-linecap="round"
|
|
149
|
+
stroke-linejoin="round"
|
|
150
|
+
aria-hidden="true"
|
|
151
|
+
>
|
|
152
|
+
<!-- Lucide eye (ISC) -->
|
|
153
|
+
<path
|
|
154
|
+
d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"
|
|
155
|
+
/>
|
|
156
|
+
<circle cx="12" cy="12" r="3" />
|
|
157
|
+
</svg>
|
|
158
|
+
}
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
@if (meter()) {
|
|
162
|
+
<div data-slot="meter" class="flex flex-col gap-2" [id]="meterId">
|
|
163
|
+
<div class="flex items-center gap-2.5">
|
|
164
|
+
<div
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
class="grid flex-1 grid-cols-4 gap-[var(--mimi-password-meter-gap,4px)]"
|
|
167
|
+
>
|
|
168
|
+
@for (bar of bars(); track $index) {
|
|
169
|
+
<span
|
|
170
|
+
class="h-[var(--mimi-password-meter-height,6px)] rounded-full transition-colors duration-300 motion-reduce:transition-none"
|
|
171
|
+
[class]="bar"
|
|
172
|
+
></span>
|
|
173
|
+
}
|
|
174
|
+
</div>
|
|
175
|
+
<span
|
|
176
|
+
data-slot="level"
|
|
177
|
+
class="min-w-12 text-end text-[13px] font-semibold"
|
|
178
|
+
[class]="levelClass()"
|
|
179
|
+
>{{ levelText() }}</span
|
|
180
|
+
>
|
|
181
|
+
</div>
|
|
182
|
+
@if (activeRules().length) {
|
|
183
|
+
<ul class="m-0 grid list-none grid-cols-2 gap-x-3 gap-y-1 p-0">
|
|
184
|
+
@for (rule of ruleState(); track rule.label) {
|
|
185
|
+
<li
|
|
186
|
+
class="flex items-center gap-1.5 text-[12.5px]"
|
|
187
|
+
[class]="rule.ok ? 'text-success-soft-foreground' : 'text-muted-foreground'"
|
|
188
|
+
[attr.data-state]="rule.ok ? 'met' : 'unmet'"
|
|
189
|
+
>
|
|
190
|
+
@if (rule.ok) {
|
|
191
|
+
<svg
|
|
192
|
+
class="size-3.5 shrink-0"
|
|
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
|
+
>
|
|
201
|
+
<!-- Lucide circle-check (ISC) -->
|
|
202
|
+
<circle cx="12" cy="12" r="10" />
|
|
203
|
+
<path d="m9 12 2 2 4-4" />
|
|
204
|
+
</svg>
|
|
205
|
+
} @else {
|
|
206
|
+
<svg
|
|
207
|
+
class="size-3.5 shrink-0"
|
|
208
|
+
viewBox="0 0 24 24"
|
|
209
|
+
fill="none"
|
|
210
|
+
stroke="currentColor"
|
|
211
|
+
stroke-width="2"
|
|
212
|
+
stroke-linecap="round"
|
|
213
|
+
stroke-linejoin="round"
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
>
|
|
216
|
+
<!-- Lucide circle (ISC) -->
|
|
217
|
+
<circle cx="12" cy="12" r="10" />
|
|
218
|
+
</svg>
|
|
219
|
+
}
|
|
220
|
+
{{ rule.label }}
|
|
221
|
+
</li>
|
|
222
|
+
}
|
|
223
|
+
</ul>
|
|
224
|
+
}
|
|
225
|
+
</div>
|
|
226
|
+
}
|
|
227
|
+
`,
|
|
228
|
+
host: {
|
|
229
|
+
class: 'flex w-full min-w-0 flex-col gap-2',
|
|
230
|
+
'[attr.data-state]': 'shown() ? "shown" : "hidden"',
|
|
231
|
+
'[attr.data-level]': 'meter() ? level() : null',
|
|
232
|
+
'(input)': 'read()',
|
|
233
|
+
'(change)': 'read()',
|
|
234
|
+
},
|
|
235
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
236
|
+
})
|
|
237
|
+
export class MimiPassword {
|
|
238
|
+
private readonly announcer = inject(LiveAnnouncer);
|
|
239
|
+
protected readonly text = inject(MIMI_PASSWORD_TEXT);
|
|
240
|
+
|
|
241
|
+
/** Muestra el medidor (nivel y reglas) debajo del campo. */
|
|
242
|
+
readonly meter = input(false, { transform: booleanAttribute });
|
|
243
|
+
/**
|
|
244
|
+
* Reglas del medidor. Por defecto, las cuatro del diseño. Con `[]` no hay lista y la heurística
|
|
245
|
+
* no tiene qué contar (todo es «débil»): el nivel lo da `strength`.
|
|
246
|
+
*/
|
|
247
|
+
readonly rules = input<readonly MimiPasswordRule[] | undefined>(undefined);
|
|
248
|
+
/** Nivel propio (0 a 3). Por defecto, la heurística de las reglas (orientativa). */
|
|
249
|
+
readonly strength = input<((value: string) => MimiPasswordLevel) | undefined>(undefined);
|
|
250
|
+
/** Clases de la caja, mezcladas con `cn()` (gana la del usuario). */
|
|
251
|
+
readonly userClass = input('', { alias: 'class' });
|
|
252
|
+
|
|
253
|
+
private readonly field = contentChild(MimiInput);
|
|
254
|
+
/** El input del usuario: el de mimiInput o, si no lo usa, el primero que haya. */
|
|
255
|
+
private readonly inputElement = computed(
|
|
256
|
+
() => (this.field()?.controlElement as HTMLInputElement | undefined) ?? null,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
protected readonly shown = signal(false);
|
|
260
|
+
protected readonly value = signal('');
|
|
261
|
+
/** `disabled` escrito en el input (sin formulario). */
|
|
262
|
+
private readonly nativeDisabled = signal(false);
|
|
263
|
+
protected readonly isDisabled = computed(
|
|
264
|
+
() => this.nativeDisabled() || (this.field()?.fieldState.disabled() ?? false),
|
|
265
|
+
);
|
|
266
|
+
protected readonly invalid = computed(() => this.field()?.showInvalid() ?? false);
|
|
267
|
+
|
|
268
|
+
readonly meterId = `mimi-password-meter-${nextId++}`;
|
|
269
|
+
|
|
270
|
+
/** Reglas activas (las del usuario o las cuatro del diseño, con los textos del idioma). */
|
|
271
|
+
protected readonly activeRules = computed<readonly MimiPasswordRule[]>(
|
|
272
|
+
() =>
|
|
273
|
+
this.rules() ?? [
|
|
274
|
+
{ label: this.text.minLength, test: (v) => v.length >= 8 },
|
|
275
|
+
{ label: this.text.uppercase, test: (v) => /\p{Lu}/u.test(v) },
|
|
276
|
+
{ label: this.text.number, test: (v) => /\d/.test(v) },
|
|
277
|
+
{ label: this.text.symbol, test: (v) => /[^\p{L}\p{N}\s]/u.test(v) },
|
|
278
|
+
],
|
|
279
|
+
);
|
|
280
|
+
protected readonly ruleState = computed(() =>
|
|
281
|
+
this.activeRules().map((rule) => ({ label: rule.label, ok: rule.test(this.value()) })),
|
|
282
|
+
);
|
|
283
|
+
readonly level = computed<MimiPasswordLevel>(() => {
|
|
284
|
+
const value = this.value();
|
|
285
|
+
if (!value) return 0;
|
|
286
|
+
const own = this.strength();
|
|
287
|
+
return own ? own(value) : mimiPasswordLevel(value, this.activeRules());
|
|
288
|
+
});
|
|
289
|
+
protected readonly levelText = computed(() => {
|
|
290
|
+
const level = this.level();
|
|
291
|
+
return level === 0 ? '' : [this.text.weak, this.text.medium, this.text.strong][level - 1];
|
|
292
|
+
});
|
|
293
|
+
protected readonly levelClass = computed(() => {
|
|
294
|
+
const level = this.level();
|
|
295
|
+
return level === 0 ? 'text-muted-foreground' : LEVEL_TEXT[level];
|
|
296
|
+
});
|
|
297
|
+
/** Barras llenas como el diseño (`bars()`): débil 1, media 2 o 3 (según las reglas), fuerte 4. */
|
|
298
|
+
protected readonly bars = computed(() => {
|
|
299
|
+
const level = this.level();
|
|
300
|
+
if (level === 0) return [BAR_EMPTY, BAR_EMPTY, BAR_EMPTY, BAR_EMPTY];
|
|
301
|
+
const passed = this.ruleState().filter((r) => r.ok).length;
|
|
302
|
+
const filled = level === 3 ? 4 : level === 2 ? Math.max(2, Math.min(passed, 3)) : 1;
|
|
303
|
+
return [0, 1, 2, 3].map((i) => (i < filled ? BAR_FILL[level] : BAR_EMPTY));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
protected readonly boxClasses = computed(() => cn(BOX_CLASSES, this.userClass()));
|
|
307
|
+
protected readonly toggleClasses = TOGGLE_CLASSES;
|
|
308
|
+
|
|
309
|
+
/** El último nivel anunciado: solo se anuncia un cambio que viene de escribir. */
|
|
310
|
+
private announced: MimiPasswordLevel = 0;
|
|
311
|
+
private typed = false;
|
|
312
|
+
|
|
313
|
+
constructor() {
|
|
314
|
+
afterNextRender(() => {
|
|
315
|
+
const el = this.inputElement();
|
|
316
|
+
if (!el) return;
|
|
317
|
+
this.value.set(el.value);
|
|
318
|
+
this.announced = this.level();
|
|
319
|
+
this.nativeDisabled.set(el.disabled);
|
|
320
|
+
});
|
|
321
|
+
// El medidor describe el input (sin borrar el aria-describedby del usuario ni el de
|
|
322
|
+
// mimi-form-field).
|
|
323
|
+
afterRenderEffect({
|
|
324
|
+
write: () => {
|
|
325
|
+
const el = this.inputElement();
|
|
326
|
+
if (!el) return;
|
|
327
|
+
const ids = (el.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
|
|
328
|
+
const has = ids.includes(this.meterId);
|
|
329
|
+
if (this.meter() && !has) ids.push(this.meterId);
|
|
330
|
+
if (!this.meter() && has) ids.splice(ids.indexOf(this.meterId), 1);
|
|
331
|
+
if (ids.length) el.setAttribute('aria-describedby', ids.join(' '));
|
|
332
|
+
else el.removeAttribute('aria-describedby');
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
inject(DestroyRef).onDestroy(() => {
|
|
336
|
+
const el = this.inputElement();
|
|
337
|
+
if (el) el.type = 'password';
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Muestra u oculta: cambia el `type` del mismo input. El foco se queda en el botón. */
|
|
342
|
+
protected toggle(): void {
|
|
343
|
+
const el = this.inputElement();
|
|
344
|
+
if (!el || this.isDisabled()) return;
|
|
345
|
+
const shown = !this.shown();
|
|
346
|
+
el.type = shown ? 'text' : 'password';
|
|
347
|
+
this.shown.set(shown);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Lee el valor al escribir y anuncia el nivel solo si cambió. */
|
|
351
|
+
protected read(): void {
|
|
352
|
+
const el = this.inputElement();
|
|
353
|
+
if (!el) return;
|
|
354
|
+
this.typed = true;
|
|
355
|
+
this.value.set(el.value);
|
|
356
|
+
this.nativeDisabled.set(el.disabled);
|
|
357
|
+
const level = this.level();
|
|
358
|
+
if (this.meter() && this.typed && level !== this.announced) {
|
|
359
|
+
this.announced = level;
|
|
360
|
+
if (level > 0) void this.announcer.announce(this.text.announce(this.levelText()), 'polite');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Solo la API pública: MimiPopoverOutlet es interno (se queda en './popover').
|
|
2
|
+
export {
|
|
3
|
+
MimiPopover,
|
|
4
|
+
type MimiPopoverAlign,
|
|
5
|
+
MimiPopoverClose,
|
|
6
|
+
MimiPopoverContent,
|
|
7
|
+
MimiPopoverDescription,
|
|
8
|
+
MimiPopoverImports,
|
|
9
|
+
type MimiPopoverSide,
|
|
10
|
+
MimiPopoverTitle,
|
|
11
|
+
MimiPopoverTrigger,
|
|
12
|
+
} from './popover';
|