@mesgflow/ui-kit 0.11.1 → 0.12.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/fesm2022/mesgflow-ui-kit-core.mjs +24 -1
- package/fesm2022/mesgflow-ui-kit-core.mjs.map +1 -1
- package/fesm2022/mesgflow-ui-kit-i18n-tr.mjs +22 -0
- package/fesm2022/mesgflow-ui-kit-i18n-tr.mjs.map +1 -1
- package/fesm2022/mesgflow-ui-kit-password-strength.mjs +590 -0
- package/fesm2022/mesgflow-ui-kit-password-strength.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit.mjs +1 -0
- package/fesm2022/mesgflow-ui-kit.mjs.map +1 -1
- package/package.json +5 -1
- package/tokens.json +1 -1
- package/types/mesgflow-ui-kit-core.d.ts +38 -2
- package/types/mesgflow-ui-kit-password-strength.d.ts +180 -0
- package/types/mesgflow-ui-kit.d.ts +1 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, input, inject, DOCUMENT, computed, afterRenderEffect, Component } from '@angular/core';
|
|
3
|
+
import { MF_PASSWORD_STRENGTH_LABELS, mergeLabels, nextId, cn } from '@mesgflow/ui-kit/core';
|
|
4
|
+
import { Icon } from '@mesgflow/ui-kit/icon';
|
|
5
|
+
import { cva } from 'class-variance-authority';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sızıntı listelerinin en tepesindeki parolalar + parola tabanı olarak en sık kullanılan kelimeler
|
|
9
|
+
* (EN + TR). Tam bir sözlük DEĞİL — tahmincinin en bariz tahminleri yakalaması için küçük bir
|
|
10
|
+
* örneklem (~3 KB). Girdiler küçük harf; tahminci karşılaştırmadan önce iki tarafı da leet
|
|
11
|
+
* normalizasyonundan geçirir (`p@ssw0rd` → `password`), bu yüzden leet varyantları ayrıca yazılmaz.
|
|
12
|
+
* Tam kapsam isteyen tüketici `MF_PASSWORD_STRENGTH_ESTIMATOR` ile `zxcvbn-ts` takar.
|
|
13
|
+
*/
|
|
14
|
+
const COMMON_PASSWORDS = [
|
|
15
|
+
// Sızıntı listelerinin tepesi
|
|
16
|
+
'password',
|
|
17
|
+
'passwort',
|
|
18
|
+
'passw',
|
|
19
|
+
'pass',
|
|
20
|
+
'qwerty',
|
|
21
|
+
'qwertz',
|
|
22
|
+
'azerty',
|
|
23
|
+
'letmein',
|
|
24
|
+
'welcome',
|
|
25
|
+
'admin',
|
|
26
|
+
'administrator',
|
|
27
|
+
'root',
|
|
28
|
+
'login',
|
|
29
|
+
'iloveyou',
|
|
30
|
+
'monkey',
|
|
31
|
+
'dragon',
|
|
32
|
+
'master',
|
|
33
|
+
'shadow',
|
|
34
|
+
'superman',
|
|
35
|
+
'batman',
|
|
36
|
+
'trustno1',
|
|
37
|
+
'whatever',
|
|
38
|
+
'freedom',
|
|
39
|
+
'sunshine',
|
|
40
|
+
'princess',
|
|
41
|
+
'football',
|
|
42
|
+
'baseball',
|
|
43
|
+
'soccer',
|
|
44
|
+
'hockey',
|
|
45
|
+
'basketball',
|
|
46
|
+
'starwars',
|
|
47
|
+
'pokemon',
|
|
48
|
+
'computer',
|
|
49
|
+
'internet',
|
|
50
|
+
'secret',
|
|
51
|
+
'default',
|
|
52
|
+
'changeme',
|
|
53
|
+
'guest',
|
|
54
|
+
'test',
|
|
55
|
+
'tester',
|
|
56
|
+
'demo',
|
|
57
|
+
'user',
|
|
58
|
+
'access',
|
|
59
|
+
'hello',
|
|
60
|
+
'love',
|
|
61
|
+
'lovely',
|
|
62
|
+
'charlie',
|
|
63
|
+
'michael',
|
|
64
|
+
'jennifer',
|
|
65
|
+
'jordan',
|
|
66
|
+
'hunter',
|
|
67
|
+
'ranger',
|
|
68
|
+
'buster',
|
|
69
|
+
'thomas',
|
|
70
|
+
'robert',
|
|
71
|
+
'daniel',
|
|
72
|
+
'andrew',
|
|
73
|
+
'joshua',
|
|
74
|
+
'jessica',
|
|
75
|
+
'ashley',
|
|
76
|
+
'nicole',
|
|
77
|
+
'killer',
|
|
78
|
+
'cheese',
|
|
79
|
+
'cookie',
|
|
80
|
+
'chocolate',
|
|
81
|
+
'flower',
|
|
82
|
+
'summer',
|
|
83
|
+
'winter',
|
|
84
|
+
'spring',
|
|
85
|
+
'autumn',
|
|
86
|
+
'monday',
|
|
87
|
+
'friday',
|
|
88
|
+
'january',
|
|
89
|
+
'october',
|
|
90
|
+
'december',
|
|
91
|
+
'banana',
|
|
92
|
+
'orange',
|
|
93
|
+
'purple',
|
|
94
|
+
'yellow',
|
|
95
|
+
'silver',
|
|
96
|
+
'golden',
|
|
97
|
+
'diamond',
|
|
98
|
+
'matrix',
|
|
99
|
+
'mustang',
|
|
100
|
+
'ferrari',
|
|
101
|
+
'harley',
|
|
102
|
+
'corvette',
|
|
103
|
+
'maverick',
|
|
104
|
+
'phoenix',
|
|
105
|
+
'tigger',
|
|
106
|
+
'pepper',
|
|
107
|
+
'ginger',
|
|
108
|
+
'maggie',
|
|
109
|
+
'bailey',
|
|
110
|
+
'snoopy',
|
|
111
|
+
'family',
|
|
112
|
+
'friends',
|
|
113
|
+
'money',
|
|
114
|
+
'heaven',
|
|
115
|
+
'angel',
|
|
116
|
+
'blessed',
|
|
117
|
+
'jesus',
|
|
118
|
+
'samsung',
|
|
119
|
+
'apple',
|
|
120
|
+
'google',
|
|
121
|
+
'microsoft',
|
|
122
|
+
'facebook',
|
|
123
|
+
'linkedin',
|
|
124
|
+
'company',
|
|
125
|
+
'business',
|
|
126
|
+
'office',
|
|
127
|
+
'zaq1zaq1',
|
|
128
|
+
'asdfgh',
|
|
129
|
+
'zxcvbn',
|
|
130
|
+
'qazwsx',
|
|
131
|
+
'1q2w3e4r',
|
|
132
|
+
'abc123',
|
|
133
|
+
// TR
|
|
134
|
+
'sifre',
|
|
135
|
+
'parola',
|
|
136
|
+
'parolam',
|
|
137
|
+
'sifrem',
|
|
138
|
+
'gizli',
|
|
139
|
+
'merhaba',
|
|
140
|
+
'selam',
|
|
141
|
+
'askim',
|
|
142
|
+
'sevgilim',
|
|
143
|
+
'canim',
|
|
144
|
+
'bitanem',
|
|
145
|
+
'hayatim',
|
|
146
|
+
'annem',
|
|
147
|
+
'babam',
|
|
148
|
+
'kardesim',
|
|
149
|
+
'bismillah',
|
|
150
|
+
'allah',
|
|
151
|
+
'galatasaray',
|
|
152
|
+
'fenerbahce',
|
|
153
|
+
'besiktas',
|
|
154
|
+
'trabzonspor',
|
|
155
|
+
'cimbom',
|
|
156
|
+
'istanbul',
|
|
157
|
+
'ankara',
|
|
158
|
+
'izmir',
|
|
159
|
+
'turkiye',
|
|
160
|
+
'turkey',
|
|
161
|
+
'ataturk',
|
|
162
|
+
'mustafa',
|
|
163
|
+
'mehmet',
|
|
164
|
+
'ahmet',
|
|
165
|
+
'ayse',
|
|
166
|
+
'fatma',
|
|
167
|
+
'zeynep',
|
|
168
|
+
'emine',
|
|
169
|
+
'yildiz',
|
|
170
|
+
'gunes',
|
|
171
|
+
'deniz',
|
|
172
|
+
'kartal',
|
|
173
|
+
'aslan',
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
/** Değerlendirilen azami uzunluk (kod noktası) — çok uzun yapıştırmalarda maliyet tavanı. */
|
|
177
|
+
const MAX_LENGTH = 128;
|
|
178
|
+
/** Skor eşikleri (bit) — sırasıyla 1, 2, 3, 4 için alt sınır. */
|
|
179
|
+
const THRESHOLDS = [28, 40, 56, 72];
|
|
180
|
+
/** Bu uzunluktan kısa yaygın kelimeler parolanın İÇİNDE aranmaz (yanlış pozitif olur). */
|
|
181
|
+
const MIN_EMBEDDED_WORD = 5;
|
|
182
|
+
/** Kişisel değer parçası olarak sayılacak asgari uzunluk. */
|
|
183
|
+
const MIN_PERSONAL_TOKEN = 3;
|
|
184
|
+
const LEET = {
|
|
185
|
+
'@': 'a',
|
|
186
|
+
'4': 'a',
|
|
187
|
+
'8': 'b',
|
|
188
|
+
'3': 'e',
|
|
189
|
+
'1': 'i',
|
|
190
|
+
'!': 'i',
|
|
191
|
+
'0': 'o',
|
|
192
|
+
$: 's',
|
|
193
|
+
'5': 's',
|
|
194
|
+
'7': 't',
|
|
195
|
+
};
|
|
196
|
+
const KEYBOARD_ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm', 'qwertzuiop', 'azertyuiop'];
|
|
197
|
+
const deleet = (s) => Array.from(s, (c) => LEET[c] ?? c).join('');
|
|
198
|
+
const COMMON = new Set(COMMON_PASSWORDS.map(deleet));
|
|
199
|
+
const COMMON_EMBEDDED = [...COMMON].filter((w) => w.length >= MIN_EMBEDDED_WORD);
|
|
200
|
+
/** İki harf aynı klavye satırında komşuysa +1/-1 yön, değilse 0. */
|
|
201
|
+
function keyboardDirection(a, b) {
|
|
202
|
+
for (const row of KEYBOARD_ROWS) {
|
|
203
|
+
const i = row.indexOf(a);
|
|
204
|
+
if (i === -1)
|
|
205
|
+
continue;
|
|
206
|
+
const j = row.indexOf(b);
|
|
207
|
+
if (j !== -1 && Math.abs(i - j) === 1)
|
|
208
|
+
return j - i;
|
|
209
|
+
}
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Bağımlılıksız, birkaç KB'lık tahmin edilebilirlik tahmincisi. "1 büyük harf + 1 rakam" saymaz
|
|
214
|
+
* (NIST 800-63B): her karakterin TAHMİN EDİLEMEZ olup olmadığına bakar, tahmin edilebilir
|
|
215
|
+
* olanları (tekrar, ardışık dizi, klavye satırı, yıl, yaygın kelime, kişisel değer) entropiye
|
|
216
|
+
* saymaz, kalan "etkin uzunluk × log2(karakter havuzu)" bitini 0–4'e eşler.
|
|
217
|
+
*
|
|
218
|
+
* Sınırı: tam bir sözlük taşımaz — sözlükte olup listede olmayan kelime tabanlı parolaları
|
|
219
|
+
* fazla puanlayabilir. Tam kapsam için `zxcvbn-ts`'i `MF_PASSWORD_STRENGTH_ESTIMATOR` ile takın.
|
|
220
|
+
*/
|
|
221
|
+
function estimatePasswordStrength(value, options = {}) {
|
|
222
|
+
const minLength = options.minLength ?? 8;
|
|
223
|
+
const chars = Array.from(value).slice(0, MAX_LENGTH);
|
|
224
|
+
if (!chars.length)
|
|
225
|
+
return { score: 0 };
|
|
226
|
+
// Kod noktası başına TEK karakterlik küçük harf izi — indeksler `chars` ile birebir hizalı kalsın
|
|
227
|
+
// diye uzunluğu değişen dönüşümler (`İ` → `i̇`, astral karakterler) yer tutucuyla değiştirilir.
|
|
228
|
+
const lower = chars.map((c) => {
|
|
229
|
+
const l = c.toLowerCase();
|
|
230
|
+
return l.length === 1 ? l : '';
|
|
231
|
+
});
|
|
232
|
+
const flat = lower.join('');
|
|
233
|
+
const plain = deleet(flat);
|
|
234
|
+
const predictable = new Array(chars.length).fill(false);
|
|
235
|
+
const found = new Set();
|
|
236
|
+
const tooShort = chars.length < minLength;
|
|
237
|
+
// 1. Yaygın parola — tamamı ya da sonundaki rakam/sembol kırpılmış hali.
|
|
238
|
+
const base = flat.replace(/[\d\p{P}\p{S}\s]+$/u, '');
|
|
239
|
+
if (COMMON.has(plain) || (base.length >= 4 && COMMON.has(deleet(base)))) {
|
|
240
|
+
return { score: 0, warning: tooShort ? 'tooShort' : 'common' };
|
|
241
|
+
}
|
|
242
|
+
// İlki hariç bir aralığı tahmin edilebilir işaretler (kelime TEK bir "karakter" kadar bilgi taşır).
|
|
243
|
+
const markRange = (start, length, reason) => {
|
|
244
|
+
for (let i = start + 1; i < start + length && i < chars.length; i++)
|
|
245
|
+
predictable[i] = true;
|
|
246
|
+
found.add(reason);
|
|
247
|
+
};
|
|
248
|
+
const markAll = (haystack, needle, reason) => {
|
|
249
|
+
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) {
|
|
250
|
+
markRange(at, needle.length, reason);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
// 2. Gömülü yaygın kelimeler (`MySummer!x`).
|
|
254
|
+
for (const word of COMMON_EMBEDDED)
|
|
255
|
+
markAll(plain, word, 'common');
|
|
256
|
+
// 3. Kişisel değerler — e-postanın yerel kısmı ve alan adı dahil, harf/rakam olmayan yerden bölünür.
|
|
257
|
+
for (const input of options.userInputs ?? []) {
|
|
258
|
+
const tokens = input
|
|
259
|
+
.toLowerCase()
|
|
260
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
261
|
+
.filter((t) => t.length >= MIN_PERSONAL_TOKEN);
|
|
262
|
+
for (const token of new Set(tokens)) {
|
|
263
|
+
markAll(flat, token, 'personal');
|
|
264
|
+
markAll(plain, deleet(token), 'personal');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// 4. Yıllar (19xx/20xx) — iki rakam kadar bilgi.
|
|
268
|
+
for (const m of flat.matchAll(/(?:19|20)\d\d/g)) {
|
|
269
|
+
const at = m.index ?? 0;
|
|
270
|
+
predictable[at + 2] = true;
|
|
271
|
+
predictable[at + 3] = true;
|
|
272
|
+
found.add('sequence');
|
|
273
|
+
}
|
|
274
|
+
// 5. Tekrar eden bloklar (`abcabc`, `xk9v…xk9v`) — daha önce geçmiş bir bloğun kopyası bilgi taşımaz.
|
|
275
|
+
for (let i = 1; i < chars.length;) {
|
|
276
|
+
let copy = 0;
|
|
277
|
+
for (let len = Math.min(chars.length - i, i); len >= 2; len--) {
|
|
278
|
+
if (flat.slice(0, i).includes(flat.slice(i, i + len))) {
|
|
279
|
+
copy = len;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (copy) {
|
|
284
|
+
for (let k = i; k < i + copy; k++)
|
|
285
|
+
predictable[k] = true;
|
|
286
|
+
found.add('repeated');
|
|
287
|
+
i += copy;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
i++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// 6. Karakter karakter: tekrar (`aaa`), ardışık kod noktası (`abcd`, `4321`), klavye satırı (`asdf`).
|
|
294
|
+
let previousStep = 0;
|
|
295
|
+
let previousKey = 0;
|
|
296
|
+
for (let i = 1; i < chars.length; i++) {
|
|
297
|
+
const step = (flat.codePointAt(i) ?? 0) - (flat.codePointAt(i - 1) ?? 0);
|
|
298
|
+
const key = keyboardDirection(flat[i - 1], flat[i]);
|
|
299
|
+
if (step === 0) {
|
|
300
|
+
predictable[i] = true;
|
|
301
|
+
found.add('repeated');
|
|
302
|
+
}
|
|
303
|
+
else if (Math.abs(step) === 1 && step === previousStep) {
|
|
304
|
+
predictable[i] = true;
|
|
305
|
+
found.add('sequence');
|
|
306
|
+
}
|
|
307
|
+
else if (key !== 0 && key === previousKey) {
|
|
308
|
+
predictable[i] = true;
|
|
309
|
+
found.add('sequence');
|
|
310
|
+
}
|
|
311
|
+
previousStep = step;
|
|
312
|
+
previousKey = key;
|
|
313
|
+
}
|
|
314
|
+
// Karakter havuzu — Unicode sınıflarıyla (Türkçe `ğ`/`Ş` doğru sınıfa düşer).
|
|
315
|
+
let pool = 0;
|
|
316
|
+
if (chars.some((c) => /\p{Ll}/u.test(c)))
|
|
317
|
+
pool += 26;
|
|
318
|
+
if (chars.some((c) => /\p{Lu}/u.test(c)))
|
|
319
|
+
pool += 26;
|
|
320
|
+
if (chars.some((c) => /\p{Nd}/u.test(c)))
|
|
321
|
+
pool += 10;
|
|
322
|
+
if (chars.some((c) => /[^\p{L}\p{N}]/u.test(c)))
|
|
323
|
+
pool += 33;
|
|
324
|
+
if (chars.some((c) => (c.codePointAt(0) ?? 0) > 0x7f))
|
|
325
|
+
pool += 32;
|
|
326
|
+
const effective = Math.max(1, predictable.filter((p) => !p).length);
|
|
327
|
+
const bits = effective * Math.log2(Math.max(pool, 2));
|
|
328
|
+
let score = THRESHOLDS.filter((t) => bits >= t).length;
|
|
329
|
+
if (tooShort)
|
|
330
|
+
score = Math.min(score, 1);
|
|
331
|
+
// İpucu yalnızca parola henüz iyi DEĞİLKEN — güçlü bir parolaya "şundan kaçının" denmez.
|
|
332
|
+
if (score >= 3)
|
|
333
|
+
return { score };
|
|
334
|
+
const warning = tooShort
|
|
335
|
+
? 'tooShort'
|
|
336
|
+
: ['common', 'personal', 'sequence', 'repeated'].find((w) => found.has(w));
|
|
337
|
+
return warning ? { score, warning } : { score };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* `mf-password-strength`'in kullandığı tahminci — varsayılan `estimatePasswordStrength`.
|
|
341
|
+
* Uygulama genelinde değiştirmek için (ör. `zxcvbn-ts`):
|
|
342
|
+
* `{ provide: MF_PASSWORD_STRENGTH_ESTIMATOR, useValue: (v, o) => fromZxcvbn(zxcvbn(v, o.userInputs)) }`.
|
|
343
|
+
*/
|
|
344
|
+
const MF_PASSWORD_STRENGTH_ESTIMATOR = new InjectionToken('mesgflow.password-strength-estimator', { factory: () => estimatePasswordStrength });
|
|
345
|
+
/** Skoru görsel seviyeye eşler: 0–1 zayıf, 2 orta, 3 iyi, 4 güçlü. */
|
|
346
|
+
function passwordStrengthLevel(score) {
|
|
347
|
+
return score <= 1 ? 'weak' : score === 2 ? 'fair' : score === 3 ? 'good' : 'strong';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const PATTERNS = {
|
|
351
|
+
lowercase: /\p{Ll}/u,
|
|
352
|
+
uppercase: /\p{Lu}/u,
|
|
353
|
+
digit: /\p{Nd}/u,
|
|
354
|
+
symbol: /[^\p{L}\p{N}\s]/u,
|
|
355
|
+
};
|
|
356
|
+
/** Tek bir gereksinimi sınar — Unicode-farkında (`ş` küçük harf, `Ğ` büyük harf sayılır). */
|
|
357
|
+
function meetsPasswordRequirement(value, requirement) {
|
|
358
|
+
switch (requirement.kind) {
|
|
359
|
+
case 'minLength':
|
|
360
|
+
return Array.from(value).length >= requirement.value;
|
|
361
|
+
case 'custom':
|
|
362
|
+
return requirement.test(value);
|
|
363
|
+
default:
|
|
364
|
+
return PATTERNS[requirement.kind].test(value);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** Gereksinim listesini sınar — form doğrulayıcısında da aynı sonucu üretmek için dışa açık. */
|
|
368
|
+
function checkPasswordRequirements(value, requirements) {
|
|
369
|
+
return requirements.map((requirement) => ({
|
|
370
|
+
requirement,
|
|
371
|
+
met: meetsPasswordRequirement(value, requirement),
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Segment satırı — boyut ekseni `mf-progress` çubuğunun kalınlıklarıyla aynı ritimde. */
|
|
376
|
+
const passwordStrengthTrackVariants = cva('flex w-full', {
|
|
377
|
+
variants: {
|
|
378
|
+
size: {
|
|
379
|
+
sm: 'h-1 gap-1',
|
|
380
|
+
md: 'h-1.5 gap-1.5',
|
|
381
|
+
lg: 'h-2 gap-1.5',
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
defaultVariants: { size: 'md' },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const SEGMENTS = [0, 1, 2, 3];
|
|
388
|
+
/** Segmentlerin art arda dolması için kademe (ms). */
|
|
389
|
+
const SEGMENT_STAGGER_MS = 40;
|
|
390
|
+
/** Karşılanmamış gereksinim varken gösterilen azami skor — form reddedecekken "Güçlü" yazmasın. */
|
|
391
|
+
const UNSATISFIED_CAP = 2;
|
|
392
|
+
// Tailwind dinamik sınıf adı göremez — ton sınıfları literal kalmalı.
|
|
393
|
+
const SEGMENT_TONE = {
|
|
394
|
+
weak: 'bg-danger-fill',
|
|
395
|
+
fair: 'bg-warning-fill',
|
|
396
|
+
good: 'bg-success-fill',
|
|
397
|
+
strong: 'bg-success-fill',
|
|
398
|
+
};
|
|
399
|
+
const TEXT_TONE = {
|
|
400
|
+
weak: 'text-danger',
|
|
401
|
+
fair: 'text-warning',
|
|
402
|
+
good: 'text-success',
|
|
403
|
+
strong: 'text-success',
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* `<mf-password-input inputId="pw" autocomplete="new-password" [(value)]="password" />
|
|
407
|
+
* <mf-password-strength for="pw" [value]="password()" [requirements]="rules" />`
|
|
408
|
+
*
|
|
409
|
+
* Kayıt / parola değiştirme formlarında parola alanının ALTINA kompoze edilen güç ölçer —
|
|
410
|
+
* `mf-character-count`'un `mfInput`'a göre durduğu yerde durur, değeri herhangi bir kaynaktan
|
|
411
|
+
* alabilir. Parolayı saklamaz, yaymaz: tek girdisi `value`.
|
|
412
|
+
*
|
|
413
|
+
* - Ölçüm: tahmin edilebilirlik (uzunluk ağırlıklı + tekrar/dizi/yaygın kelime/kişisel değer
|
|
414
|
+
* cezaları), "1 büyük harf + 1 rakam" sayımı DEĞİL — bkz. `estimatePasswordStrength`.
|
|
415
|
+
* Uygulama genelinde `MF_PASSWORD_STRENGTH_ESTIMATOR`, örnek bazında `[result]` ile değişir
|
|
416
|
+
* (ör. `zxcvbn-ts`, sunucu/HIBP sonucu).
|
|
417
|
+
* - Görsel: 4 segment + seviye etiketi + tek satır ipucu; isteğe bağlı gereksinim listesi.
|
|
418
|
+
* Renk tek gösterge değil — segment sayısı ve metin de aynı bilgiyi taşır (WCAG 1.4.1).
|
|
419
|
+
* - Erişilebilirlik: çubuk `role="meter"` (`aria-valuetext` = seviye adı). Görünen metin
|
|
420
|
+
* ekran okuyucudan gizli; yerine `for` ile bağlanan kontrolün `aria-describedby`'ına eklenen
|
|
421
|
+
* gizli bir özet okunur (yalnızca kendi id'sini ekler/çıkarır — `mf-field` ile çakışmaz).
|
|
422
|
+
* `aria-live` bölgesi her tuşta DEĞİL, yalnızca seviye değişince duyurur.
|
|
423
|
+
*
|
|
424
|
+
* Dışa açık sinyaller (`#strength="mfPasswordStrength"`): `score()`, `level()`, `satisfied()` —
|
|
425
|
+
* ör. gönder düğmesini `!strength.satisfied()` iken kilitlemek için.
|
|
426
|
+
*/
|
|
427
|
+
class PasswordStrength {
|
|
428
|
+
/** Ölçülecek parola. */
|
|
429
|
+
value = input.required(/* @ts-ignore */
|
|
430
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
431
|
+
/** Parola `<input>`'unun id'si (`mf-password-input [inputId]` / `mf-field for` ile aynı). */
|
|
432
|
+
for = input(/* @ts-ignore */
|
|
433
|
+
...(ngDevMode ? [undefined, { debugName: "for" }] : /* istanbul ignore next */ []));
|
|
434
|
+
size = input('md', /* @ts-ignore */
|
|
435
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
436
|
+
/** Kontrol listesi — boşsa çizilmez. Karşılanmayan varken skor en fazla "orta" gösterilir. */
|
|
437
|
+
requirements = input([], /* @ts-ignore */
|
|
438
|
+
...(ngDevMode ? [{ debugName: "requirements" }] : /* istanbul ignore next */ []));
|
|
439
|
+
/** Parolada geçmemesi gereken kişisel değerler (e-posta, ad…) — yerleşik tahminciye iletilir. */
|
|
440
|
+
userInputs = input([], /* @ts-ignore */
|
|
441
|
+
...(ngDevMode ? [{ debugName: "userInputs" }] : /* istanbul ignore next */ []));
|
|
442
|
+
/** Bunun altındaki uzunluk en fazla "zayıf" sayılır. */
|
|
443
|
+
minLength = input(8, /* @ts-ignore */
|
|
444
|
+
...(ngDevMode ? [{ debugName: "minLength" }] : /* istanbul ignore next */ []));
|
|
445
|
+
/** Kontrollü mod: verilirse tahminci ÇALIŞMAZ, bu sonuç gösterilir (async/sunucu tahmincileri). */
|
|
446
|
+
result = input(/* @ts-ignore */
|
|
447
|
+
...(ngDevMode ? [undefined, { debugName: "result" }] : /* istanbul ignore next */ []));
|
|
448
|
+
/** Örnek bazında metin override'ı — verilmeyen alanlar `provideMesgflowUi` etiketlerinden gelir. */
|
|
449
|
+
labels = input(/* @ts-ignore */
|
|
450
|
+
...(ngDevMode ? [undefined, { debugName: "labels" }] : /* istanbul ignore next */ []));
|
|
451
|
+
document = inject(DOCUMENT);
|
|
452
|
+
estimator = inject(MF_PASSWORD_STRENGTH_ESTIMATOR);
|
|
453
|
+
injectedLabels = inject(MF_PASSWORD_STRENGTH_LABELS);
|
|
454
|
+
text = computed(() => mergeLabels(this.injectedLabels(), this.labels()), /* @ts-ignore */
|
|
455
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
456
|
+
segments = SEGMENTS;
|
|
457
|
+
staggerMs = SEGMENT_STAGGER_MS;
|
|
458
|
+
descriptionId = nextId('mf-password-strength');
|
|
459
|
+
estimate = computed(() => {
|
|
460
|
+
const value = this.value();
|
|
461
|
+
if (!value)
|
|
462
|
+
return { score: 0 };
|
|
463
|
+
return (this.result() ??
|
|
464
|
+
this.estimator(value, { minLength: this.minLength(), userInputs: this.userInputs() }));
|
|
465
|
+
}, /* @ts-ignore */
|
|
466
|
+
...(ngDevMode ? [{ debugName: "estimate" }] : /* istanbul ignore next */ []));
|
|
467
|
+
checks = computed(() => {
|
|
468
|
+
const value = this.value();
|
|
469
|
+
const text = this.text();
|
|
470
|
+
return checkPasswordRequirements(value, this.requirements()).map(({ requirement, met }) => ({
|
|
471
|
+
met,
|
|
472
|
+
key: requirement.kind === 'custom' ? `custom:${requirement.id}` : requirement.kind,
|
|
473
|
+
label: requirement.kind === 'custom'
|
|
474
|
+
? requirement.label
|
|
475
|
+
: requirement.kind === 'minLength'
|
|
476
|
+
? text.requirements.minLength.replace('{count}', String(requirement.value))
|
|
477
|
+
: text.requirements[requirement.kind],
|
|
478
|
+
}));
|
|
479
|
+
}, /* @ts-ignore */
|
|
480
|
+
...(ngDevMode ? [{ debugName: "checks" }] : /* istanbul ignore next */ []));
|
|
481
|
+
/** Tüm gereksinimler karşılandı mı (liste boşsa `true`). */
|
|
482
|
+
satisfied = computed(() => this.checks().every((c) => c.met), /* @ts-ignore */
|
|
483
|
+
...(ngDevMode ? [{ debugName: "satisfied" }] : /* istanbul ignore next */ []));
|
|
484
|
+
/** Gösterilen skor (0–4) — karşılanmamış gereksinim varken en fazla 2. */
|
|
485
|
+
score = computed(() => {
|
|
486
|
+
if (!this.value())
|
|
487
|
+
return 0;
|
|
488
|
+
const raw = Math.min(4, Math.max(0, Math.round(this.estimate().score)));
|
|
489
|
+
return (this.satisfied() ? raw : Math.min(raw, UNSATISFIED_CAP));
|
|
490
|
+
}, /* @ts-ignore */
|
|
491
|
+
...(ngDevMode ? [{ debugName: "score" }] : /* istanbul ignore next */ []));
|
|
492
|
+
/** Görsel seviye — değer boşken `null`. */
|
|
493
|
+
level = computed(() => this.value() ? passwordStrengthLevel(this.score()) : null, /* @ts-ignore */
|
|
494
|
+
...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
|
|
495
|
+
filled = computed(() => (this.value() ? Math.max(1, this.score()) : 0), /* @ts-ignore */
|
|
496
|
+
...(ngDevMode ? [{ debugName: "filled" }] : /* istanbul ignore next */ []));
|
|
497
|
+
levelLabel = computed(() => {
|
|
498
|
+
const level = this.level();
|
|
499
|
+
return level ? this.text().levels[level] : '';
|
|
500
|
+
}, /* @ts-ignore */
|
|
501
|
+
...(ngDevMode ? [{ debugName: "levelLabel" }] : /* istanbul ignore next */ []));
|
|
502
|
+
hint = computed(() => {
|
|
503
|
+
if (!this.value())
|
|
504
|
+
return '';
|
|
505
|
+
const { hint, warning } = this.estimate();
|
|
506
|
+
if (hint)
|
|
507
|
+
return hint;
|
|
508
|
+
// Sınırlı skor (gereksinim eksik) "iyi" ipucunu gizlemesin diye ipucu ham sonuçtan gelir.
|
|
509
|
+
return warning ? this.text().warnings[warning] : '';
|
|
510
|
+
}, /* @ts-ignore */
|
|
511
|
+
...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
512
|
+
metCount = computed(() => this.checks().filter((c) => c.met).length, /* @ts-ignore */
|
|
513
|
+
...(ngDevMode ? [{ debugName: "metCount" }] : /* istanbul ignore next */ []));
|
|
514
|
+
requirementsSummary = computed(() => this.checks().length
|
|
515
|
+
? this.text()
|
|
516
|
+
.requirementsMet.replace('{met}', String(this.metCount()))
|
|
517
|
+
.replace('{total}', String(this.checks().length))
|
|
518
|
+
: '', /* @ts-ignore */
|
|
519
|
+
...(ngDevMode ? [{ debugName: "requirementsSummary" }] : /* istanbul ignore next */ []));
|
|
520
|
+
/** Kontrole odaklanınca okunan gizli özet. Değer boşken gereksinimleri sıralar. */
|
|
521
|
+
description = computed(() => {
|
|
522
|
+
const text = this.text();
|
|
523
|
+
if (!this.value()) {
|
|
524
|
+
const rules = this.checks().map((c) => c.label);
|
|
525
|
+
return rules.length ? `${text.requirementsLabel}: ${rules.join(', ')}.` : '';
|
|
526
|
+
}
|
|
527
|
+
return [`${text.ariaLabel}: ${this.levelLabel()}.`, this.hint(), this.requirementsSummary()]
|
|
528
|
+
.filter(Boolean)
|
|
529
|
+
.join(' ');
|
|
530
|
+
}, /* @ts-ignore */
|
|
531
|
+
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
532
|
+
/**
|
|
533
|
+
* Canlı bölge metni YALNIZCA seviyeye bağlı — aynı seviyede yazmaya devam etmek metni
|
|
534
|
+
* değiştirmediği için DOM'a yazılmaz, ekran okuyucu her tuşta konuşmaz.
|
|
535
|
+
*/
|
|
536
|
+
announcement = computed(() => {
|
|
537
|
+
const label = this.levelLabel();
|
|
538
|
+
return label ? `${this.text().ariaLabel}: ${label}` : '';
|
|
539
|
+
}, /* @ts-ignore */
|
|
540
|
+
...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
541
|
+
levelText = computed(() => {
|
|
542
|
+
const level = this.level();
|
|
543
|
+
return cn('inline-flex shrink-0 items-center gap-1 font-medium transition-colors duration-fast ease-emphasis', level ? TEXT_TONE[level] : 'text-fg-subtle');
|
|
544
|
+
}, /* @ts-ignore */
|
|
545
|
+
...(ngDevMode ? [{ debugName: "levelText" }] : /* istanbul ignore next */ []));
|
|
546
|
+
trackClasses = computed(() => cn(passwordStrengthTrackVariants({ size: this.size() })), /* @ts-ignore */
|
|
547
|
+
...(ngDevMode ? [{ debugName: "trackClasses" }] : /* istanbul ignore next */ []));
|
|
548
|
+
hostClasses = computed(() => cn('@container block w-full'), /* @ts-ignore */
|
|
549
|
+
...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
|
|
550
|
+
segmentFill(index) {
|
|
551
|
+
const level = this.level();
|
|
552
|
+
return cn('absolute inset-0 origin-[0_50%] rounded-full transition-[transform,background-color] duration-slow ease-emphasis rtl:origin-[100%_50%]', level ? SEGMENT_TONE[level] : 'bg-transparent', index < this.filled() ? 'scale-x-100' : 'scale-x-0');
|
|
553
|
+
}
|
|
554
|
+
constructor() {
|
|
555
|
+
afterRenderEffect((onCleanup) => {
|
|
556
|
+
const id = this.for();
|
|
557
|
+
if (!id)
|
|
558
|
+
return;
|
|
559
|
+
const control = this.document.getElementById(id);
|
|
560
|
+
if (!control)
|
|
561
|
+
return;
|
|
562
|
+
const own = this.descriptionId;
|
|
563
|
+
const ids = (control.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
|
|
564
|
+
if (!ids.includes(own))
|
|
565
|
+
control.setAttribute('aria-describedby', [...ids, own].join(' '));
|
|
566
|
+
onCleanup(() => {
|
|
567
|
+
const rest = (control.getAttribute('aria-describedby') ?? '')
|
|
568
|
+
.split(/\s+/)
|
|
569
|
+
.filter((x) => x && x !== own);
|
|
570
|
+
if (rest.length)
|
|
571
|
+
control.setAttribute('aria-describedby', rest.join(' '));
|
|
572
|
+
else
|
|
573
|
+
control.removeAttribute('aria-describedby');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: PasswordStrength, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
578
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: PasswordStrength, isStandalone: true, selector: "mf-password-strength", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, for: { classPropertyName: "for", publicName: "for", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, requirements: { classPropertyName: "requirements", publicName: "requirements", isSignal: true, isRequired: false, transformFunction: null }, userInputs: { classPropertyName: "userInputs", publicName: "userInputs", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, result: { classPropertyName: "result", publicName: "result", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()" } }, exportAs: ["mfPasswordStrength"], ngImport: i0, template: "<div\r\n role=\"meter\"\r\n aria-valuemin=\"0\"\r\n aria-valuemax=\"4\"\r\n [attr.aria-valuenow]=\"score()\"\r\n [attr.aria-valuetext]=\"levelLabel() || null\"\r\n [attr.aria-label]=\"text().ariaLabel\"\r\n [class]=\"trackClasses()\"\r\n>\r\n @for (segment of segments; track segment) {\r\n <span class=\"relative flex-1 overflow-hidden rounded-full bg-border\">\r\n <span [class]=\"segmentFill(segment)\" [style.transition-delay.ms]=\"segment * staggerMs\"></span>\r\n </span>\r\n }\r\n</div>\r\n\r\n<p class=\"m-0 mt-1.5 flex min-h-5 flex-wrap items-center gap-x-2 text-xs\" aria-hidden=\"true\">\r\n @if (level(); as current) {\r\n <span [class]=\"levelText()\">\r\n @if (current === 'strong') {\r\n <span mfIcon [size]=\"14\" [filled]=\"true\" class=\"animate-pop\">check_circle</span>\r\n }\r\n {{ levelLabel() }}\r\n </span>\r\n @if (hint()) {\r\n <span class=\"min-w-0 text-fg-subtle animate-fade-in\">{{ hint() }}</span>\r\n }\r\n }\r\n</p>\r\n\r\n@if (checks().length) {\r\n <ul\r\n class=\"m-0 mt-2 grid list-none gap-x-4 gap-y-1 p-0 @sm:grid-cols-2\"\r\n [attr.aria-label]=\"text().requirementsLabel\"\r\n >\r\n @for (check of checks(); track check.key) {\r\n <li\r\n class=\"flex items-center gap-1.5 text-xs transition-colors duration-fast ease-emphasis\"\r\n [class]=\"check.met ? 'text-fg' : 'text-fg-subtle'\"\r\n >\r\n @if (check.met) {\r\n <span mfIcon [size]=\"14\" [filled]=\"true\" class=\"text-success animate-pop\"\r\n >check_circle</span\r\n >\r\n } @else {\r\n <span mfIcon [size]=\"14\">radio_button_unchecked</span>\r\n }\r\n <span>{{ check.label }}</span>\r\n <span class=\"sr-only\"> ({{ check.met ? text().met : text().unmet }})</span>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n<span class=\"sr-only\" [id]=\"descriptionId\">{{ description() }}</span>\r\n<span class=\"sr-only\" aria-live=\"polite\">{{ announcement() }}</span>\r\n", dependencies: [{ kind: "directive", type: Icon, selector: "span[mfIcon]", inputs: ["size", "filled"] }] });
|
|
579
|
+
}
|
|
580
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: PasswordStrength, decorators: [{
|
|
581
|
+
type: Component,
|
|
582
|
+
args: [{ selector: 'mf-password-strength', exportAs: 'mfPasswordStrength', imports: [Icon], host: { '[class]': 'hostClasses()' }, template: "<div\r\n role=\"meter\"\r\n aria-valuemin=\"0\"\r\n aria-valuemax=\"4\"\r\n [attr.aria-valuenow]=\"score()\"\r\n [attr.aria-valuetext]=\"levelLabel() || null\"\r\n [attr.aria-label]=\"text().ariaLabel\"\r\n [class]=\"trackClasses()\"\r\n>\r\n @for (segment of segments; track segment) {\r\n <span class=\"relative flex-1 overflow-hidden rounded-full bg-border\">\r\n <span [class]=\"segmentFill(segment)\" [style.transition-delay.ms]=\"segment * staggerMs\"></span>\r\n </span>\r\n }\r\n</div>\r\n\r\n<p class=\"m-0 mt-1.5 flex min-h-5 flex-wrap items-center gap-x-2 text-xs\" aria-hidden=\"true\">\r\n @if (level(); as current) {\r\n <span [class]=\"levelText()\">\r\n @if (current === 'strong') {\r\n <span mfIcon [size]=\"14\" [filled]=\"true\" class=\"animate-pop\">check_circle</span>\r\n }\r\n {{ levelLabel() }}\r\n </span>\r\n @if (hint()) {\r\n <span class=\"min-w-0 text-fg-subtle animate-fade-in\">{{ hint() }}</span>\r\n }\r\n }\r\n</p>\r\n\r\n@if (checks().length) {\r\n <ul\r\n class=\"m-0 mt-2 grid list-none gap-x-4 gap-y-1 p-0 @sm:grid-cols-2\"\r\n [attr.aria-label]=\"text().requirementsLabel\"\r\n >\r\n @for (check of checks(); track check.key) {\r\n <li\r\n class=\"flex items-center gap-1.5 text-xs transition-colors duration-fast ease-emphasis\"\r\n [class]=\"check.met ? 'text-fg' : 'text-fg-subtle'\"\r\n >\r\n @if (check.met) {\r\n <span mfIcon [size]=\"14\" [filled]=\"true\" class=\"text-success animate-pop\"\r\n >check_circle</span\r\n >\r\n } @else {\r\n <span mfIcon [size]=\"14\">radio_button_unchecked</span>\r\n }\r\n <span>{{ check.label }}</span>\r\n <span class=\"sr-only\"> ({{ check.met ? text().met : text().unmet }})</span>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n<span class=\"sr-only\" [id]=\"descriptionId\">{{ description() }}</span>\r\n<span class=\"sr-only\" aria-live=\"polite\">{{ announcement() }}</span>\r\n" }]
|
|
583
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], requirements: [{ type: i0.Input, args: [{ isSignal: true, alias: "requirements", required: false }] }], userInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "userInputs", required: false }] }], minLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minLength", required: false }] }], result: [{ type: i0.Input, args: [{ isSignal: true, alias: "result", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }] } });
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Generated bundle index. Do not edit.
|
|
587
|
+
*/
|
|
588
|
+
|
|
589
|
+
export { MF_PASSWORD_STRENGTH_ESTIMATOR, PasswordStrength, checkPasswordRequirements, estimatePasswordStrength, meetsPasswordRequirement, passwordStrengthLevel, passwordStrengthTrackVariants };
|
|
590
|
+
//# sourceMappingURL=mesgflow-ui-kit-password-strength.mjs.map
|