@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.
- package/README.de.md +26 -4
- package/README.md +26 -4
- package/cli/blueprint.js +8 -0
- package/cli/blueprints/bottom-nav-mobile.html +17 -0
- package/cli/blueprints/cookie-consent.html +9 -0
- package/cli/blueprints/empty-state.html +5 -0
- package/cli/blueprints/filter-bar.html +15 -0
- package/cli/blueprints/notification-center.html +13 -0
- package/cli/blueprints/onboarding.html +23 -0
- package/cli/blueprints/pricing-table.html +20 -0
- package/cli/blueprints/settings-panel.html +20 -0
- package/cli/index.js +116 -1
- package/cli/layout-audit.js +325 -0
- package/cli/scaffold-recipes.json +70 -0
- package/cli/scaffold.js +155 -0
- package/cli/scanner.js +68 -0
- package/components/index.js +19 -0
- package/components/sanitize.js +29 -3
- package/components/shadow-a11y-styles.js +18 -0
- package/components/velin-announcer.js +35 -0
- package/components/velin-bottom-nav.js +89 -0
- package/components/velin-combobox.js +149 -0
- package/components/velin-command.js +127 -0
- package/components/velin-counter.js +152 -0
- package/components/velin-flip.js +220 -0
- package/components/velin-icon.js +43 -9
- package/components/velin-live-dot.js +85 -0
- package/components/velin-menubar.js +83 -0
- package/components/velin-rating.js +91 -0
- package/components/velin-reveal.js +80 -0
- package/components/velin-segmented-control.js +108 -0
- package/components/velin-sheet.js +107 -0
- package/components/velin-sparkline.js +207 -0
- package/components/velin-theme-toggle.js +277 -60
- package/dist/velinstyle-components.iife.js +1629 -69
- package/dist/velinstyle-components.js +1649 -69
- package/dist/velinstyle-components.min.js +424 -80
- package/dist/velinstyle.css +472 -3
- package/dist/velinstyle.min.css +1 -1
- package/package.json +3 -2
- package/src/a11y/security.css +18 -0
- package/src/base/reset.css +12 -1
- package/src/components/nav.css +152 -151
- package/src/tokens/motion.css +7 -0
- package/src/utilities/animation.css +97 -1
- package/src/utilities/chart-animation.css +101 -0
- package/src/utilities/filter-effects.css +103 -0
- package/src/utilities/safe-area.css +39 -0
- package/src/velinstyle.css +3 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { rovingTabindex } from './focus-manager.js';
|
|
2
|
+
import { escapeHTML } from './sanitize.js';
|
|
3
|
+
import { SHADOW_A11Y_STYLES } from './shadow-a11y-styles.js';
|
|
4
|
+
|
|
5
|
+
const styles = `
|
|
6
|
+
${SHADOW_A11Y_STYLES}
|
|
7
|
+
:host { display: block; }
|
|
8
|
+
.group {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
gap: var(--velin-space-1, 0.25rem);
|
|
11
|
+
padding: var(--velin-space-1, 0.25rem);
|
|
12
|
+
background: var(--velin-color-surface-dim, #eee);
|
|
13
|
+
border-radius: var(--velin-radius-md, 0.5rem);
|
|
14
|
+
}
|
|
15
|
+
::slotted(button) {
|
|
16
|
+
min-inline-size: 2.75rem;
|
|
17
|
+
min-block-size: 2.75rem;
|
|
18
|
+
padding: var(--velin-space-2, 0.5rem) var(--velin-space-4, 1rem);
|
|
19
|
+
border: none;
|
|
20
|
+
border-radius: var(--velin-radius-sm, 0.25rem);
|
|
21
|
+
background: transparent;
|
|
22
|
+
color: var(--velin-color-text-muted, #666);
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
font-size: var(--velin-text-sm, 0.875rem);
|
|
25
|
+
}
|
|
26
|
+
::slotted(button[aria-pressed="true"]) {
|
|
27
|
+
background: var(--velin-color-surface-bright, #fff);
|
|
28
|
+
color: var(--velin-color-text, #111);
|
|
29
|
+
font-weight: var(--velin-weight-semibold, 600);
|
|
30
|
+
box-shadow: var(--velin-shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
class VelinSegmentedControl extends HTMLElement {
|
|
35
|
+
constructor() {
|
|
36
|
+
super();
|
|
37
|
+
this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
38
|
+
this._onClick = this._onClick.bind(this);
|
|
39
|
+
this._onKey = this._onKey.bind(this);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
connectedCallback() {
|
|
43
|
+
const label = escapeHTML(this.getAttribute('aria-label') || 'Segmented control');
|
|
44
|
+
this.shadowRoot.innerHTML = `
|
|
45
|
+
<style>${styles}</style>
|
|
46
|
+
<div class="group" role="group" aria-label="${label}"><slot></slot></div>
|
|
47
|
+
`;
|
|
48
|
+
this.addEventListener('click', this._onClick);
|
|
49
|
+
this.addEventListener('keydown', this._onKey);
|
|
50
|
+
this.shadowRoot.querySelector('slot')?.addEventListener('slotchange', () => this._init());
|
|
51
|
+
this._init();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_getButtons() {
|
|
55
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
56
|
+
return slot ? slot.assignedElements().filter((el) => el.tagName === 'BUTTON') : [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_init() {
|
|
60
|
+
const buttons = this._getButtons();
|
|
61
|
+
const selected = buttons.find((b) => b.hasAttribute('selected')) || buttons[0];
|
|
62
|
+
buttons.forEach((btn, i) => {
|
|
63
|
+
btn.setAttribute('aria-pressed', btn === selected ? 'true' : 'false');
|
|
64
|
+
btn.setAttribute('tabindex', btn === selected ? '0' : '-1');
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_onClick(e) {
|
|
69
|
+
const btn = e.target.closest('button');
|
|
70
|
+
if (!btn || !this.contains(btn)) return;
|
|
71
|
+
this._select(btn);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_select(btn) {
|
|
75
|
+
this._getButtons().forEach((b) => {
|
|
76
|
+
b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');
|
|
77
|
+
b.setAttribute('tabindex', b === btn ? '0' : '-1');
|
|
78
|
+
});
|
|
79
|
+
this.dispatchEvent(new CustomEvent('velin-change', { bubbles: true, detail: { value: btn.value || btn.textContent?.trim() } }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_onKey(e) {
|
|
83
|
+
const buttons = this._getButtons();
|
|
84
|
+
if (!buttons.includes(e.target)) return;
|
|
85
|
+
rovingTabindex(this, buttons, e);
|
|
86
|
+
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
|
|
87
|
+
const focused = buttons.find((b) => b.getAttribute('tabindex') === '0');
|
|
88
|
+
if (focused) this._select(focused);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static get observedAttributes() { return ['aria-label']; }
|
|
93
|
+
|
|
94
|
+
attributeChangedCallback(name) {
|
|
95
|
+
if (name === 'aria-label') {
|
|
96
|
+
const group = this.shadowRoot?.querySelector('.group');
|
|
97
|
+
if (group) group.setAttribute('aria-label', escapeHTML(this.getAttribute('aria-label') || 'Segmented control'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
disconnectedCallback() {
|
|
102
|
+
this.removeEventListener('click', this._onClick);
|
|
103
|
+
this.removeEventListener('keydown', this._onKey);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
customElements.define('velin-segmented-control', VelinSegmentedControl);
|
|
108
|
+
export default VelinSegmentedControl;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { trapFocus, saveFocus, restoreFocus, getFocusableElements, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
|
|
2
|
+
import { escapeHTML } from './sanitize.js';
|
|
3
|
+
import { SHADOW_A11Y_STYLES } from './shadow-a11y-styles.js';
|
|
4
|
+
|
|
5
|
+
const styles = `
|
|
6
|
+
${SHADOW_A11Y_STYLES}
|
|
7
|
+
:host { display: contents; }
|
|
8
|
+
.overlay {
|
|
9
|
+
position: fixed; inset: 0; z-index: var(--velin-z-overlay, 400);
|
|
10
|
+
background: var(--velin-color-overlay, rgba(0,0,0,0.4));
|
|
11
|
+
opacity: 0; visibility: hidden;
|
|
12
|
+
transition: opacity 200ms ease, visibility 200ms ease;
|
|
13
|
+
}
|
|
14
|
+
:host([open]) .overlay { opacity: 1; visibility: visible; }
|
|
15
|
+
.sheet {
|
|
16
|
+
position: fixed; inset-inline: 0; inset-block-end: 0;
|
|
17
|
+
z-index: var(--velin-z-modal, 500);
|
|
18
|
+
max-block-size: min(85vh, 32rem);
|
|
19
|
+
background: var(--velin-color-surface-bright, #fff);
|
|
20
|
+
border-radius: var(--velin-radius-lg, 0.75rem) var(--velin-radius-lg, 0.75rem) 0 0;
|
|
21
|
+
box-shadow: var(--velin-shadow-xl, 0 -4px 24px rgba(0,0,0,0.12));
|
|
22
|
+
display: flex; flex-direction: column;
|
|
23
|
+
transform: translateY(100%);
|
|
24
|
+
transition: transform 250ms ease;
|
|
25
|
+
padding-block-end: env(safe-area-inset-bottom, 0px);
|
|
26
|
+
}
|
|
27
|
+
:host([open]) .sheet { transform: translateY(0); }
|
|
28
|
+
.header {
|
|
29
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
30
|
+
padding: var(--velin-space-4, 1rem) var(--velin-space-5, 1.25rem);
|
|
31
|
+
border-bottom: 1px solid var(--velin-color-border, #ddd);
|
|
32
|
+
}
|
|
33
|
+
.title { font-size: var(--velin-text-lg, 1.25rem); font-weight: 600; margin: 0; }
|
|
34
|
+
.body { flex: 1; overflow-y: auto; padding: var(--velin-space-5, 1.25rem); }
|
|
35
|
+
@media (prefers-reduced-motion: reduce) { .overlay, .sheet { transition: none; } }
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
class VelinSheet extends HTMLElement {
|
|
39
|
+
static get observedAttributes() { return ['open']; }
|
|
40
|
+
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
44
|
+
this._prev = null;
|
|
45
|
+
this._onKey = this._onKey.bind(this);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
connectedCallback() {
|
|
49
|
+
const title = escapeHTML(this.getAttribute('title') || this.getAttribute('label') || '');
|
|
50
|
+
const titleId = 'velin-sheet-title';
|
|
51
|
+
this.shadowRoot.innerHTML = `
|
|
52
|
+
<style>${styles}</style>
|
|
53
|
+
<div class="overlay" part="overlay"></div>
|
|
54
|
+
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="${titleId}" part="sheet">
|
|
55
|
+
<div class="header" part="header">
|
|
56
|
+
<h2 class="title" id="${titleId}">${title}</h2>
|
|
57
|
+
<button class="close-btn" aria-label="Close" part="close">×</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="body" part="body"><slot></slot></div>
|
|
60
|
+
</div>
|
|
61
|
+
`;
|
|
62
|
+
this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => this.close());
|
|
63
|
+
this.shadowRoot.querySelector('.overlay').addEventListener('click', () => this.close());
|
|
64
|
+
if (this.hasAttribute('open')) this._open();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
attributeChangedCallback(name) {
|
|
68
|
+
if (name === 'open') this.hasAttribute('open') ? this._open() : this._close();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
open() { this.setAttribute('open', ''); }
|
|
72
|
+
close() {
|
|
73
|
+
this.removeAttribute('open');
|
|
74
|
+
this.dispatchEvent(new CustomEvent('velin-close', { bubbles: true }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_open() {
|
|
78
|
+
this._prev = saveFocus();
|
|
79
|
+
setBackgroundInert(this);
|
|
80
|
+
document.addEventListener('keydown', this._onKey);
|
|
81
|
+
document.body.style.overflow = 'hidden';
|
|
82
|
+
requestAnimationFrame(() => {
|
|
83
|
+
const f = getFocusableElements(this.shadowRoot);
|
|
84
|
+
if (f.length) f[0].focus();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_close() {
|
|
89
|
+
document.removeEventListener('keydown', this._onKey);
|
|
90
|
+
document.body.style.overflow = '';
|
|
91
|
+
clearBackgroundInert();
|
|
92
|
+
restoreFocus(this._prev);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_onKey(e) {
|
|
96
|
+
if (e.key === 'Escape') { this.close(); return; }
|
|
97
|
+
trapFocus(this.shadowRoot, e);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
disconnectedCallback() {
|
|
101
|
+
document.removeEventListener('keydown', this._onKey);
|
|
102
|
+
document.body.style.overflow = '';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
customElements.define('velin-sheet', VelinSheet);
|
|
107
|
+
export default VelinSheet;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <velin-sparkline values="1,3,2,5,7,6,9" area glow animate="draw">
|
|
3
|
+
*
|
|
4
|
+
* Tiny inline-SVG line chart for KPI tiles, live dashboards, and "this week"
|
|
5
|
+
* trends. Renders into light DOM (no Shadow) so theme tokens cascade in for
|
|
6
|
+
* stroke color via `currentColor`. Animates the stroke draw-in once on mount
|
|
7
|
+
* using the chart-animation utility classes; calling `update(values)` swaps
|
|
8
|
+
* the path and runs a short value-bump on the host.
|
|
9
|
+
*
|
|
10
|
+
* Attributes:
|
|
11
|
+
* values CSV or JSON-array of numbers
|
|
12
|
+
* width viewBox width (default 320)
|
|
13
|
+
* height viewBox height (default 96)
|
|
14
|
+
* min/max clamp range (optional; otherwise derived from values)
|
|
15
|
+
* area truthy enables the gradient area fill
|
|
16
|
+
* glow truthy enables continuous drop-shadow pulse
|
|
17
|
+
* animate "draw" (default) or "none"
|
|
18
|
+
* label accessible label; sets role=img + aria-label
|
|
19
|
+
*
|
|
20
|
+
* Public API:
|
|
21
|
+
* element.update(values: number[]): replaces the path with a new dataset.
|
|
22
|
+
* element.values: number[] reflect getter/setter.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
26
|
+
|
|
27
|
+
function parseValues(raw) {
|
|
28
|
+
if (!raw) return [];
|
|
29
|
+
const trimmed = String(raw).trim();
|
|
30
|
+
if (trimmed.startsWith('[')) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(trimmed);
|
|
33
|
+
return Array.isArray(parsed) ? parsed.map(Number).filter((n) => Number.isFinite(n)) : [];
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return trimmed
|
|
39
|
+
.split(/[\s,]+/)
|
|
40
|
+
.map((s) => Number.parseFloat(s))
|
|
41
|
+
.filter((n) => Number.isFinite(n));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildPoints(values, w, h, min, max) {
|
|
45
|
+
const n = values.length;
|
|
46
|
+
if (n === 0) return [];
|
|
47
|
+
if (n === 1) {
|
|
48
|
+
return [[0, h / 2], [w, h / 2]];
|
|
49
|
+
}
|
|
50
|
+
const range = Math.max(max - min, 1e-6);
|
|
51
|
+
const stepX = w / (n - 1);
|
|
52
|
+
return values.map((v, i) => {
|
|
53
|
+
const x = i * stepX;
|
|
54
|
+
const y = h - ((v - min) / range) * h;
|
|
55
|
+
return [x, y];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pointsToPath(points) {
|
|
60
|
+
if (!points.length) return '';
|
|
61
|
+
return points.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`).join(' ');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pointsToArea(points, h) {
|
|
65
|
+
if (!points.length) return '';
|
|
66
|
+
const line = pointsToPath(points);
|
|
67
|
+
const last = points[points.length - 1][0];
|
|
68
|
+
const first = points[0][0];
|
|
69
|
+
return `${line} L${last.toFixed(2)},${h} L${first.toFixed(2)},${h} Z`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class VelinSparkline extends HTMLElement {
|
|
73
|
+
static get observedAttributes() {
|
|
74
|
+
return ['values', 'width', 'height', 'min', 'max', 'area', 'glow', 'animate', 'label'];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
constructor() {
|
|
78
|
+
super();
|
|
79
|
+
this._values = [];
|
|
80
|
+
this._gradientId = `velin-spark-grad-${Math.random().toString(36).slice(2, 8)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
connectedCallback() {
|
|
84
|
+
this._render();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
attributeChangedCallback() {
|
|
88
|
+
if (this.isConnected) this._render();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get values() {
|
|
92
|
+
return this._values.slice();
|
|
93
|
+
}
|
|
94
|
+
set values(arr) {
|
|
95
|
+
if (!Array.isArray(arr)) return;
|
|
96
|
+
this._values = arr.filter((n) => Number.isFinite(Number(n))).map(Number);
|
|
97
|
+
this.setAttribute('values', this._values.join(','));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
update(values) {
|
|
101
|
+
if (!Array.isArray(values)) return;
|
|
102
|
+
this._values = values.filter((n) => Number.isFinite(Number(n))).map(Number);
|
|
103
|
+
this._render({ tick: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_render({ tick = false } = {}) {
|
|
107
|
+
const w = Number.parseFloat(this.getAttribute('width')) || 320;
|
|
108
|
+
const h = Number.parseFloat(this.getAttribute('height')) || 96;
|
|
109
|
+
const values = this._values.length ? this._values : parseValues(this.getAttribute('values'));
|
|
110
|
+
this._values = values;
|
|
111
|
+
if (!values.length) {
|
|
112
|
+
this.innerHTML = '';
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const minAttr = Number.parseFloat(this.getAttribute('min'));
|
|
116
|
+
const maxAttr = Number.parseFloat(this.getAttribute('max'));
|
|
117
|
+
const min = Number.isFinite(minAttr) ? minAttr : Math.min(...values);
|
|
118
|
+
const max = Number.isFinite(maxAttr) ? maxAttr : Math.max(...values);
|
|
119
|
+
|
|
120
|
+
const wantsArea = this.hasAttribute('area') && this.getAttribute('area') !== 'false';
|
|
121
|
+
const wantsGlow = this.hasAttribute('glow') && this.getAttribute('glow') !== 'false';
|
|
122
|
+
const animate = (this.getAttribute('animate') || 'draw').toLowerCase();
|
|
123
|
+
const label = this.getAttribute('label');
|
|
124
|
+
|
|
125
|
+
const points = buildPoints(values, w, h, min, max);
|
|
126
|
+
const linePath = pointsToPath(points);
|
|
127
|
+
const areaPath = wantsArea ? pointsToArea(points, h) : '';
|
|
128
|
+
|
|
129
|
+
this.innerHTML = '';
|
|
130
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
131
|
+
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
132
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
133
|
+
svg.style.display = 'block';
|
|
134
|
+
svg.style.width = '100%';
|
|
135
|
+
svg.style.height = '100%';
|
|
136
|
+
if (label) {
|
|
137
|
+
svg.setAttribute('role', 'img');
|
|
138
|
+
svg.setAttribute('aria-label', label);
|
|
139
|
+
} else {
|
|
140
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (wantsArea) {
|
|
144
|
+
const defs = document.createElementNS(NS, 'defs');
|
|
145
|
+
const grad = document.createElementNS(NS, 'linearGradient');
|
|
146
|
+
grad.setAttribute('id', this._gradientId);
|
|
147
|
+
grad.setAttribute('x1', '0');
|
|
148
|
+
grad.setAttribute('x2', '0');
|
|
149
|
+
grad.setAttribute('y1', '0');
|
|
150
|
+
grad.setAttribute('y2', '1');
|
|
151
|
+
const stops = [
|
|
152
|
+
['0%', 'currentColor', '0.35'],
|
|
153
|
+
['100%', 'currentColor', '0'],
|
|
154
|
+
];
|
|
155
|
+
stops.forEach(([offset, color, op]) => {
|
|
156
|
+
const stop = document.createElementNS(NS, 'stop');
|
|
157
|
+
stop.setAttribute('offset', offset);
|
|
158
|
+
stop.setAttribute('stop-color', color);
|
|
159
|
+
stop.setAttribute('stop-opacity', op);
|
|
160
|
+
grad.appendChild(stop);
|
|
161
|
+
});
|
|
162
|
+
defs.appendChild(grad);
|
|
163
|
+
svg.appendChild(defs);
|
|
164
|
+
|
|
165
|
+
const area = document.createElementNS(NS, 'path');
|
|
166
|
+
area.setAttribute('d', areaPath);
|
|
167
|
+
area.setAttribute('fill', `url(#${this._gradientId})`);
|
|
168
|
+
area.setAttribute('stroke', 'none');
|
|
169
|
+
area.classList.add('velin-chart-area');
|
|
170
|
+
svg.appendChild(area);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const line = document.createElementNS(NS, 'path');
|
|
174
|
+
line.setAttribute('d', linePath);
|
|
175
|
+
line.setAttribute('fill', 'none');
|
|
176
|
+
line.setAttribute('stroke', 'currentColor');
|
|
177
|
+
line.setAttribute('stroke-width', '2');
|
|
178
|
+
line.setAttribute('stroke-linecap', 'round');
|
|
179
|
+
line.setAttribute('stroke-linejoin', 'round');
|
|
180
|
+
line.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
181
|
+
svg.appendChild(line);
|
|
182
|
+
if (wantsGlow) svg.classList.add('velin-chart-glow');
|
|
183
|
+
|
|
184
|
+
this.appendChild(svg);
|
|
185
|
+
|
|
186
|
+
if (animate !== 'none') {
|
|
187
|
+
const len = (typeof line.getTotalLength === 'function' && line.getTotalLength()) || w;
|
|
188
|
+
line.style.setProperty('--velin-chart-len', len.toFixed(2));
|
|
189
|
+
line.classList.add('velin-chart-line');
|
|
190
|
+
} else {
|
|
191
|
+
line.style.strokeDasharray = '';
|
|
192
|
+
line.style.strokeDashoffset = '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (tick) {
|
|
196
|
+
this.classList.remove('velin-spark-tick');
|
|
197
|
+
void this.offsetWidth;
|
|
198
|
+
this.classList.add('velin-spark-tick');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (typeof customElements !== 'undefined' && !customElements.get('velin-sparkline')) {
|
|
204
|
+
customElements.define('velin-sparkline', VelinSparkline);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default VelinSparkline;
|