@birdapi/velinstyle 0.7.0 → 0.8.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 (49) hide show
  1. package/README.de.md +26 -4
  2. package/README.md +26 -4
  3. package/cli/blueprint.js +8 -0
  4. package/cli/blueprints/bottom-nav-mobile.html +17 -0
  5. package/cli/blueprints/cookie-consent.html +9 -0
  6. package/cli/blueprints/empty-state.html +5 -0
  7. package/cli/blueprints/filter-bar.html +15 -0
  8. package/cli/blueprints/notification-center.html +13 -0
  9. package/cli/blueprints/onboarding.html +23 -0
  10. package/cli/blueprints/pricing-table.html +20 -0
  11. package/cli/blueprints/settings-panel.html +20 -0
  12. package/cli/index.js +116 -1
  13. package/cli/layout-audit.js +325 -0
  14. package/cli/scaffold-recipes.json +70 -0
  15. package/cli/scaffold.js +155 -0
  16. package/cli/scanner.js +68 -0
  17. package/components/index.js +19 -0
  18. package/components/sanitize.js +29 -3
  19. package/components/shadow-a11y-styles.js +18 -0
  20. package/components/velin-announcer.js +35 -0
  21. package/components/velin-bottom-nav.js +89 -0
  22. package/components/velin-combobox.js +149 -0
  23. package/components/velin-command.js +127 -0
  24. package/components/velin-counter.js +152 -0
  25. package/components/velin-flip.js +220 -0
  26. package/components/velin-icon.js +43 -9
  27. package/components/velin-live-dot.js +85 -0
  28. package/components/velin-menubar.js +83 -0
  29. package/components/velin-rating.js +91 -0
  30. package/components/velin-reveal.js +80 -0
  31. package/components/velin-segmented-control.js +108 -0
  32. package/components/velin-sheet.js +107 -0
  33. package/components/velin-sparkline.js +207 -0
  34. package/components/velin-theme-toggle.js +277 -60
  35. package/dist/velinstyle-components.iife.js +1629 -69
  36. package/dist/velinstyle-components.js +1649 -69
  37. package/dist/velinstyle-components.min.js +424 -80
  38. package/dist/velinstyle.css +472 -3
  39. package/dist/velinstyle.min.css +1 -1
  40. package/package.json +3 -2
  41. package/src/a11y/security.css +18 -0
  42. package/src/base/reset.css +12 -1
  43. package/src/components/nav.css +152 -151
  44. package/src/tokens/motion.css +7 -0
  45. package/src/utilities/animation.css +97 -1
  46. package/src/utilities/chart-animation.css +101 -0
  47. package/src/utilities/filter-effects.css +103 -0
  48. package/src/utilities/safe-area.css +39 -0
  49. package/src/velinstyle.css +3 -0
