@birdapi/velinstyle 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.de.md +148 -11
  2. package/README.md +174 -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 +441 -161
  17. package/cli/docgen/extract-cli.js +2 -1
  18. package/cli/docs-generate.js +9 -0
  19. package/cli/experience.js +173 -0
  20. package/cli/index.js +1384 -361
  21. package/cli/motion.js +738 -0
  22. package/cli/production/component-graph.json +166 -0
  23. package/cli/production/explain.js +52 -0
  24. package/cli/production/extract.js +278 -0
  25. package/cli/production/graph.js +102 -0
  26. package/cli/production/report.js +168 -0
  27. package/cli/production/run.js +355 -0
  28. package/cli/production/trim-css.js +177 -0
  29. package/cli/production/trim-fonts.js +23 -0
  30. package/cli/production/trim-icons.js +38 -0
  31. package/cli/production/trim-js.js +48 -0
  32. package/cli/production/trim-motion.js +22 -0
  33. package/cli/production/trim-themes.js +50 -0
  34. package/cli/production/watch.js +38 -0
  35. package/cli/review.js +48 -1
  36. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  37. package/cli/security.js +190 -0
  38. package/cli/skills.js +53 -10
  39. package/cli/transparency.js +140 -41
  40. package/cli/workflow.js +32 -17
  41. package/components/index.js +3 -0
  42. package/components/runtime/component-loaders.js +3 -0
  43. package/components/velin-drawer.js +43 -4
  44. package/components/velin-empty-state.js +83 -0
  45. package/components/velin-modal.js +76 -15
  46. package/components/velin-otp-input.js +220 -0
  47. package/components/velin-password-strength.js +147 -0
  48. package/components/velin-sheet.js +49 -4
  49. package/components/velin-theme-toggle.js +15 -1
  50. package/core/a11y/component-contracts.json +391 -255
  51. package/core/attributes/registry.js +1 -1
  52. package/core/meta/knowledge/components.json +75 -3
  53. package/core/motion/analyze.js +38 -0
  54. package/core/motion/blueprint.js +51 -0
  55. package/core/motion/composer.js +114 -0
  56. package/core/motion/data/registry.json +352 -0
  57. package/core/motion/effects.js +17 -1
  58. package/core/motion/index.js +112 -9
  59. package/core/motion/optimize.js +44 -0
  60. package/core/motion/policy.js +77 -0
  61. package/core/motion/registry.js +131 -0
  62. package/core/motion/review.js +59 -0
  63. package/core/motion/scan.js +150 -0
  64. package/core/motion/timeline.js +68 -0
  65. package/core/motion/transitions.js +49 -0
  66. package/core/motion/triggers.js +168 -0
  67. package/core/security/checks/fs.js +92 -0
  68. package/core/security/detect.js +35 -0
  69. package/core/security/engine.js +116 -0
  70. package/core/security/index.js +18 -0
  71. package/core/security/registry.js +85 -0
  72. package/core/security/report/report.js +113 -0
  73. package/core/security/rules/ci/index.js +146 -0
  74. package/core/security/rules/consumer/index.js +174 -0
  75. package/core/security/rules/deps/index.js +182 -0
  76. package/core/security/rules/index.js +24 -0
  77. package/core/security/rules/publish/index.js +230 -0
  78. package/core/security/rules/repo/index.js +143 -0
  79. package/core/security/rules/secrets/index.js +133 -0
  80. package/core/security/score/dependency-health.js +53 -0
  81. package/core/security/score/release-health.js +66 -0
  82. package/core/security/score/score.js +102 -0
  83. package/core/security/timeline/timeline.js +76 -0
  84. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  85. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  86. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  87. package/dist/chunks/chunk-MYKUI364.js +116 -0
  88. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  89. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  90. package/dist/chunks/runtime-entry.js +1 -1
  91. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  92. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  93. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  94. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  95. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  96. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  97. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  98. package/dist/llms.txt +4 -4
  99. package/dist/search-index.json +232 -5
  100. package/dist/velin-agent.json +433 -46
  101. package/dist/velinstyle-components.iife.js +3695 -2272
  102. package/dist/velinstyle-components.js +3698 -2271
  103. package/dist/velinstyle-components.min.js +262 -120
  104. package/dist/velinstyle.css +96 -13
  105. package/dist/velinstyle.d.ts +3 -0
  106. package/dist/velinstyle.min.css +1 -1
  107. package/package.json +19 -4
  108. package/packages/velinstyle-cli-core/package.json +11 -0
  109. package/packages/velinstyle-cli-core/src/config.js +84 -0
  110. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  111. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  112. package/packages/velinstyle-cli-core/src/index.js +18 -0
  113. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  114. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  115. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  116. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  117. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  118. package/packages/velinstyle-cli-registry/package.json +13 -0
  119. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  120. package/packages/velinstyle-cli-renderer/package.json +8 -0
  121. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  122. package/packages/velinstyle-cli-theme/package.json +8 -0
  123. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  124. package/packages/velinstyle-cli-ui/package.json +8 -0
  125. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  126. package/packages/velinstyle-motion/README.md +81 -0
  127. package/packages/velinstyle-motion/package.json +37 -0
  128. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  129. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  130. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  131. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  132. package/packages/velinstyle-motion/src/config.js +257 -0
  133. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  134. package/packages/velinstyle-motion/src/index.js +304 -0
  135. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  136. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  137. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  138. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  139. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  140. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  141. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  142. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  143. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  144. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  145. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  146. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  147. package/packages/velinstyle-skills/catalog.json +1 -1
  148. package/packages/velinstyle-skills/registry.json +1 -142
  149. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  150. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  151. package/src/components/data-table.css +19 -0
  152. package/src/components/empty-auth.css +33 -0
  153. package/src/components/table.css +16 -6
  154. package/src/tokens/motion.css +7 -0
  155. package/src/utilities/scroll-animation.css +66 -0
  156. package/src/velinstyle.css +1 -0
