secure-ui-components 0.2.3 → 0.2.4

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.
@@ -1,329 +1 @@
1
- import { SecureBaseComponent } from '../../core/base-component.js';
2
- export class SecurePasswordConfirm extends SecureBaseComponent {
3
- #passwordInput = null;
4
- #confirmInput = null;
5
- #passwordError = null;
6
- #confirmError = null;
7
- #matchIndicator = null;
8
- #passwordToggle = null;
9
- #confirmToggle = null;
10
- #passwordValue = '';
11
- #confirmValue = '';
12
- #confirmTouched = false;
13
- #passwordVisible = false;
14
- #confirmVisible = false;
15
- #hiddenInput = null;
16
- #instanceId = `secure-password-confirm-${Math.random().toString(36).substring(2, 11)}`;
17
- // Strip any pre-set security-tier before the base reads it,
18
- // guaranteeing this component is always CRITICAL.
19
- connectedCallback() {
20
- this.removeAttribute('security-tier');
21
- super.connectedCallback();
22
- }
23
- static get observedAttributes() {
24
- return [
25
- ...super.observedAttributes,
26
- 'name',
27
- 'label',
28
- 'password-label',
29
- 'confirm-label',
30
- 'required',
31
- 'minlength',
32
- ];
33
- }
34
- render() {
35
- const fragment = document.createDocumentFragment();
36
- const container = document.createElement('div');
37
- container.className = 'container';
38
- container.setAttribute('part', 'container');
39
- // Optional group label
40
- const groupLabelText = this.getAttribute('label');
41
- if (groupLabelText) {
42
- const groupLabel = document.createElement('div');
43
- groupLabel.className = 'group-label';
44
- groupLabel.textContent = this.sanitizeValue(groupLabelText);
45
- container.appendChild(groupLabel);
46
- }
47
- const isRequired = this.hasAttribute('required');
48
- // ── Password field ──────────────────────────────────────────────────────
49
- const passwordSection = this.#buildField({
50
- inputPart: 'password-input',
51
- wrapperPart: 'password-wrapper',
52
- labelPart: 'password-label',
53
- errorPart: 'password-error',
54
- togglePart: 'password-toggle',
55
- labelText: this.getAttribute('password-label') ?? 'New Password',
56
- inputId: `${this.#instanceId}-password`,
57
- errorId: `${this.#instanceId}-password-error`,
58
- isRequired,
59
- });
60
- this.#passwordInput = passwordSection.querySelector('[part="password-input"]');
61
- this.#passwordToggle = passwordSection.querySelector('[part="password-toggle"]');
62
- this.#passwordError = passwordSection.querySelector('[part="password-error"]');
63
- // ── Confirm field ───────────────────────────────────────────────────────
64
- const confirmSection = this.#buildField({
65
- inputPart: 'confirm-input',
66
- wrapperPart: 'confirm-wrapper',
67
- labelPart: 'confirm-label',
68
- errorPart: 'confirm-error',
69
- togglePart: 'confirm-toggle',
70
- labelText: this.getAttribute('confirm-label') ?? 'Confirm Password',
71
- inputId: `${this.#instanceId}-confirm`,
72
- errorId: `${this.#instanceId}-confirm-error`,
73
- isRequired,
74
- });
75
- this.#confirmInput = confirmSection.querySelector('[part="confirm-input"]');
76
- this.#confirmToggle = confirmSection.querySelector('[part="confirm-toggle"]');
77
- this.#confirmError = confirmSection.querySelector('[part="confirm-error"]');
78
- // ── Match indicator ─────────────────────────────────────────────────────
79
- this.#matchIndicator = document.createElement('div');
80
- this.#matchIndicator.setAttribute('part', 'match-indicator');
81
- this.#matchIndicator.className = 'match-indicator';
82
- this.#matchIndicator.setAttribute('aria-hidden', 'true');
83
- container.appendChild(passwordSection);
84
- container.appendChild(confirmSection);
85
- container.appendChild(this.#matchIndicator);
86
- this.#attachPasswordListeners();
87
- this.#attachConfirmListeners();
88
- this.#attachToggleListeners();
89
- this.#createHiddenInput();
90
- this.addComponentStyles(new URL('./secure-password-confirm.css', import.meta.url).href);
91
- fragment.appendChild(container);
92
- return fragment;
93
- }
94
- // ── DOM builders ──────────────────────────────────────────────────────────
95
- #buildField(opts) {
96
- const section = document.createElement('div');
97
- section.className = 'field-section';
98
- const label = document.createElement('label');
99
- label.setAttribute('part', opts.labelPart);
100
- label.htmlFor = opts.inputId;
101
- label.textContent = this.sanitizeValue(opts.labelText);
102
- const wrapper = document.createElement('div');
103
- wrapper.className = 'input-wrapper';
104
- wrapper.setAttribute('part', opts.wrapperPart);
105
- const input = document.createElement('input');
106
- input.id = opts.inputId;
107
- input.type = 'password';
108
- input.autocomplete = 'new-password';
109
- input.setAttribute('part', opts.inputPart);
110
- input.setAttribute('aria-describedby', opts.errorId);
111
- if (opts.isRequired) {
112
- input.required = true;
113
- input.setAttribute('aria-required', 'true');
114
- }
115
- const toggle = document.createElement('button');
116
- toggle.type = 'button';
117
- toggle.className = 'toggle-btn';
118
- toggle.setAttribute('part', opts.togglePart);
119
- toggle.setAttribute('aria-label', 'Show password');
120
- toggle.appendChild(this.#createEyeIcon());
121
- wrapper.appendChild(input);
122
- wrapper.appendChild(toggle);
123
- const error = document.createElement('div');
124
- error.id = opts.errorId;
125
- error.setAttribute('part', opts.errorPart);
126
- error.setAttribute('role', 'alert');
127
- error.className = 'error-container hidden';
128
- section.appendChild(label);
129
- section.appendChild(wrapper);
130
- section.appendChild(error);
131
- return section;
132
- }
133
- // ── Event wiring ──────────────────────────────────────────────────────────
134
- #attachPasswordListeners() {
135
- this.#passwordInput.addEventListener('focus', () => {
136
- this.recordTelemetryFocus();
137
- });
138
- this.#passwordInput.addEventListener('input', (e) => {
139
- this.recordTelemetryInput(e);
140
- this.#passwordValue = this.#passwordInput.value;
141
- this.detectInjection(this.#passwordValue, this.getAttribute('name') ?? '');
142
- if (this.#confirmTouched) {
143
- this.#checkMatch();
144
- }
145
- this.dispatchEvent(new CustomEvent('secure-input', {
146
- detail: { name: this.getAttribute('name') ?? '', field: 'password' },
147
- bubbles: true,
148
- composed: true,
149
- }));
150
- });
151
- this.#passwordInput.addEventListener('blur', () => {
152
- this.recordTelemetryBlur();
153
- this.#validateStrength();
154
- });
155
- }
156
- #attachConfirmListeners() {
157
- this.#confirmInput.addEventListener('input', () => {
158
- this.#confirmValue = this.#confirmInput.value;
159
- this.detectInjection(this.#confirmValue, this.getAttribute('name') ?? '');
160
- if (this.#confirmTouched) {
161
- this.#checkMatch();
162
- }
163
- });
164
- this.#confirmInput.addEventListener('blur', () => {
165
- this.#confirmTouched = true;
166
- this.#confirmValue = this.#confirmInput.value;
167
- this.#checkMatch();
168
- });
169
- }
170
- #attachToggleListeners() {
171
- this.#passwordToggle.addEventListener('click', () => {
172
- this.#passwordVisible = !this.#passwordVisible;
173
- this.#passwordInput.type = this.#passwordVisible ? 'text' : 'password';
174
- this.#passwordToggle.classList.toggle('is-visible', this.#passwordVisible);
175
- this.#passwordToggle.setAttribute('aria-label', this.#passwordVisible ? 'Hide password' : 'Show password');
176
- });
177
- this.#confirmToggle.addEventListener('click', () => {
178
- this.#confirmVisible = !this.#confirmVisible;
179
- this.#confirmInput.type = this.#confirmVisible ? 'text' : 'password';
180
- this.#confirmToggle.classList.toggle('is-visible', this.#confirmVisible);
181
- this.#confirmToggle.setAttribute('aria-label', this.#confirmVisible ? 'Hide confirm password' : 'Show confirm password');
182
- });
183
- }
184
- // ── SVG icon ──────────────────────────────────────────────────────────────
185
- #createEyeIcon() {
186
- const ns = 'http://www.w3.org/2000/svg';
187
- const svg = document.createElementNS(ns, 'svg');
188
- svg.setAttribute('viewBox', '0 0 24 24');
189
- svg.setAttribute('aria-hidden', 'true');
190
- svg.setAttribute('class', 'eye-icon');
191
- svg.setAttribute('width', '18');
192
- svg.setAttribute('height', '18');
193
- // Outer eye shape
194
- const outline = document.createElementNS(ns, 'path');
195
- outline.setAttribute('class', 'eye-outline');
196
- outline.setAttribute('d', 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z');
197
- // Pupil
198
- const pupil = document.createElementNS(ns, 'circle');
199
- pupil.setAttribute('class', 'eye-pupil');
200
- pupil.setAttribute('cx', '12');
201
- pupil.setAttribute('cy', '12');
202
- pupil.setAttribute('r', '3');
203
- // Slash — drawn in when password is visible (eye-off state)
204
- // Line (3,3)→(21,21): length = √(18²+18²) ≈ 25.46 → dasharray 26
205
- const slash = document.createElementNS(ns, 'line');
206
- slash.setAttribute('class', 'eye-slash');
207
- slash.setAttribute('x1', '3');
208
- slash.setAttribute('y1', '3');
209
- slash.setAttribute('x2', '21');
210
- slash.setAttribute('y2', '21');
211
- svg.append(outline, pupil, slash);
212
- return svg;
213
- }
214
- // ── Validation ────────────────────────────────────────────────────────────
215
- #validateStrength() {
216
- const err = this.#strengthError(this.#passwordValue);
217
- if (err) {
218
- this.#showError(this.#passwordError, this.#passwordInput, err);
219
- }
220
- else {
221
- this.#clearError(this.#passwordError, this.#passwordInput);
222
- }
223
- }
224
- #strengthError(value) {
225
- if (!value)
226
- return null;
227
- if (value.length < 8)
228
- return 'Password must be at least 8 characters';
229
- if (!/[a-z]/.test(value))
230
- return 'Password must include a lowercase letter';
231
- if (!/[A-Z]/.test(value))
232
- return 'Password must include an uppercase letter';
233
- if (!/[0-9]/.test(value))
234
- return 'Password must include a number';
235
- if (!/[^a-zA-Z0-9]/.test(value))
236
- return 'Password must include a special character';
237
- return null;
238
- }
239
- #checkMatch() {
240
- const matched = this.#passwordValue.length > 0 && this.#passwordValue === this.#confirmValue;
241
- this.#syncHiddenInput(matched);
242
- this.#updateMatchIndicator(matched);
243
- if (matched) {
244
- this.#clearError(this.#confirmError, this.#confirmInput);
245
- this.dispatchEvent(new CustomEvent('secure-password-match', {
246
- detail: { name: this.getAttribute('name') ?? '', matched: true },
247
- bubbles: true,
248
- composed: true,
249
- }));
250
- }
251
- else {
252
- const msg = this.#confirmValue.length > 0
253
- ? 'Passwords do not match'
254
- : 'Please confirm your password';
255
- this.#showError(this.#confirmError, this.#confirmInput, msg);
256
- this.dispatchEvent(new CustomEvent('secure-password-mismatch', {
257
- detail: { name: this.getAttribute('name') ?? '', matched: false },
258
- bubbles: true,
259
- composed: true,
260
- }));
261
- }
262
- }
263
- // ── UI helpers ────────────────────────────────────────────────────────────
264
- #showError(container, input, message) {
265
- container.textContent = message;
266
- container.classList.remove('hidden');
267
- input.setAttribute('aria-invalid', 'true');
268
- }
269
- #clearError(container, input) {
270
- container.classList.add('hidden');
271
- container.textContent = '';
272
- input.removeAttribute('aria-invalid');
273
- }
274
- #updateMatchIndicator(matched) {
275
- if (!this.#matchIndicator)
276
- return;
277
- this.#matchIndicator.className = `match-indicator ${matched ? 'matched' : 'mismatched'}`;
278
- this.#matchIndicator.textContent = matched ? '✓ Passwords match' : '✗ Passwords do not match';
279
- }
280
- // ── Form participation ────────────────────────────────────────────────────
281
- #createHiddenInput() {
282
- const name = this.getAttribute('name');
283
- if (!name || this.closest('secure-form'))
284
- return;
285
- this.#hiddenInput = document.createElement('input');
286
- this.#hiddenInput.type = 'hidden';
287
- this.#hiddenInput.name = name;
288
- this.#hiddenInput.value = '';
289
- this.appendChild(this.#hiddenInput);
290
- }
291
- #syncHiddenInput(matched) {
292
- if (!this.#hiddenInput)
293
- return;
294
- this.#hiddenInput.value = matched ? this.#passwordValue : '';
295
- }
296
- // ── Public API ────────────────────────────────────────────────────────────
297
- getPasswordValue() {
298
- if (!this.#passwordValue || !this.#confirmValue)
299
- return null;
300
- if (this.#passwordValue !== this.#confirmValue)
301
- return null;
302
- return this.#passwordValue;
303
- }
304
- get valid() {
305
- if (!this.#passwordValue || !this.#confirmValue)
306
- return false;
307
- if (this.#passwordValue !== this.#confirmValue)
308
- return false;
309
- return this.#strengthError(this.#passwordValue) === null;
310
- }
311
- get name() {
312
- return this.getAttribute('name') ?? '';
313
- }
314
- // ── Lifecycle ─────────────────────────────────────────────────────────────
315
- disconnectedCallback() {
316
- super.disconnectedCallback();
317
- this.#passwordValue = '';
318
- this.#confirmValue = '';
319
- if (this.#passwordInput)
320
- this.#passwordInput.value = '';
321
- if (this.#confirmInput)
322
- this.#confirmInput.value = '';
323
- if (this.#hiddenInput)
324
- this.#hiddenInput.value = '';
325
- }
326
- }
327
- customElements.define('secure-password-confirm', SecurePasswordConfirm);
328
- export default SecurePasswordConfirm;
329
- //# sourceMappingURL=secure-password-confirm.js.map
1
+ import{SecureBaseComponent as l}from"../../core/base-component.js";class o extends l{#e=null;#s=null;#c=null;#h=null;#a=null;#l=null;#u=null;#t="";#r="";#p=!1;#n=!1;#o=!1;#i=null;#d=`secure-password-confirm-${Math.random().toString(36).substring(2,11)}`;connectedCallback(){this.removeAttribute("security-tier"),super.connectedCallback()}static get observedAttributes(){return[...super.observedAttributes,"name","label","password-label","confirm-label","required","minlength"]}render(){const t=document.createDocumentFragment(),e=document.createElement("div");e.className="container",e.setAttribute("part","container");const i=this.getAttribute("label");if(i){const n=document.createElement("div");n.className="group-label",n.textContent=this.sanitizeValue(i),e.appendChild(n)}const r=this.hasAttribute("required"),s=this.#b({inputPart:"password-input",wrapperPart:"password-wrapper",labelPart:"password-label",errorPart:"password-error",togglePart:"password-toggle",labelText:this.getAttribute("password-label")??"New Password",inputId:`${this.#d}-password`,errorId:`${this.#d}-password-error`,isRequired:r});this.#e=s.querySelector('[part="password-input"]'),this.#l=s.querySelector('[part="password-toggle"]'),this.#c=s.querySelector('[part="password-error"]');const a=this.#b({inputPart:"confirm-input",wrapperPart:"confirm-wrapper",labelPart:"confirm-label",errorPart:"confirm-error",togglePart:"confirm-toggle",labelText:this.getAttribute("confirm-label")??"Confirm Password",inputId:`${this.#d}-confirm`,errorId:`${this.#d}-confirm-error`,isRequired:r});return this.#s=a.querySelector('[part="confirm-input"]'),this.#u=a.querySelector('[part="confirm-toggle"]'),this.#h=a.querySelector('[part="confirm-error"]'),this.#a=document.createElement("div"),this.#a.setAttribute("part","match-indicator"),this.#a.className="match-indicator",this.#a.setAttribute("aria-hidden","true"),e.appendChild(s),e.appendChild(a),e.appendChild(this.#a),this.#A(),this.#v(),this.#E(),this.#I(),this.addComponentStyles(new URL("./secure-password-confirm.css",import.meta.url).href),t.appendChild(e),t}#b(t){const e=document.createElement("div");e.className="field-section";const i=document.createElement("label");i.setAttribute("part",t.labelPart),i.htmlFor=t.inputId,i.textContent=this.sanitizeValue(t.labelText);const r=document.createElement("div");r.className="input-wrapper",r.setAttribute("part",t.wrapperPart);const s=document.createElement("input");s.id=t.inputId,s.type="password",s.autocomplete="new-password",s.setAttribute("part",t.inputPart),s.setAttribute("aria-describedby",t.errorId),t.isRequired&&(s.required=!0,s.setAttribute("aria-required","true"));const a=document.createElement("button");a.type="button",a.className="toggle-btn",a.setAttribute("part",t.togglePart),a.setAttribute("aria-label","Show password"),a.appendChild(this.#C()),r.appendChild(s),r.appendChild(a);const n=document.createElement("div");return n.id=t.errorId,n.setAttribute("part",t.errorPart),n.setAttribute("role","alert"),n.className="error-container hidden",e.appendChild(i),e.appendChild(r),e.appendChild(n),e}#A(){this.#e.addEventListener("focus",()=>{this.recordTelemetryFocus()}),this.#e.addEventListener("input",t=>{this.recordTelemetryInput(t),this.#t=this.#e.value,this.detectInjection(this.#t,this.getAttribute("name")??""),this.#p&&this.#m(),this.dispatchEvent(new CustomEvent("secure-input",{detail:{name:this.getAttribute("name")??"",field:"password"},bubbles:!0,composed:!0}))}),this.#e.addEventListener("blur",()=>{this.recordTelemetryBlur(),this.#P()})}#v(){this.#s.addEventListener("input",()=>{this.#r=this.#s.value,this.detectInjection(this.#r,this.getAttribute("name")??""),this.#p&&this.#m()}),this.#s.addEventListener("blur",()=>{this.#p=!0,this.#r=this.#s.value,this.#m()})}#E(){this.#l.addEventListener("click",()=>{this.#n=!this.#n,this.#e.type=this.#n?"text":"password",this.#l.classList.toggle("is-visible",this.#n),this.#l.setAttribute("aria-label",this.#n?"Hide password":"Show password")}),this.#u.addEventListener("click",()=>{this.#o=!this.#o,this.#s.type=this.#o?"text":"password",this.#u.classList.toggle("is-visible",this.#o),this.#u.setAttribute("aria-label",this.#o?"Hide confirm password":"Show confirm password")})}#C(){const t="http://www.w3.org/2000/svg",e=document.createElementNS(t,"svg");e.setAttribute("viewBox","0 0 24 24"),e.setAttribute("aria-hidden","true"),e.setAttribute("class","eye-icon"),e.setAttribute("width","18"),e.setAttribute("height","18");const i=document.createElementNS(t,"path");i.setAttribute("class","eye-outline"),i.setAttribute("d","M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z");const r=document.createElementNS(t,"circle");r.setAttribute("class","eye-pupil"),r.setAttribute("cx","12"),r.setAttribute("cy","12"),r.setAttribute("r","3");const s=document.createElementNS(t,"line");return s.setAttribute("class","eye-slash"),s.setAttribute("x1","3"),s.setAttribute("y1","3"),s.setAttribute("x2","21"),s.setAttribute("y2","21"),e.append(i,r,s),e}#P(){const t=this.#w(this.#t);t?this.#f(this.#c,this.#e,t):this.#g(this.#c,this.#e)}#w(t){return t?t.length<8?"Password must be at least 8 characters":/[a-z]/.test(t)?/[A-Z]/.test(t)?/[0-9]/.test(t)?/[^a-zA-Z0-9]/.test(t)?null:"Password must include a special character":"Password must include a number":"Password must include an uppercase letter":"Password must include a lowercase letter":null}#m(){const t=this.#t.length>0&&this.#t===this.#r;if(this.#S(t),this.#y(t),t)this.#g(this.#h,this.#s),this.dispatchEvent(new CustomEvent("secure-password-match",{detail:{name:this.getAttribute("name")??"",matched:!0},bubbles:!0,composed:!0}));else{const e=this.#r.length>0?"Passwords do not match":"Please confirm your password";this.#f(this.#h,this.#s,e),this.dispatchEvent(new CustomEvent("secure-password-mismatch",{detail:{name:this.getAttribute("name")??"",matched:!1},bubbles:!0,composed:!0}))}}#f(t,e,i){t.textContent=i,t.classList.remove("hidden"),e.setAttribute("aria-invalid","true")}#g(t,e){t.classList.add("hidden"),t.textContent="",e.removeAttribute("aria-invalid")}#y(t){this.#a&&(this.#a.className=`match-indicator ${t?"matched":"mismatched"}`,this.#a.textContent=t?"\u2713 Passwords match":"\u2717 Passwords do not match")}#I(){const t=this.getAttribute("name");!t||this.closest("secure-form")||(this.#i=document.createElement("input"),this.#i.type="hidden",this.#i.name=t,this.#i.value="",this.appendChild(this.#i))}#S(t){this.#i&&(this.#i.value=t?this.#t:"")}getPasswordValue(){return!this.#t||!this.#r||this.#t!==this.#r?null:this.#t}get valid(){return!this.#t||!this.#r||this.#t!==this.#r?!1:this.#w(this.#t)===null}get name(){return this.getAttribute("name")??""}disconnectedCallback(){super.disconnectedCallback(),this.#t="",this.#r="",this.#e&&(this.#e.value=""),this.#s&&(this.#s.value=""),this.#i&&(this.#i.value="")}}customElements.define("secure-password-confirm",o);var c=o;export{o as SecurePasswordConfirm,c as default};