@birdapi/velinstyle 1.2.0 → 1.2.2
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.de.md +150 -24
- package/README.md +155 -33
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
- package/cli/atelier-catalog.json +1267 -0
- package/cli/atelier-formats.js +159 -0
- package/cli/atelier.js +382 -0
- package/cli/blueprints/empty-state.html +3 -5
- package/cli/cli-manifest.json +23 -10
- package/cli/docgen/extract-attributes.js +2 -0
- package/cli/docgen/extract-cli.js +1 -1
- package/cli/index.js +181 -6
- package/cli/production/component-graph.json +166 -0
- package/cli/production/explain.js +52 -0
- package/cli/production/extract.js +238 -0
- package/cli/production/graph.js +102 -0
- package/cli/production/report.js +78 -0
- package/cli/production/run.js +312 -0
- package/cli/production/trim-css.js +177 -0
- package/cli/production/trim-fonts.js +23 -0
- package/cli/production/trim-icons.js +38 -0
- package/cli/production/trim-js.js +48 -0
- package/cli/production/trim-motion.js +22 -0
- package/cli/production/trim-themes.js +50 -0
- package/cli/production/watch.js +38 -0
- package/cli/review.js +48 -1
- package/cli/scripts/write-atelier-fixtures.mjs +33 -0
- package/cli/transparency.js +221 -0
- package/components/index.js +3 -0
- package/components/runtime/component-loaders.js +3 -0
- package/components/velin-drawer.js +43 -4
- package/components/velin-empty-state.js +83 -0
- package/components/velin-modal.js +76 -15
- package/components/velin-otp-input.js +220 -0
- package/components/velin-password-strength.js +147 -0
- package/components/velin-sheet.js +49 -4
- package/core/a11y/component-contracts.json +23 -4
- package/core/attributes/registry.js +14 -0
- package/core/meta/knowledge/components.json +75 -3
- package/core/meta/schema.js +10 -0
- package/core/transparency/attach.js +74 -0
- package/core/transparency/claims.js +97 -0
- package/core/transparency/doctor.js +142 -0
- package/core/transparency/engine.js +75 -0
- package/core/transparency/export.js +98 -0
- package/core/transparency/index.js +30 -0
- package/core/transparency/migrate.js +130 -0
- package/core/transparency/normalize.js +125 -0
- package/core/transparency/policy.js +105 -0
- package/core/transparency/providers.js +235 -0
- package/core/transparency/registry.js +104 -0
- package/core/transparency/renderer.js +111 -0
- package/core/transparency/reporter.js +113 -0
- package/core/transparency/validator.js +112 -0
- package/dist/chunks/attach-H2ZSEAE6.js +365 -0
- package/dist/chunks/attributes-2ORR27KA.js +404 -0
- package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
- package/dist/chunks/chunk-CQMTCEI6.js +113 -0
- package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
- package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
- package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
- package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
- package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
- package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
- package/dist/llms.txt +5 -4
- package/dist/search-index.json +94 -5
- package/dist/velin-agent.json +290 -23
- package/dist/velinstyle-components.iife.js +3189 -2202
- package/dist/velinstyle-components.js +3187 -2196
- package/dist/velinstyle-components.min.js +261 -119
- package/dist/velinstyle.css +214 -6
- package/dist/velinstyle.d.ts +3 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +143 -142
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/src/components/data-table.css +19 -0
- package/src/components/empty-auth.css +33 -0
- package/src/components/table.css +16 -6
- package/src/components/transparency.css +138 -0
- package/src/velinstyle.css +2 -0
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { trapFocus, saveFocus, restoreFocus, getFocusableElements, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
|
|
2
|
-
import { escapeHTML } from './sanitize.js';
|
|
3
2
|
|
|
4
3
|
const styles = `
|
|
5
4
|
:host {
|
|
6
5
|
display: contents;
|
|
7
6
|
}
|
|
7
|
+
.sr-only {
|
|
8
|
+
position: absolute;
|
|
9
|
+
inline-size: 1px;
|
|
10
|
+
block-size: 1px;
|
|
11
|
+
padding: 0;
|
|
12
|
+
margin: -1px;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
clip: rect(0, 0, 0, 0);
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
border: 0;
|
|
17
|
+
}
|
|
8
18
|
.overlay {
|
|
9
19
|
position: fixed;
|
|
10
20
|
inset: 0;
|
|
@@ -87,7 +97,7 @@ const styles = `
|
|
|
87
97
|
|
|
88
98
|
class VelinModal extends HTMLElement {
|
|
89
99
|
static get observedAttributes() {
|
|
90
|
-
return ['open'];
|
|
100
|
+
return ['open', 'title'];
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
constructor() {
|
|
@@ -95,20 +105,20 @@ class VelinModal extends HTMLElement {
|
|
|
95
105
|
this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
96
106
|
this._previouslyFocused = null;
|
|
97
107
|
this._onKeydown = this._onKeydown.bind(this);
|
|
108
|
+
this._onTitleSlot = this._onTitleSlot.bind(this);
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
connectedCallback() {
|
|
101
|
-
|
|
102
|
-
const safeTitle = escapeHTML(title);
|
|
103
|
-
const dialogLabel = title
|
|
104
|
-
? 'aria-labelledby="velin-modal-title"'
|
|
105
|
-
: `aria-label="${escapeHTML(this.getAttribute('aria-label') || 'Dialog')}"`;
|
|
112
|
+
if (this.shadowRoot.querySelector('.dialog')) return;
|
|
106
113
|
this.shadowRoot.innerHTML = `
|
|
107
114
|
<style>${styles}</style>
|
|
108
115
|
<div class="overlay" part="overlay">
|
|
109
|
-
<div class="dialog" role="dialog" aria-modal="true"
|
|
116
|
+
<div class="dialog" role="dialog" aria-modal="true" part="dialog">
|
|
110
117
|
<div class="header" part="header">
|
|
111
|
-
<h2 class="title" id="velin-modal-title"
|
|
118
|
+
<h2 class="title" id="velin-modal-title" part="title">
|
|
119
|
+
<slot name="title"></slot>
|
|
120
|
+
<span class="title-fallback"></span>
|
|
121
|
+
</h2>
|
|
112
122
|
<button class="close-btn" aria-label="Close" part="close">×</button>
|
|
113
123
|
</div>
|
|
114
124
|
<div class="body" part="body"><slot></slot></div>
|
|
@@ -121,16 +131,16 @@ class VelinModal extends HTMLElement {
|
|
|
121
131
|
this.shadowRoot.querySelector('.overlay').addEventListener('click', (e) => {
|
|
122
132
|
if (e.target === e.currentTarget) this.close();
|
|
123
133
|
});
|
|
134
|
+
this.shadowRoot.querySelector('slot[name="title"]').addEventListener('slotchange', this._onTitleSlot);
|
|
135
|
+
this._syncTitle();
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
attributeChangedCallback(name
|
|
138
|
+
attributeChangedCallback(name) {
|
|
127
139
|
if (name === 'open') {
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
} else {
|
|
131
|
-
this._close();
|
|
132
|
-
}
|
|
140
|
+
if (this.hasAttribute('open')) this._open();
|
|
141
|
+
else this._close();
|
|
133
142
|
}
|
|
143
|
+
if (name === 'title') this._syncTitle();
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
open() {
|
|
@@ -142,6 +152,57 @@ class VelinModal extends HTMLElement {
|
|
|
142
152
|
this.dispatchEvent(new CustomEvent('velin-close', { bubbles: true }));
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
_syncTitle() {
|
|
156
|
+
const fallback = this.shadowRoot?.querySelector('.title-fallback');
|
|
157
|
+
const heading = this.shadowRoot?.querySelector('.title');
|
|
158
|
+
const dialog = this.shadowRoot?.querySelector('[role="dialog"]');
|
|
159
|
+
if (!fallback || !heading || !dialog) return;
|
|
160
|
+
|
|
161
|
+
if (this._hasTitleSlot()) {
|
|
162
|
+
fallback.hidden = true;
|
|
163
|
+
heading.classList.remove('sr-only');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fallback.hidden = false;
|
|
168
|
+
const title = (this.getAttribute('title') || '').trim();
|
|
169
|
+
fallback.textContent = title || 'Dialog';
|
|
170
|
+
heading.classList.toggle('sr-only', !title);
|
|
171
|
+
if (title) {
|
|
172
|
+
dialog.setAttribute('aria-labelledby', 'velin-modal-title');
|
|
173
|
+
dialog.removeAttribute('aria-label');
|
|
174
|
+
} else {
|
|
175
|
+
dialog.removeAttribute('aria-labelledby');
|
|
176
|
+
dialog.setAttribute('aria-label', this.getAttribute('aria-label') || 'Dialog');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_hasTitleSlot() {
|
|
181
|
+
const slot = this.shadowRoot?.querySelector('slot[name="title"]');
|
|
182
|
+
if (!slot) return false;
|
|
183
|
+
return slot.assignedNodes({ flatten: true }).some((n) => {
|
|
184
|
+
if (n.nodeType === Node.TEXT_NODE) return Boolean(n.textContent.trim());
|
|
185
|
+
return n.nodeType === Node.ELEMENT_NODE;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_onTitleSlot() {
|
|
190
|
+
const slot = this.shadowRoot?.querySelector('slot[name="title"]');
|
|
191
|
+
const dialog = this.shadowRoot?.querySelector('[role="dialog"]');
|
|
192
|
+
const heading = this.shadowRoot?.querySelector('.title');
|
|
193
|
+
const fallback = this.shadowRoot?.querySelector('.title-fallback');
|
|
194
|
+
if (!slot || !dialog || !heading || !fallback) return;
|
|
195
|
+
if (this._hasTitleSlot()) {
|
|
196
|
+
fallback.hidden = true;
|
|
197
|
+
heading.classList.remove('sr-only');
|
|
198
|
+
dialog.removeAttribute('aria-labelledby');
|
|
199
|
+
const label = slot.assignedNodes({ flatten: true }).map((n) => n.textContent || '').join(' ').trim();
|
|
200
|
+
if (label) dialog.setAttribute('aria-label', label);
|
|
201
|
+
} else {
|
|
202
|
+
this._syncTitle();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
145
206
|
_open() {
|
|
146
207
|
this._previouslyFocused = saveFocus();
|
|
147
208
|
setBackgroundInert(this);
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { escapeHTML } from './sanitize.js';
|
|
2
|
+
import { SHADOW_A11Y_STYLES } from './shadow-a11y-styles.js';
|
|
3
|
+
|
|
4
|
+
const styles = `
|
|
5
|
+
${SHADOW_A11Y_STYLES}
|
|
6
|
+
:host { display: block; }
|
|
7
|
+
.group {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
gap: var(--velin-space-2, 0.5rem);
|
|
11
|
+
}
|
|
12
|
+
input {
|
|
13
|
+
inline-size: 2.75rem;
|
|
14
|
+
block-size: 2.75rem;
|
|
15
|
+
text-align: center;
|
|
16
|
+
font: inherit;
|
|
17
|
+
font-size: var(--velin-text-lg, 1.25rem);
|
|
18
|
+
font-weight: var(--velin-weight-semibold, 600);
|
|
19
|
+
color: var(--velin-color-text, #111);
|
|
20
|
+
background: var(--velin-color-surface-bright, #fff);
|
|
21
|
+
border: 1px solid var(--velin-color-border, #ddd);
|
|
22
|
+
border-radius: var(--velin-radius-md, 0.5rem);
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
input:focus-visible {
|
|
26
|
+
outline: 3px solid var(--velin-color-focus, #2563eb);
|
|
27
|
+
outline-offset: 2px;
|
|
28
|
+
}
|
|
29
|
+
input:disabled {
|
|
30
|
+
opacity: 0.55;
|
|
31
|
+
cursor: not-allowed;
|
|
32
|
+
}
|
|
33
|
+
@media (forced-colors: active) {
|
|
34
|
+
input { border: 1px solid ButtonText; }
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
function digitsOnly(value) {
|
|
39
|
+
return String(value || '').replace(/\D/g, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class VelinOtpInput extends HTMLElement {
|
|
43
|
+
static get observedAttributes() {
|
|
44
|
+
return ['length', 'value', 'disabled', 'name', 'autocomplete', 'label'];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
super();
|
|
49
|
+
this.attachShadow({ mode: 'open' });
|
|
50
|
+
this._onInput = this._onInput.bind(this);
|
|
51
|
+
this._onKeydown = this._onKeydown.bind(this);
|
|
52
|
+
this._onPaste = this._onPaste.bind(this);
|
|
53
|
+
this._onFocus = this._onFocus.bind(this);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
connectedCallback() {
|
|
57
|
+
if (!this.shadowRoot.querySelector('.group')) this._render();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
61
|
+
if (oldVal === newVal) return;
|
|
62
|
+
if (!this.shadowRoot.querySelector('.group')) return;
|
|
63
|
+
if (name === 'length') this._render();
|
|
64
|
+
else if (name === 'value') this._applyValue(this.getAttribute('value') || '', false);
|
|
65
|
+
else if (name === 'disabled') this._syncDisabled();
|
|
66
|
+
else if (name === 'label' || name === 'autocomplete' || name === 'name') this._syncMeta();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get value() {
|
|
70
|
+
return this._readValue();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
set value(v) {
|
|
74
|
+
this.setAttribute('value', digitsOnly(v).slice(0, this._length()));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
focus() {
|
|
78
|
+
const inputs = this._inputs();
|
|
79
|
+
const firstEmpty = inputs.find((i) => !i.value) || inputs[0];
|
|
80
|
+
firstEmpty?.focus();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_length() {
|
|
84
|
+
const n = parseInt(this.getAttribute('length') || '6', 10);
|
|
85
|
+
return Number.isFinite(n) && n > 0 ? Math.min(n, 12) : 6;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_inputs() {
|
|
89
|
+
return [...(this.shadowRoot?.querySelectorAll('input') || [])];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_readValue() {
|
|
93
|
+
return this._inputs().map((i) => i.value).join('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_render() {
|
|
97
|
+
const len = this._length();
|
|
98
|
+
const label = escapeHTML(this.getAttribute('label') || 'One-time password');
|
|
99
|
+
this.shadowRoot.innerHTML = `
|
|
100
|
+
<style>${styles}</style>
|
|
101
|
+
<div class="group" role="group" aria-label="${label}" part="group"></div>
|
|
102
|
+
`;
|
|
103
|
+
const group = this.shadowRoot.querySelector('.group');
|
|
104
|
+
for (let i = 0; i < len; i++) {
|
|
105
|
+
const input = document.createElement('input');
|
|
106
|
+
input.type = 'text';
|
|
107
|
+
input.inputMode = 'numeric';
|
|
108
|
+
input.pattern = '[0-9]*';
|
|
109
|
+
input.maxLength = 1;
|
|
110
|
+
input.setAttribute('aria-label', `Digit ${i + 1} of ${len}`);
|
|
111
|
+
input.dataset.index = String(i);
|
|
112
|
+
input.part = 'digit';
|
|
113
|
+
input.addEventListener('input', this._onInput);
|
|
114
|
+
input.addEventListener('keydown', this._onKeydown);
|
|
115
|
+
input.addEventListener('paste', this._onPaste);
|
|
116
|
+
input.addEventListener('focus', this._onFocus);
|
|
117
|
+
group.appendChild(input);
|
|
118
|
+
}
|
|
119
|
+
this._syncMeta();
|
|
120
|
+
this._syncDisabled();
|
|
121
|
+
this._applyValue(this.getAttribute('value') || '', false);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_syncMeta() {
|
|
125
|
+
const group = this.shadowRoot?.querySelector('.group');
|
|
126
|
+
if (!group) return;
|
|
127
|
+
group.setAttribute('aria-label', this.getAttribute('label') || 'One-time password');
|
|
128
|
+
const autocomplete = this.getAttribute('autocomplete') || 'one-time-code';
|
|
129
|
+
const name = this.getAttribute('name') || '';
|
|
130
|
+
this._inputs().forEach((input, i) => {
|
|
131
|
+
input.autocomplete = i === 0 ? autocomplete : 'off';
|
|
132
|
+
input.name = name ? `${name}-${i + 1}` : '';
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_syncDisabled() {
|
|
137
|
+
const disabled = this.hasAttribute('disabled');
|
|
138
|
+
this._inputs().forEach((input) => {
|
|
139
|
+
input.disabled = disabled;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_applyValue(raw, emit) {
|
|
144
|
+
const digits = digitsOnly(raw).slice(0, this._length()).split('');
|
|
145
|
+
const inputs = this._inputs();
|
|
146
|
+
inputs.forEach((input, i) => {
|
|
147
|
+
input.value = digits[i] || '';
|
|
148
|
+
});
|
|
149
|
+
if (emit) this._emit();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_emit() {
|
|
153
|
+
const value = this._readValue();
|
|
154
|
+
if (this.getAttribute('value') !== value) {
|
|
155
|
+
this.setAttribute('value', value);
|
|
156
|
+
}
|
|
157
|
+
this.dispatchEvent(new CustomEvent('velin-change', {
|
|
158
|
+
bubbles: true,
|
|
159
|
+
composed: true,
|
|
160
|
+
detail: { value },
|
|
161
|
+
}));
|
|
162
|
+
if (value.length === this._length()) {
|
|
163
|
+
this.dispatchEvent(new CustomEvent('velin-complete', {
|
|
164
|
+
bubbles: true,
|
|
165
|
+
composed: true,
|
|
166
|
+
detail: { value },
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_onInput(e) {
|
|
172
|
+
const input = e.target;
|
|
173
|
+
const cleaned = digitsOnly(input.value).slice(-1);
|
|
174
|
+
input.value = cleaned;
|
|
175
|
+
const index = Number(input.dataset.index);
|
|
176
|
+
const inputs = this._inputs();
|
|
177
|
+
if (cleaned && index < inputs.length - 1) inputs[index + 1].focus();
|
|
178
|
+
this._emit();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_onKeydown(e) {
|
|
182
|
+
const input = e.target;
|
|
183
|
+
const index = Number(input.dataset.index);
|
|
184
|
+
const inputs = this._inputs();
|
|
185
|
+
if (e.key === 'Backspace' && !input.value && index > 0) {
|
|
186
|
+
inputs[index - 1].focus();
|
|
187
|
+
inputs[index - 1].value = '';
|
|
188
|
+
this._emit();
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
} else if (e.key === 'ArrowLeft' && index > 0) {
|
|
191
|
+
inputs[index - 1].focus();
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
} else if (e.key === 'ArrowRight' && index < inputs.length - 1) {
|
|
194
|
+
inputs[index + 1].focus();
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_onPaste(e) {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
const text = digitsOnly(e.clipboardData?.getData('text') || '');
|
|
202
|
+
if (!text) return;
|
|
203
|
+
const index = Number(e.target.dataset.index);
|
|
204
|
+
const inputs = this._inputs();
|
|
205
|
+
const chars = text.split('');
|
|
206
|
+
for (let i = 0; i < chars.length && index + i < inputs.length; i++) {
|
|
207
|
+
inputs[index + i].value = chars[i];
|
|
208
|
+
}
|
|
209
|
+
const next = Math.min(index + chars.length, inputs.length - 1);
|
|
210
|
+
inputs[next]?.focus();
|
|
211
|
+
this._emit();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_onFocus(e) {
|
|
215
|
+
e.target.select?.();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
customElements.define('velin-otp-input', VelinOtpInput);
|
|
220
|
+
export default VelinOtpInput;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { SHADOW_A11Y_STYLES } from './shadow-a11y-styles.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Heuristic password strength meter (same scoring as Atelier showcases).
|
|
5
|
+
* Not a substitute for server-side policy or entropy libraries.
|
|
6
|
+
*/
|
|
7
|
+
export function scorePassword(pw) {
|
|
8
|
+
const value = String(pw || '');
|
|
9
|
+
let score = 0;
|
|
10
|
+
if (value.length >= 8) score += 1;
|
|
11
|
+
if (value.length >= 12) score += 1;
|
|
12
|
+
if (/[A-Z]/.test(value) && /[a-z]/.test(value)) score += 1;
|
|
13
|
+
if (/\d/.test(value)) score += 1;
|
|
14
|
+
if (/[^A-Za-z0-9]/.test(value)) score += 1;
|
|
15
|
+
const labels = ['Too weak', 'Weak', 'Okay', 'Strong', 'Excellent', 'Excellent'];
|
|
16
|
+
return {
|
|
17
|
+
score,
|
|
18
|
+
label: labels[score] || 'Too weak',
|
|
19
|
+
pct: Math.min(100, score * 20),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const styles = `
|
|
24
|
+
${SHADOW_A11Y_STYLES}
|
|
25
|
+
:host { display: block; }
|
|
26
|
+
.wrap { display: grid; gap: var(--velin-space-2, 0.5rem); }
|
|
27
|
+
.meter {
|
|
28
|
+
inline-size: 100%;
|
|
29
|
+
block-size: 0.5rem;
|
|
30
|
+
background: var(--velin-color-surface-dim, #eee);
|
|
31
|
+
border-radius: var(--velin-radius-full, 999px);
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
.bar {
|
|
35
|
+
block-size: 100%;
|
|
36
|
+
inline-size: 0%;
|
|
37
|
+
border-radius: inherit;
|
|
38
|
+
background: var(--velin-color-danger, #dc2626);
|
|
39
|
+
transition: inline-size 160ms ease, background-color 160ms ease;
|
|
40
|
+
}
|
|
41
|
+
:host([data-score="2"]) .bar,
|
|
42
|
+
:host([data-score="3"]) .bar { background: var(--velin-color-warning, #f59e0b); }
|
|
43
|
+
:host([data-score="4"]) .bar,
|
|
44
|
+
:host([data-score="5"]) .bar { background: var(--velin-color-success, #16a34a); }
|
|
45
|
+
.label {
|
|
46
|
+
margin: 0;
|
|
47
|
+
font-size: var(--velin-text-sm, 0.875rem);
|
|
48
|
+
color: var(--velin-color-text-muted, #666);
|
|
49
|
+
}
|
|
50
|
+
@media (prefers-reduced-motion: reduce) {
|
|
51
|
+
.bar { transition: none; }
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
class VelinPasswordStrength extends HTMLElement {
|
|
56
|
+
static get observedAttributes() {
|
|
57
|
+
return ['for', 'value', 'label'];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
super();
|
|
62
|
+
this.attachShadow({ mode: 'open' });
|
|
63
|
+
this._onTargetInput = this._onTargetInput.bind(this);
|
|
64
|
+
this._target = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
connectedCallback() {
|
|
68
|
+
if (!this.shadowRoot.querySelector('.wrap')) {
|
|
69
|
+
this.shadowRoot.innerHTML = `
|
|
70
|
+
<style>${styles}</style>
|
|
71
|
+
<div class="wrap" part="wrap">
|
|
72
|
+
<div class="meter" part="meter" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
|
73
|
+
<div class="bar" part="bar"></div>
|
|
74
|
+
</div>
|
|
75
|
+
<p class="label" part="label" aria-live="polite"></p>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
this._bindTarget();
|
|
80
|
+
this._render(this._currentValue());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disconnectedCallback() {
|
|
84
|
+
this._unbindTarget();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
88
|
+
if (oldVal === newVal) return;
|
|
89
|
+
if (name === 'for') {
|
|
90
|
+
this._bindTarget();
|
|
91
|
+
this._render(this._currentValue());
|
|
92
|
+
} else if (name === 'value' || name === 'label') {
|
|
93
|
+
this._render(this._currentValue());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_currentValue() {
|
|
98
|
+
if (this._target) return this._target.value || '';
|
|
99
|
+
return this.getAttribute('value') || '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_unbindTarget() {
|
|
103
|
+
if (this._target) {
|
|
104
|
+
this._target.removeEventListener('input', this._onTargetInput);
|
|
105
|
+
this._target = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_bindTarget() {
|
|
110
|
+
this._unbindTarget();
|
|
111
|
+
const id = this.getAttribute('for');
|
|
112
|
+
if (!id || typeof document === 'undefined') return;
|
|
113
|
+
const el = document.getElementById(id);
|
|
114
|
+
if (el && 'value' in el) {
|
|
115
|
+
this._target = el;
|
|
116
|
+
this._target.addEventListener('input', this._onTargetInput);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_onTargetInput(e) {
|
|
121
|
+
this._render(e.target.value || '');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_render(pw) {
|
|
125
|
+
const result = scorePassword(pw);
|
|
126
|
+
const meter = this.shadowRoot?.querySelector('.meter');
|
|
127
|
+
const bar = this.shadowRoot?.querySelector('.bar');
|
|
128
|
+
const label = this.shadowRoot?.querySelector('.label');
|
|
129
|
+
if (!meter || !bar || !label) return;
|
|
130
|
+
|
|
131
|
+
bar.style.inlineSize = `${result.pct}%`;
|
|
132
|
+
meter.setAttribute('aria-valuenow', String(result.pct));
|
|
133
|
+
meter.setAttribute('aria-valuetext', result.label);
|
|
134
|
+
const prefix = this.getAttribute('label');
|
|
135
|
+
label.textContent = prefix ? `${prefix}: ${result.label}` : result.label;
|
|
136
|
+
this.setAttribute('data-score', String(result.score));
|
|
137
|
+
|
|
138
|
+
this.dispatchEvent(new CustomEvent('velin-strength', {
|
|
139
|
+
bubbles: true,
|
|
140
|
+
composed: true,
|
|
141
|
+
detail: result,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
customElements.define('velin-password-strength', VelinPasswordStrength);
|
|
147
|
+
export default VelinPasswordStrength;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { trapFocus, saveFocus, restoreFocus, getFocusableElements, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
|
|
2
|
-
import { escapeHTML } from './sanitize.js';
|
|
3
2
|
import { SHADOW_A11Y_STYLES } from './shadow-a11y-styles.js';
|
|
4
3
|
|
|
5
4
|
const styles = `
|
|
@@ -31,29 +30,39 @@ const styles = `
|
|
|
31
30
|
border-bottom: 1px solid var(--velin-color-border, #ddd);
|
|
32
31
|
}
|
|
33
32
|
.title { font-size: var(--velin-text-lg, 1.25rem); font-weight: 600; margin: 0; }
|
|
33
|
+
.close-btn {
|
|
34
|
+
min-width: 2.75rem; min-height: 2.75rem; display: inline-flex; align-items: center; justify-content: center;
|
|
35
|
+
background: none; border: none; border-radius: var(--velin-radius-md, 0.5rem); cursor: pointer;
|
|
36
|
+
color: var(--velin-color-text-muted, #666); font-size: 1.5rem; line-height: 1;
|
|
37
|
+
}
|
|
38
|
+
.close-btn:hover { background: var(--velin-color-surface-dim, #eee); color: var(--velin-color-text, #111); }
|
|
34
39
|
.body { flex: 1; overflow-y: auto; padding: var(--velin-space-5, 1.25rem); }
|
|
35
40
|
@media (prefers-reduced-motion: reduce) { .overlay, .sheet { transition: none; } }
|
|
36
41
|
`;
|
|
37
42
|
|
|
38
43
|
class VelinSheet extends HTMLElement {
|
|
39
|
-
static get observedAttributes() { return ['open']; }
|
|
44
|
+
static get observedAttributes() { return ['open', 'title', 'label']; }
|
|
40
45
|
|
|
41
46
|
constructor() {
|
|
42
47
|
super();
|
|
43
48
|
this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
44
49
|
this._prev = null;
|
|
45
50
|
this._onKey = this._onKey.bind(this);
|
|
51
|
+
this._onTitleSlot = this._onTitleSlot.bind(this);
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
connectedCallback() {
|
|
49
|
-
|
|
55
|
+
if (this.shadowRoot.querySelector('.sheet')) return;
|
|
50
56
|
const titleId = 'velin-sheet-title';
|
|
51
57
|
this.shadowRoot.innerHTML = `
|
|
52
58
|
<style>${styles}</style>
|
|
53
59
|
<div class="overlay" part="overlay"></div>
|
|
54
60
|
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="${titleId}" part="sheet">
|
|
55
61
|
<div class="header" part="header">
|
|
56
|
-
<h2 class="title" id="${titleId}"
|
|
62
|
+
<h2 class="title" id="${titleId}" part="title">
|
|
63
|
+
<slot name="title"></slot>
|
|
64
|
+
<span class="title-fallback"></span>
|
|
65
|
+
</h2>
|
|
57
66
|
<button class="close-btn" aria-label="Close" part="close">×</button>
|
|
58
67
|
</div>
|
|
59
68
|
<div class="body" part="body"><slot></slot></div>
|
|
@@ -61,11 +70,14 @@ class VelinSheet extends HTMLElement {
|
|
|
61
70
|
`;
|
|
62
71
|
this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => this.close());
|
|
63
72
|
this.shadowRoot.querySelector('.overlay').addEventListener('click', () => this.close());
|
|
73
|
+
this.shadowRoot.querySelector('slot[name="title"]').addEventListener('slotchange', this._onTitleSlot);
|
|
74
|
+
this._syncTitle();
|
|
64
75
|
if (this.hasAttribute('open')) this._open();
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
attributeChangedCallback(name) {
|
|
68
79
|
if (name === 'open') this.hasAttribute('open') ? this._open() : this._close();
|
|
80
|
+
if (name === 'title' || name === 'label') this._syncTitle();
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
open() { this.setAttribute('open', ''); }
|
|
@@ -74,6 +86,39 @@ class VelinSheet extends HTMLElement {
|
|
|
74
86
|
this.dispatchEvent(new CustomEvent('velin-close', { bubbles: true }));
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
_syncTitle() {
|
|
90
|
+
const fallback = this.shadowRoot?.querySelector('.title-fallback');
|
|
91
|
+
if (!fallback) return;
|
|
92
|
+
fallback.textContent = this.getAttribute('title') || this.getAttribute('label') || '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_hasTitleSlot() {
|
|
96
|
+
const slot = this.shadowRoot?.querySelector('slot[name="title"]');
|
|
97
|
+
if (!slot) return false;
|
|
98
|
+
return slot.assignedNodes({ flatten: true }).some((n) => {
|
|
99
|
+
if (n.nodeType === Node.TEXT_NODE) return Boolean(n.textContent.trim());
|
|
100
|
+
return n.nodeType === Node.ELEMENT_NODE;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_onTitleSlot() {
|
|
105
|
+
const dialog = this.shadowRoot?.querySelector('[role="dialog"]');
|
|
106
|
+
const fallback = this.shadowRoot?.querySelector('.title-fallback');
|
|
107
|
+
if (!dialog || !fallback) return;
|
|
108
|
+
if (this._hasTitleSlot()) {
|
|
109
|
+
fallback.hidden = true;
|
|
110
|
+
dialog.removeAttribute('aria-labelledby');
|
|
111
|
+
const slot = this.shadowRoot.querySelector('slot[name="title"]');
|
|
112
|
+
const label = slot.assignedNodes({ flatten: true }).map((n) => n.textContent || '').join(' ').trim();
|
|
113
|
+
if (label) dialog.setAttribute('aria-label', label);
|
|
114
|
+
} else {
|
|
115
|
+
fallback.hidden = false;
|
|
116
|
+
dialog.removeAttribute('aria-label');
|
|
117
|
+
dialog.setAttribute('aria-labelledby', 'velin-sheet-title');
|
|
118
|
+
this._syncTitle();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
77
122
|
_open() {
|
|
78
123
|
this._prev = saveFocus();
|
|
79
124
|
setBackgroundInert(this);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"wcagLevel": "AAA",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"components": {
|
|
5
5
|
"velin-accordion": {
|
|
6
6
|
"status": "pass",
|
|
@@ -111,7 +111,8 @@
|
|
|
111
111
|
"status": "pass",
|
|
112
112
|
"roles": ["dialog"],
|
|
113
113
|
"keyboard": "Escape, Tab trap",
|
|
114
|
-
"requiredAttributes": []
|
|
114
|
+
"requiredAttributes": [],
|
|
115
|
+
"notes": "Reactive title attribute + slot=title."
|
|
115
116
|
},
|
|
116
117
|
"velin-dropdown": {
|
|
117
118
|
"status": "pass",
|
|
@@ -123,6 +124,11 @@
|
|
|
123
124
|
"roles": ["button"],
|
|
124
125
|
"requiredAttributes": ["aria-label"]
|
|
125
126
|
},
|
|
127
|
+
"velin-empty-state": {
|
|
128
|
+
"status": "pass",
|
|
129
|
+
"roles": ["status"],
|
|
130
|
+
"notes": "heading/description attributes or slots; actions slot for CTAs."
|
|
131
|
+
},
|
|
126
132
|
"velin-form-summary": {
|
|
127
133
|
"status": "pass",
|
|
128
134
|
"roles": ["alert", "link"],
|
|
@@ -159,7 +165,19 @@
|
|
|
159
165
|
"status": "pass",
|
|
160
166
|
"roles": ["dialog"],
|
|
161
167
|
"keyboard": "Escape, Tab trap",
|
|
162
|
-
"requiredAttributes": []
|
|
168
|
+
"requiredAttributes": [],
|
|
169
|
+
"notes": "Reactive title attribute + slot=title; footer slot."
|
|
170
|
+
},
|
|
171
|
+
"velin-otp-input": {
|
|
172
|
+
"status": "pass",
|
|
173
|
+
"roles": ["group"],
|
|
174
|
+
"keyboard": "Digits, arrows, backspace, paste",
|
|
175
|
+
"notes": "autocomplete one-time-code on first digit."
|
|
176
|
+
},
|
|
177
|
+
"velin-password-strength": {
|
|
178
|
+
"status": "pass",
|
|
179
|
+
"roles": ["progressbar"],
|
|
180
|
+
"notes": "Heuristic meter; aria-live label; for= links to password input."
|
|
163
181
|
},
|
|
164
182
|
"velin-persist": {
|
|
165
183
|
"status": "pass",
|
|
@@ -215,7 +233,8 @@
|
|
|
215
233
|
"velin-sheet": {
|
|
216
234
|
"status": "pass",
|
|
217
235
|
"roles": ["dialog"],
|
|
218
|
-
"keyboard": "Escape, Tab trap"
|
|
236
|
+
"keyboard": "Escape, Tab trap",
|
|
237
|
+
"notes": "Reactive title/label + slot=title."
|
|
219
238
|
},
|
|
220
239
|
"velin-sparkline": {
|
|
221
240
|
"status": "pass",
|
|
@@ -226,6 +226,20 @@ function registerBuiltins() {
|
|
|
226
226
|
},
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
registerAttribute('velin-transparency', {
|
|
230
|
+
async enhance(el) {
|
|
231
|
+
const { attach } = await import('../transparency/attach.js');
|
|
232
|
+
attach(el);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
registerAttribute('velin-disclosure', {
|
|
236
|
+
async enhance(el) {
|
|
237
|
+
const { attach } = await import('../transparency/attach.js');
|
|
238
|
+
if (!el.hasAttribute('velin-transparency')) el.setAttribute('velin-transparency', '');
|
|
239
|
+
attach(el);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
229
243
|
for (const motion of ['velin-reveal', 'velin-fade', 'velin-slide', 'velin-scale', 'velin-parallax', 'velin-hover', 'velin-stagger', 'velin-scroll']) {
|
|
230
244
|
registerAttribute(motion, { enhance() {} });
|
|
231
245
|
}
|