@birdapi/velinstyle 1.2.1 → 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 (67) hide show
  1. package/README.de.md +134 -11
  2. package/README.md +139 -16
  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-cli.js +1 -1
  18. package/cli/index.js +149 -3
  19. package/cli/production/component-graph.json +166 -0
  20. package/cli/production/explain.js +52 -0
  21. package/cli/production/extract.js +238 -0
  22. package/cli/production/graph.js +102 -0
  23. package/cli/production/report.js +78 -0
  24. package/cli/production/run.js +312 -0
  25. package/cli/production/trim-css.js +177 -0
  26. package/cli/production/trim-fonts.js +23 -0
  27. package/cli/production/trim-icons.js +38 -0
  28. package/cli/production/trim-js.js +48 -0
  29. package/cli/production/trim-motion.js +22 -0
  30. package/cli/production/trim-themes.js +50 -0
  31. package/cli/production/watch.js +38 -0
  32. package/cli/review.js +48 -1
  33. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  34. package/components/index.js +3 -0
  35. package/components/runtime/component-loaders.js +3 -0
  36. package/components/velin-drawer.js +43 -4
  37. package/components/velin-empty-state.js +83 -0
  38. package/components/velin-modal.js +76 -15
  39. package/components/velin-otp-input.js +220 -0
  40. package/components/velin-password-strength.js +147 -0
  41. package/components/velin-sheet.js +49 -4
  42. package/core/a11y/component-contracts.json +23 -4
  43. package/core/meta/knowledge/components.json +75 -3
  44. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  45. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  46. package/dist/chunks/runtime-entry.js +1 -1
  47. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  48. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  49. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  50. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  51. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  52. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  53. package/dist/llms.txt +3 -3
  54. package/dist/search-index.json +68 -5
  55. package/dist/velin-agent.json +283 -20
  56. package/dist/velinstyle-components.iife.js +2716 -2138
  57. package/dist/velinstyle-components.js +2772 -2190
  58. package/dist/velinstyle-components.min.js +254 -112
  59. package/dist/velinstyle.css +54 -6
  60. package/dist/velinstyle.d.ts +3 -0
  61. package/dist/velinstyle.min.css +1 -1
  62. package/package.json +143 -143
  63. package/packages/velinstyle-skills/catalog.json +1 -1
  64. package/src/components/data-table.css +19 -0
  65. package/src/components/empty-auth.css +33 -0
  66. package/src/components/table.css +16 -6
  67. package/src/velinstyle.css +1 -0
@@ -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.1",
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",
@@ -201,7 +201,7 @@
201
201
  "density": ["default"],
202
202
  "themeSupport": ["light", "dark"],
203
203
  "motion": { "reducedMotionSafe": true },
204
- "examples": ["<velin-modal><h2 slot=\"title\">…</h2></velin-modal>"],
204
+ "examples": ["<velin-modal title=\"Confirm\"></velin-modal>", "<velin-modal><h2 slot=\"title\">…</h2></velin-modal>"],
205
205
  "promptKeywords": ["modal", "dialog overlay", "popup"],