@@ -1,91 +1,308 @@
1
+ const THEMES = [
2
+ { slug: '', label: 'Default (Light)' },
3
+ { slug: 'dark', label: 'Dark' },
4
+ { slug: 'brutalist', label: 'Brutalist' },
5
+ { slug: 'corporate', label: 'Corporate' },
6
+ { slug: 'earth', label: 'Earth' },
7
+ { slug: 'forest', label: 'Forest' },
8
+ { slug: 'midnight', label: 'Midnight' },
9
+ { slug: 'neon', label: 'Neon' },
10
+ { slug: 'nordic', label: 'Nordic' },
11
+ { slug: 'ocean', label: 'Ocean' },
12
+ { slug: 'pastel', label: 'Pastel' },
13
+ { slug: 'retro', label: 'Retro' },
14
+ { slug: 'sharp', label: 'Sharp' },
15
+ { slug: 'soft', label: 'Soft' },
16
+ { slug: 'sunset', label: 'Sunset' },
17
+ ];
18
+
19
+ const BUILTIN_THEMES = new Set(['', 'dark']);
20
+
21
+ const loadedThemeStylesheets = new Set();
22
+
23
+ function ensureThemeStylesheet(slug, base) {
24
+ if (!slug || BUILTIN_THEMES.has(slug)) return;
25
+ if (loadedThemeStylesheets.has(slug)) return;
26
+ const existing = document.querySelector(`link[data-velin-theme-css="${slug}"]`);
27
+ if (existing) {
28
+ loadedThemeStylesheets.add(slug);
29
+ return;
30
+ }
31
+ const link = document.createElement('link');
32
+ link.rel = 'stylesheet';
33
+ link.href = `${base.replace(/\/$/, '')}/${slug}.min.css`;
34
+ link.setAttribute('data-velin-theme-css', slug);
35
+ document.head.appendChild(link);
36
+ loadedThemeStylesheets.add(slug);
37
+ }
38
+
1
39
  const styles = `
2
- :host { display: inline-flex; }
40
+ :host { display: inline-flex; position: relative; }
41
+ .group {
42
+ display: inline-flex; align-items: stretch;
43
+ border: 2px solid var(--velin-color-border, #ddd);
44
+ border-radius: var(--velin-radius-md, 0.5rem);
45
+ background: none;
46
+ overflow: hidden;
47
+ }
3
48
  button {
4
49
  display: inline-flex; align-items: center; justify-content: center;
5
- min-width: 2.75rem; min-height: 2.75rem; padding: 0.5rem;
6
- background: none; border: 2px solid var(--velin-color-border, #ddd);
7
- border-radius: var(--velin-radius-md, 0.5rem); cursor: pointer;
8
- color: var(--velin-color-text, #111); transition: background 150ms ease, border-color 150ms ease;
50
+ min-height: 2.75rem; padding: 0.5rem;
51
+ background: none; border: 0; cursor: pointer;
52
+ color: var(--velin-color-text, #111);
53
+ transition: background 150ms ease;
54
+ }
55
+ button:hover { background: var(--velin-color-surface-dim, #eee); }
56
+ button:focus-visible {
57
+ outline: 3px solid var(--velin-color-focus, #2563eb);
58
+ outline-offset: 2px;
59
+ }
60
+ .toggle { min-width: 2.75rem; }
61
+ .picker {
62
+ min-width: 1.75rem;
63
+ border-inline-start: 1px solid var(--velin-color-border, #ddd);
64
+ color: var(--velin-color-text-muted, #555);
9
65
  }
10
- button:hover { background: var(--velin-color-surface-dim, #eee); border-color: var(--velin-color-border-strong, #999); }
11
- button:focus-visible { outline: 3px solid var(--velin-color-focus, #2563eb); outline-offset: 2px; }
12
66
  svg { width: 1.25rem; height: 1.25rem; transition: transform 300ms ease; }
67
+ .chev { width: 0.75rem; height: 0.75rem; }
13
68
  :host([theme="dark"]) .sun { display: none; }
14
69
  :host(:not([theme="dark"])) .moon { display: none; }
70
+ :host([compact]) .picker { display: none; }
71
+ :host([compact]) .toggle { border-inline-end: 0; }
15
72
  @media (prefers-reduced-motion: reduce) { svg { transition: none; } }
73
+
74
+ .menu {
75
+ position: absolute;
76
+ top: calc(100% + 0.5rem);
77
+ inset-inline-end: 0;
78
+ z-index: 1000;
79
+ min-width: 12rem;
80
+ padding: 0.375rem;
81
+ background: var(--velin-color-surface-bright, #fff);
82
+ border: 1px solid var(--velin-color-border, #ddd);
83
+ border-radius: var(--velin-radius-md, 0.5rem);
84
+ box-shadow: var(--velin-shadow-lg, 0 12px 32px rgba(0,0,0,0.12));
85
+ list-style: none;
86
+ margin: 0;
87
+ display: none;
88
+ max-height: min(70vh, 24rem);
89
+ overflow-y: auto;
90
+ }
91
+ :host([menu-open]) .menu { display: block; }
92
+ .menu li { margin: 0; }
93
+ .menu button {
94
+ width: 100%;
95
+ justify-content: flex-start;
96
+ padding: 0.4rem 0.75rem;
97
+ font-size: 0.875rem;
98
+ color: var(--velin-color-text, #111);
99
+ border-radius: var(--velin-radius-sm, 0.25rem);
100
+ min-height: 2rem;
101
+ text-align: start;
102
+ }
103
+ .menu button:hover,
104
+ .menu button[aria-current="true"] {
105
+ background: var(--velin-color-primary-subtle, #eef);
106
+ color: var(--velin-color-primary, #2a4cf0);
107
+ }
108
+ .menu button[aria-current="true"] {
109
+ font-weight: 600;
110
+ }
111
+ .menu .swatch {
112
+ width: 0.75rem; height: 0.75rem;
113
+ border-radius: 50%;
114
+ margin-inline-end: 0.5rem;
115
+ background: currentColor;
116
+ border: 1px solid var(--velin-color-border, #ddd);
117
+ }
16
118
  `;
17
119
 