@@ -0,0 +1,83 @@
1
+ const styles = `
2
+ :host {
3
+ display: block;
4
+ text-align: center;
5
+ padding: var(--velin-space-8, 2rem) var(--velin-space-4, 1rem);
6
+ }
7
+ .illustration {
8
+ display: flex;
9
+ justify-content: center;
10
+ margin-block-end: var(--velin-space-4, 1rem);
11
+ color: var(--velin-color-text-muted, #666);
12
+ }
13
+ .title {
14
+ margin: 0 0 var(--velin-space-2, 0.5rem);
15
+ font-size: var(--velin-text-xl, 1.25rem);
16
+ font-weight: var(--velin-weight-bold, 700);
17
+ color: var(--velin-color-text, #111);
18
+ }
19
+ .description {
20
+ margin: 0 0 var(--velin-space-4, 1rem);
21
+ color: var(--velin-color-text-muted, #666);
22
+ max-inline-size: 36rem;
23
+ margin-inline: auto;
24
+ }
25
+ .actions {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ gap: var(--velin-space-3, 0.75rem);
29
+ justify-content: center;
30
+ }
31
+ `;
32
+
33
+ class VelinEmptyState extends HTMLElement {
34
+ static get observedAttributes() {
35
+ return ['heading', 'description'];
36
+ }
37
+
38
+ constructor() {
39
+ super();
40
+ this.attachShadow({ mode: 'open' });
41
+ }
42
+
43
+ connectedCallback() {
44
+ if (this.shadowRoot.querySelector('.root')) {
45
+ this._syncText();
46
+ return;
47
+ }
48
+ const titleId = 'velin-empty-title';
49
+ this.shadowRoot.innerHTML = `
50
+ <style>${styles}</style>
51
+ <section class="root" part="root" role="status" aria-labelledby="${titleId}">
52
+ <div class="illustration" part="illustration"><slot name="illustration"></slot></div>
53
+ <h2 class="title" id="${titleId}" part="title">
54
+ <slot name="title"><span class="heading-fallback"></span></slot>
55
+ </h2>
56
+ <div class="description" part="description">
57
+ <slot name="description"><span class="description-fallback"></span></slot>
58
+ </div>
59
+ <div class="actions" part="actions"><slot name="actions"></slot></div>
60
+ <slot></slot>
61
+ </section>
62
+ `;
63
+ this._syncText();
64
+ }
65
+
66
+ attributeChangedCallback() {
67
+ this._syncText();
68
+ }
69
+
70
+ _syncText() {
71
+ const heading = this.shadowRoot?.querySelector('.heading-fallback');
72
+ const description = this.shadowRoot?.querySelector('.description-fallback');
73
+ if (heading) heading.textContent = this.getAttribute('heading') || 'Nothing here yet';
74
+ if (description) {
75
+ const text = this.getAttribute('description') || '';
76
+ description.textContent = text;
77
+ description.hidden = !text;
78
+ }
79
+ }
80
+ }
81
+
82
+ customElements.define('velin-empty-state', VelinEmptyState);
83
+ export default VelinEmptyState;
@@ -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);