@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.
Files changed (89) hide show
  1. package/README.de.md +150 -24
  2. package/README.md +155 -33
  3. package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
  4. package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
  5. package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
  6. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
  7. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
  8. package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
  9. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
  10. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
  11. package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
  12. package/cli/atelier-catalog.json +1267 -0
  13. package/cli/atelier-formats.js +159 -0
  14. package/cli/atelier.js +382 -0
  15. package/cli/blueprints/empty-state.html +3 -5
  16. package/cli/cli-manifest.json +23 -10
  17. package/cli/docgen/extract-attributes.js +2 -0
  18. package/cli/docgen/extract-cli.js +1 -1
  19. package/cli/index.js +181 -6
  20. package/cli/production/component-graph.json +166 -0
  21. package/cli/production/explain.js +52 -0
  22. package/cli/production/extract.js +238 -0
  23. package/cli/production/graph.js +102 -0
  24. package/cli/production/report.js +78 -0
  25. package/cli/production/run.js +312 -0
  26. package/cli/production/trim-css.js +177 -0
  27. package/cli/production/trim-fonts.js +23 -0
  28. package/cli/production/trim-icons.js +38 -0
  29. package/cli/production/trim-js.js +48 -0
  30. package/cli/production/trim-motion.js +22 -0
  31. package/cli/production/trim-themes.js +50 -0
  32. package/cli/production/watch.js +38 -0
  33. package/cli/review.js +48 -1
  34. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  35. package/cli/transparency.js +221 -0
  36. package/components/index.js +3 -0
  37. package/components/runtime/component-loaders.js +3 -0
  38. package/components/velin-drawer.js +43 -4
  39. package/components/velin-empty-state.js +83 -0
  40. package/components/velin-modal.js +76 -15
  41. package/components/velin-otp-input.js +220 -0
  42. package/components/velin-password-strength.js +147 -0
  43. package/components/velin-sheet.js +49 -4
  44. package/core/a11y/component-contracts.json +23 -4
  45. package/core/attributes/registry.js +14 -0
  46. package/core/meta/knowledge/components.json +75 -3
  47. package/core/meta/schema.js +10 -0
  48. package/core/transparency/attach.js +74 -0
  49. package/core/transparency/claims.js +97 -0
  50. package/core/transparency/doctor.js +142 -0
  51. package/core/transparency/engine.js +75 -0
  52. package/core/transparency/export.js +98 -0
  53. package/core/transparency/index.js +30 -0
  54. package/core/transparency/migrate.js +130 -0
  55. package/core/transparency/normalize.js +125 -0
  56. package/core/transparency/policy.js +105 -0
  57. package/core/transparency/providers.js +235 -0
  58. package/core/transparency/registry.js +104 -0
  59. package/core/transparency/renderer.js +111 -0
  60. package/core/transparency/reporter.js +113 -0
  61. package/core/transparency/validator.js +112 -0
  62. package/dist/chunks/attach-H2ZSEAE6.js +365 -0
  63. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  64. package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
  65. package/dist/chunks/chunk-CQMTCEI6.js +113 -0
  66. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  67. package/dist/chunks/runtime-entry.js +1 -1
  68. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  69. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  70. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  71. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  72. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  73. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  74. package/dist/llms.txt +5 -4
  75. package/dist/search-index.json +94 -5
  76. package/dist/velin-agent.json +290 -23
  77. package/dist/velinstyle-components.iife.js +3189 -2202
  78. package/dist/velinstyle-components.js +3187 -2196
  79. package/dist/velinstyle-components.min.js +261 -119
  80. package/dist/velinstyle.css +214 -6
  81. package/dist/velinstyle.d.ts +3 -0
  82. package/dist/velinstyle.min.css +1 -1
  83. package/package.json +143 -142
  84. package/packages/velinstyle-skills/catalog.json +1 -1
  85. package/src/components/data-table.css +19 -0
  86. package/src/components/empty-auth.css +33 -0
  87. package/src/components/table.css +16 -6
  88. package/src/components/transparency.css +138 -0
  89. 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
- const title = (this.getAttribute('title') || '').trim();
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" ${dialogLabel} part="dialog">
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"${title ? '' : ' class="velin-sr-only"'}>${safeTitle || 'Dialog'}</h2>
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">&#215;</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, oldVal, newVal) {
138
+ attributeChangedCallback(name) {
127
139
  if (name === 'open') {
128
- if (newVal !== null) {
129
- this._open();
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
- const title = escapeHTML(this.getAttribute('title') || this.getAttribute('label') || '');
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}">${title}</h2>
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">&#215;</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.0",
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
  }