18
120
  class VelinThemeToggle extends HTMLElement {
19
121
  constructor() {
20
122
  super();
21
123
  this.attachShadow({ mode: 'open' });
124
+ this._onDocClick = this._onDocClick.bind(this);
125
+ this._onKeyDown = this._onKeyDown.bind(this);
22
126
  }
23
127
 
24
128
  connectedCallback() {
25
129
  this.shadowRoot.innerHTML = `
26
130
  <style>${styles}</style>
27
- <button part="button" aria-label="Toggle dark mode">
28
- <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
29
- <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
30
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
31
- <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
32
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
33
- </svg>
34
- <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
35
- <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
36
- </svg>
37
- </button>
131
+ <div class="group" part="group">
132
+ <button class="toggle" part="button" aria-label="Toggle dark mode">
133
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
134
+ <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
135
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
136
+ <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
137
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
138
+ </svg>
139
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
140
+ <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
141
+ </svg>
142
+ </button>
143
+ <button class="picker" part="picker" aria-label="Choose theme" aria-haspopup="menu" aria-expanded="false">
144
+ <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
145
+ <polyline points="6 9 12 15 18 9"/>
146
+ </svg>
147
+ </button>
148
+ </div>
149
+ <ul class="menu" role="menu" hidden></ul>
38
150
  `;
39
151
 
40
- const target = this.getAttribute('target') || 'html';
41
- const el = document.querySelector(target);
42
- const stored = localStorage.getItem('velin-theme');
43
- const prefersDarkMq = window.matchMedia('(prefers-color-scheme: dark)');
152
+ this._target = document.querySelector(this.getAttribute('target') || 'html');
153
+ this._themesBase = this.getAttribute('themes-base') || 'dist/themes';
154
+ this._menu = this.shadowRoot.querySelector('.menu');
155
+ this._toggleBtn = this.shadowRoot.querySelector('.toggle');
156
+ this._pickerBtn = this.shadowRoot.querySelector('.picker');
44
157
 
45
- const applyFromPreference = () => {
46
- if (localStorage.getItem('velin-theme')) return;
47
- if (prefersDarkMq.matches) {
48
- el?.setAttribute('data-velin-theme', 'dark');
49
- this.setAttribute('theme', 'dark');
50
- } else {
51
- el?.removeAttribute('data-velin-theme');
52
- this.removeAttribute('theme');
53
- }
54
- };
55
-
56
- const prefersDark = prefersDarkMq.matches;
57
-
58
- if (stored === 'dark' || (!stored && prefersDark)) {
59
- el?.setAttribute('data-velin-theme', 'dark');
60
- this.setAttribute('theme', 'dark');
61
- }
158
+ this._renderMenu();
159
+ this._initPreference();
62
160
 
63
- prefersDarkMq.addEventListener('change', applyFromPreference);
161
+ this._toggleBtn.addEventListener('click', () => this._toggleDarkMode());
162
+ this._pickerBtn.addEventListener('click', (e) => {
163
+ e.stopPropagation();
164
+ this._toggleMenu();
165
+ });
166
+
167
+ document.addEventListener('click', this._onDocClick);
168
+ this.shadowRoot.addEventListener('keydown', this._onKeyDown);
169
+
170
+ const prefersDarkMq = window.matchMedia('(prefers-color-scheme: dark)');
171
+ prefersDarkMq.addEventListener('change', () => this._applyFromPreference());
64
172
  window.addEventListener('storage', (e) => {
65
- if (e.key !== 'velin-theme' || !el) return;
66
- if (e.newValue === 'dark') {
67
- el.setAttribute('data-velin-theme', 'dark');
68
- this.setAttribute('theme', 'dark');
69
- } else {
70
- el.removeAttribute('data-velin-theme');
71
- this.removeAttribute('theme');
72
- }
173
+ if (e.key === 'velin-theme') this._readStorage();
73
174
  });
175
+ }
176
+
177
+ disconnectedCallback() {
178
+ document.removeEventListener('click', this._onDocClick);
179
+ }
74
180
 
75
- this.shadowRoot.querySelector('button').addEventListener('click', () => {
76
- const isDark = el?.getAttribute('data-velin-theme') === 'dark';
77
- if (isDark) {
78
- el?.removeAttribute('data-velin-theme');
79
- this.removeAttribute('theme');
80
- localStorage.setItem('velin-theme', 'light');
81
- } else {
82
- el?.setAttribute('data-velin-theme', 'dark');
83
- this.setAttribute('theme', 'dark');
84
- localStorage.setItem('velin-theme', 'dark');
85
- }
86
- this.dispatchEvent(new CustomEvent('velin-theme-change', { bubbles: true, detail: { theme: isDark ? 'light' : 'dark', dark: !isDark } }));
181
+ _renderMenu() {
182
+ this._menu.innerHTML = THEMES.map((t) => `
183
+ <li role="none">
184
+ <button type="button" role="menuitem" data-theme="${t.slug}">
185
+ <span class="swatch" aria-hidden="true" data-theme-swatch="${t.slug}"></span>
186
+ ${t.label}
187
+ </button>
188
+ </li>
189
+ `).join('');
190
+ this._menu.removeAttribute('hidden');
191
+ this._menu.querySelectorAll('button[data-theme]').forEach((btn) => {
192
+ btn.addEventListener('click', () => {
193
+ const slug = btn.getAttribute('data-theme');
194
+ this._applyTheme(slug, { persist: true });
195
+ this._closeMenu();
196
+ });
87
197
  });
88
198
  }
199
+
200
+ _toggleMenu() {
201
+ if (this.hasAttribute('menu-open')) this._closeMenu();
202
+ else this._openMenu();
203
+ }
204
+
205
+ _openMenu() {
206
+ this.setAttribute('menu-open', '');
207
+ this._pickerBtn.setAttribute('aria-expanded', 'true');
208
+ this._highlightActive();
209
+ const first = this._menu.querySelector('button[data-theme]');
210
+ if (first) first.focus();
211
+ }
212
+
213
+ _closeMenu() {
214
+ this.removeAttribute('menu-open');
215
+ this._pickerBtn.setAttribute('aria-expanded', 'false');
216
+ }
217
+
218
+ _onDocClick(e) {
219
+ if (!this.hasAttribute('menu-open')) return;
220
+ if (e.composedPath().includes(this)) return;
221
+ this._closeMenu();
222
+ }
223
+
224
+ _onKeyDown(e) {
225
+ if (!this.hasAttribute('menu-open')) return;
226
+ if (e.key === 'Escape') {
227
+ e.preventDefault();
228
+ this._closeMenu();
229
+ this._pickerBtn.focus();
230
+ return;
231
+ }
232
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
233
+ e.preventDefault();
234
+ const items = Array.from(this._menu.querySelectorAll('button[data-theme]'));
235
+ const idx = items.indexOf(this.shadowRoot.activeElement);
236
+ const next = e.key === 'ArrowDown'
237
+ ? items[(idx + 1) % items.length]
238
+ : items[(idx - 1 + items.length) % items.length];
239
+ next?.focus();
240
+ }
241
+ }
242
+
243
+ _highlightActive() {
244
+ const current = this._currentSlug();
245
+ this._menu.querySelectorAll('button[data-theme]').forEach((btn) => {
246
+ const slug = btn.getAttribute('data-theme');
247
+ if (slug === current) btn.setAttribute('aria-current', 'true');
248
+ else btn.removeAttribute('aria-current');
249
+ });
250
+ }
251
+
252
+ _currentSlug() {
253
+ if (!this._target) return '';
254
+ const value = this._target.getAttribute('data-velin-theme');
255
+ if (!value || value === 'light') return '';
256
+ return value;
257
+ }
258
+
259
+ _initPreference() {
260
+ const stored = localStorage.getItem('velin-theme');
261
+ if (stored) {
262
+ this._applyTheme(stored, { persist: false });
263
+ return;
264
+ }
265
+ this._applyFromPreference();
266
+ }
267
+
268
+ _readStorage() {
269
+ const stored = localStorage.getItem('velin-theme');
270
+ this._applyTheme(stored || '', { persist: false });
271
+ }
272
+
273
+ _applyFromPreference() {
274
+ if (localStorage.getItem('velin-theme')) return;
275
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
276
+ this._applyTheme(prefersDark ? 'dark' : '', { persist: false });
277
+ }
278
+
279
+ _applyTheme(slug, { persist }) {
280
+ const normalized = !slug || slug === 'light' ? '' : slug;
281
+ if (!this._target) return;
282
+ if (!normalized) {
283
+ this._target.removeAttribute('data-velin-theme');
284
+ this.removeAttribute('theme');
285
+ } else {
286
+ this._target.setAttribute('data-velin-theme', normalized);
287
+ this.setAttribute('theme', normalized === 'dark' ? 'dark' : normalized);
288
+ ensureThemeStylesheet(normalized, this._themesBase);
289
+ }
290
+ if (persist) {
291
+ if (!normalized) localStorage.removeItem('velin-theme');
292
+ else localStorage.setItem('velin-theme', normalized);
293
+ }
294
+ this._highlightActive();
295
+ this.dispatchEvent(new CustomEvent('velin-theme-change', {
296
+ bubbles: true,
297
+ detail: { theme: normalized || 'light', dark: normalized === 'dark', slug: normalized },
298
+ }));
299
+ }
300
+
301
+ _toggleDarkMode() {
302
+ const current = this._currentSlug();
303
+ const next = current === 'dark' ? '' : 'dark';
304
+ this._applyTheme(next, { persist: true });
305
+ }
89
306
  }
90
307
 
91
308
  customElements.define('velin-theme-toggle', VelinThemeToggle);