206
206
  "designIntelligence": {
207
207
  "visualWeight": 5,
@@ -491,7 +491,7 @@
491
491
  "density": ["default", "compact"],
492
492
  "themeSupport": ["light", "dark"],
493
493
  "motion": { "reducedMotionSafe": true },
494
- "examples": ["<velin-drawer></velin-drawer>"],
494
+ "examples": ["<velin-drawer title=\"Filters\"></velin-drawer>", "<velin-drawer><span slot=\"title\">Filters</span></velin-drawer>"],
495
495
  "promptKeywords": ["drawer", "side panel", "sidebar overlay"],
496
496
  "designIntelligence": {
497
497
  "visualWeight": 4,
@@ -528,7 +528,7 @@
528
528
  "density": ["default", "compact"],
529
529
  "themeSupport": ["light", "dark"],
530
530
  "motion": { "reducedMotionSafe": true },
531
- "examples": ["<velin-sheet></velin-sheet>"],
531
+ "examples": ["<velin-sheet title=\"Actions\"></velin-sheet>", "<velin-sheet><span slot=\"title\">Actions</span></velin-sheet>"],
532
532
  "promptKeywords": ["sheet", "bottom sheet", "action sheet"],
533
533
  "designIntelligence": {
534
534
  "visualWeight": 4,
@@ -617,6 +617,78 @@
617
617
  "commonMistakes": ["toasts for persistent form errors"],
618
618
  "bestPractices": ["auto-dismiss with pause on focus/hover"]
619
619
  }
620
+ },
621
+ {
622
+ "id": "velin-otp-input",
623
+ "kind": "web-component",
624
+ "tag": "velin-otp-input",
625
+ "category": "forms",
626
+ "subcategory": "otp",
627
+ "purpose": "Segmented one-time-password / PIN digit entry with paste and keyboard support.",
628
+ "family": "forms",
629
+ "maturity": "core",
630
+ "recommendedPages": ["auth", "saas"],
631
+ "recommendedSections": [],
632
+ "compatibleWith": ["css-button", "velin-form-summary", "velin-secure-field"],
633
+ "incompatibleWith": [],
634
+ "accessibility": { "role": "group", "notes": "Per-digit labels; autocomplete one-time-code" },
635
+ "seo": { "impact": "none" },
636
+ "performance": { "cost": "lazy-ok" },
637
+ "variants": [],
638
+ "density": ["default"],
639
+ "themeSupport": ["light", "dark"],
640
+ "motion": { "reducedMotionSafe": true },
641
+ "examples": ["<velin-otp-input length=\"6\"></velin-otp-input>"],
642
+ "promptKeywords": ["otp", "2fa", "pin", "one-time password"],
643
+ "api": { "events": ["velin-change", "velin-complete"], "attributes": ["length", "value", "disabled", "name"] }
644
+ },
645
+ {
646
+ "id": "velin-password-strength",
647
+ "kind": "web-component",
648
+ "tag": "velin-password-strength",
649
+ "category": "forms",
650
+ "subcategory": "password",
651
+ "purpose": "Heuristic password strength meter linked to an input via for= or value.",
652
+ "family": "forms",
653
+ "maturity": "core",
654
+ "recommendedPages": ["auth", "saas"],
655
+ "recommendedSections": [],
656
+ "compatibleWith": ["css-input", "velin-secure-field", "velin-form-summary"],
657
+ "incompatibleWith": [],
658
+ "accessibility": { "role": "progressbar", "notes": "aria-live polite label" },
659
+ "seo": { "impact": "none" },
660
+ "performance": { "cost": "lazy-ok" },
661
+ "variants": [],
662
+ "density": ["default"],
663
+ "themeSupport": ["light", "dark"],
664
+ "motion": { "reducedMotionSafe": true },
665
+ "examples": ["<input id=\"pw\" type=\"password\"><velin-password-strength for=\"pw\"></velin-password-strength>"],
666
+ "promptKeywords": ["password strength", "meter", "signup"],
667
+ "api": { "events": ["velin-strength"], "attributes": ["for", "value", "label"] }
668
+ },
669
+ {
670
+ "id": "velin-empty-state",
671
+ "kind": "web-component",
672
+ "tag": "velin-empty-state",
673
+ "category": "feedback",
674
+ "subcategory": "empty",
675
+ "purpose": "Consistent empty / zero-data surface with illustration, title, description, and actions slots.",
676
+ "family": "feedback",
677
+ "maturity": "core",
678
+ "recommendedPages": ["dashboard", "saas", "shop", "docs"],
679
+ "recommendedSections": [],
680
+ "compatibleWith": ["css-button", "velin-icon"],
681
+ "incompatibleWith": [],
682
+ "accessibility": { "role": "status" },
683
+ "seo": { "impact": "none" },
684
+ "performance": { "cost": "lazy-ok" },
685
+ "variants": [],
686
+ "density": ["default"],
687
+ "themeSupport": ["light", "dark"],
688
+ "motion": { "reducedMotionSafe": true },
689
+ "examples": ["<velin-empty-state heading=\"No items yet\" description=\"Create your first entry.\"><button slot=\"actions\" class=\"velin-btn velin-btn--primary\">Create</button></velin-empty-state>"],
690
+ "promptKeywords": ["empty state", "no results", "zero data"],
691
+ "api": { "attributes": ["heading", "description"], "slots": ["illustration", "title", "description", "actions"] }
620
692
  }
621
693
  ]
622
694
  }