@cahyo-dimas/freeday 1.7.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 (130) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/adapters/blazor/freeday-blazor.js +79 -0
  5. package/adapters/react/components/FdyAutocomplete.tsx +159 -0
  6. package/adapters/react/components/FdyCascade.tsx +230 -0
  7. package/adapters/react/components/FdyCfl.tsx +362 -0
  8. package/adapters/react/components/FdyChart.tsx +69 -0
  9. package/adapters/react/components/FdyCombo.tsx +166 -0
  10. package/adapters/react/components/FdyDateRange.tsx +66 -0
  11. package/adapters/react/components/FdyDatepicker.tsx +354 -0
  12. package/adapters/react/index.d.ts +37 -0
  13. package/adapters/react/index.js +9 -0
  14. package/adapters/react/tsconfig.json +14 -0
  15. package/adapters/react/useFreeday.js +44 -0
  16. package/adapters/react/usePopover.ts +64 -0
  17. package/adapters/vue/components/FdyAutocomplete.vue +174 -0
  18. package/adapters/vue/components/FdyCascade.vue +241 -0
  19. package/adapters/vue/components/FdyCfl.vue +354 -0
  20. package/adapters/vue/components/FdyChart.vue +89 -0
  21. package/adapters/vue/components/FdyCombo.vue +232 -0
  22. package/adapters/vue/components/FdyDateRange.vue +78 -0
  23. package/adapters/vue/components/FdyDatepicker.vue +333 -0
  24. package/adapters/vue/index.d.ts +44 -0
  25. package/adapters/vue/index.js +8 -0
  26. package/adapters/vue/useFreeday.js +44 -0
  27. package/adapters/vue/usePopover.ts +78 -0
  28. package/dist/freeday-autocomplete.js +135 -0
  29. package/dist/freeday-breakpoint.js +51 -0
  30. package/dist/freeday-carousel.js +111 -0
  31. package/dist/freeday-cascade.js +256 -0
  32. package/dist/freeday-cfl.js +213 -0
  33. package/dist/freeday-chart.js +403 -0
  34. package/dist/freeday-chip.js +83 -0
  35. package/dist/freeday-datepicker.js +321 -0
  36. package/dist/freeday-datetime.js +59 -0
  37. package/dist/freeday-drawer.js +43 -0
  38. package/dist/freeday-form.js +181 -0
  39. package/dist/freeday-mask.js +114 -0
  40. package/dist/freeday-menu.js +93 -0
  41. package/dist/freeday-popover.js +69 -0
  42. package/dist/freeday-rating.js +50 -0
  43. package/dist/freeday-select.js +194 -0
  44. package/dist/freeday-slider.js +34 -0
  45. package/dist/freeday-stepper.js +90 -0
  46. package/dist/freeday-table.js +475 -0
  47. package/dist/freeday-tabs.js +68 -0
  48. package/dist/freeday-timepicker.js +180 -0
  49. package/dist/freeday-toast.js +84 -0
  50. package/dist/freeday-tree.js +94 -0
  51. package/dist/freeday-upload.js +206 -0
  52. package/dist/freeday.bundle.css +1352 -0
  53. package/dist/freeday.css +1007 -0
  54. package/dist/freeday.js +3632 -0
  55. package/dist/freeday.tokens.css +343 -0
  56. package/package.json +72 -0
  57. package/src/base.css +20 -0
  58. package/src/components/.gitkeep +0 -0
  59. package/src/components/accordion.css +13 -0
  60. package/src/components/alert.css +18 -0
  61. package/src/components/app-shell.css +88 -0
  62. package/src/components/appbar.css +15 -0
  63. package/src/components/autocomplete.css +12 -0
  64. package/src/components/avatar.css +8 -0
  65. package/src/components/badge.css +27 -0
  66. package/src/components/breadcrumb.css +9 -0
  67. package/src/components/breakpoints.css +15 -0
  68. package/src/components/button.css +58 -0
  69. package/src/components/card.css +9 -0
  70. package/src/components/carousel.css +17 -0
  71. package/src/components/cascade.css +36 -0
  72. package/src/components/cfl.css +28 -0
  73. package/src/components/chart.css +55 -0
  74. package/src/components/chip.css +18 -0
  75. package/src/components/combo.css +27 -0
  76. package/src/components/datepicker.css +44 -0
  77. package/src/components/datetimepicker.css +8 -0
  78. package/src/components/description-list.css +6 -0
  79. package/src/components/divider.css +3 -0
  80. package/src/components/drawer.css +25 -0
  81. package/src/components/file-upload.css +34 -0
  82. package/src/components/filterbar.css +25 -0
  83. package/src/components/form-grid.css +8 -0
  84. package/src/components/input-group.css +22 -0
  85. package/src/components/input.css +13 -0
  86. package/src/components/kbd.css +2 -0
  87. package/src/components/menu.css +28 -0
  88. package/src/components/modal.css +22 -0
  89. package/src/components/pagination.css +8 -0
  90. package/src/components/progress.css +7 -0
  91. package/src/components/rating.css +10 -0
  92. package/src/components/selection.css +25 -0
  93. package/src/components/skeleton.css +7 -0
  94. package/src/components/slider.css +12 -0
  95. package/src/components/spinner.css +6 -0
  96. package/src/components/states.css +8 -0
  97. package/src/components/stepper.css +21 -0
  98. package/src/components/table.css +55 -0
  99. package/src/components/tabs.css +10 -0
  100. package/src/components/timeline.css +12 -0
  101. package/src/components/timepicker.css +22 -0
  102. package/src/components/toast.css +17 -0
  103. package/src/components/tooltip.css +6 -0
  104. package/src/components/tree.css +21 -0
  105. package/src/freeday-autocomplete.js +135 -0
  106. package/src/freeday-breakpoint.js +51 -0
  107. package/src/freeday-carousel.js +111 -0
  108. package/src/freeday-cascade.js +256 -0
  109. package/src/freeday-cfl.js +213 -0
  110. package/src/freeday-chart.js +403 -0
  111. package/src/freeday-chip.js +83 -0
  112. package/src/freeday-datepicker.js +321 -0
  113. package/src/freeday-datetime.js +59 -0
  114. package/src/freeday-drawer.js +43 -0
  115. package/src/freeday-form.js +181 -0
  116. package/src/freeday-mask.js +114 -0
  117. package/src/freeday-menu.js +93 -0
  118. package/src/freeday-popover.js +69 -0
  119. package/src/freeday-rating.js +50 -0
  120. package/src/freeday-select.js +194 -0
  121. package/src/freeday-slider.js +34 -0
  122. package/src/freeday-stepper.js +90 -0
  123. package/src/freeday-table.js +475 -0
  124. package/src/freeday-tabs.js +68 -0
  125. package/src/freeday-timepicker.js +180 -0
  126. package/src/freeday-toast.js +84 -0
  127. package/src/freeday-tree.js +94 -0
  128. package/src/freeday-upload.js +206 -0
  129. package/tokens/breakpoints.mjs +4 -0
  130. package/tokens/tokens.json +123 -0
@@ -0,0 +1,403 @@
1
+ /* Freeday — chart renderer (optional, zero-dependency, no external chart lib).
2
+ * Renders sparkline / bar / line / area / donut into an element from data attributes.
3
+ * Auto-inits [data-fdy-chart] once; call FreedayChart.update(el) to re-render after the
4
+ * data attributes change (used by the freeday/vue <FdyChart> wrapper for reactive data).
5
+ *
6
+ * Single-series colour (sparkline / simple bar / single line) via data-fdy-color references a
7
+ * semantic token by name (primary, accent, success, warning, danger, info). Multi-series
8
+ * defaults draw from the validated categorical --chart-1..8 palette in fixed order (never
9
+ * cycled, 8-series cap); pass data-fdy-colors to override with semantic names instead.
10
+ * Cartesian charts (line/area, multi-series/stacked bar) draw themed axes from --chart-grid /
11
+ * --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency).
12
+ * Charts are an enhancement — put a table/text fallback inside the element and it will be
13
+ * replaced on render; give the element role="img" + aria-label.
14
+ *
15
+ * Sparkline: <span data-fdy-chart="sparkline" data-values="4,6,5,8,7,10" data-fdy-color="primary">
16
+ * Bar: <div data-fdy-chart="bar" data-values="12,19,8,15" data-labels="Sen,Sel,Rab,Kam">
17
+ * Donut: <div data-fdy-chart="donut" data-values="45,30,25" data-labels="Lunas,Tertunda,Batal">
18
+ * Line/area/multi bar (axes + legend + formatting):
19
+ * <div data-fdy-chart="line" data-labels="Jan,Feb,Mar,Apr" data-fdy-format="percent"
20
+ * data-series='[{"label":"Posted","values":[12,19,8,15]},{"label":"Draft","values":[4,6,5,8]}]'>
21
+ * <div data-fdy-chart="bar" data-fdy-stacked data-labels="..." data-series='[...]'>
22
+ */
23
+ (function () {
24
+ 'use strict';
25
+
26
+ var NS = 'http://www.w3.org/2000/svg';
27
+
28
+ // Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
29
+ // (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
30
+ function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
31
+ function colorVar(name) { return 'var(--color-' + (name || 'primary') + ')'; }
32
+
33
+ function nums(el, attr) {
34
+ var v = el.getAttribute(attr);
35
+ return v ? v.split(',').map(function (s) { return parseFloat(s.trim()); }).filter(function (n) { return !isNaN(n); }) : [];
36
+ }
37
+ function strs(el, attr) {
38
+ var v = el.getAttribute(attr);
39
+ return v ? v.split(',').map(function (s) { return s.trim(); }) : [];
40
+ }
41
+ function ensureImg(el) { if (!el.hasAttribute('role')) el.setAttribute('role', 'img'); }
42
+ function svgEl(name) { return document.createElementNS(NS, name); }
43
+
44
+ // Multi-series model: data-series (JSON) wins; else the data-values single-series shortcut.
45
+ function getSeries(el) {
46
+ var raw = el.getAttribute('data-series');
47
+ if (raw) {
48
+ try {
49
+ var arr = JSON.parse(raw);
50
+ if (Array.isArray(arr)) return arr.map(function (s) {
51
+ return { label: s && s.label != null ? String(s.label) : '', values: ((s && s.values) || []).map(Number) };
52
+ });
53
+ } catch (e) { /* fall through to data-values */ }
54
+ }
55
+ var vals = nums(el, 'data-values');
56
+ return vals.length ? [{ label: el.getAttribute('data-fdy-label') || '', values: vals }] : [];
57
+ }
58
+
59
+ // Value formatter for ticks + tooltips.
60
+ function makeFormat(kind) {
61
+ if (kind === 'percent') return function (v) { return (Math.round(v * 10) / 10) + '%'; };
62
+ if (kind === 'currency' || kind === 'usd') return function (v) { return '$' + Math.round(v).toLocaleString('en-US'); };
63
+ return function (v) { return (Math.round(v * 100) / 100).toLocaleString(); };
64
+ }
65
+ // Round a positive number up to a "nice" axis maximum (1/2/2.5/5/10 * 10^n).
66
+ function niceCeil(x) {
67
+ if (x <= 0) return 1;
68
+ var exp = Math.floor(Math.log10(x)), base = Math.pow(10, exp), f = x / base;
69
+ var nf = f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10;
70
+ return nf * base;
71
+ }
72
+
73
+ // ---- Interactive tooltip (Chart.js-style hover readout; purely visual) ----
74
+ function makeTip(container) {
75
+ if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
76
+ var tip = document.createElement('div');
77
+ tip.className = 'fdy-chart__tip';
78
+ tip.setAttribute('aria-hidden', 'true');
79
+ container.appendChild(tip);
80
+ return tip;
81
+ }
82
+ function showTip(tip, container, clientX, clientY, text) {
83
+ var r = container.getBoundingClientRect();
84
+ tip.textContent = text;
85
+ tip.style.left = (clientX - r.left) + 'px';
86
+ tip.style.top = (clientY - r.top) + 'px';
87
+ tip.classList.add('is-visible');
88
+ }
89
+ function hideTip(tip) { tip.classList.remove('is-visible'); }
90
+
91
+ // Donut sector geometry (SVG path for a ring slice between two radii/angles).
92
+ function polar(cx, cy, r, deg) { var a = (deg - 90) * Math.PI / 180; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }
93
+ function sectorPath(cx, cy, R, r, a0, a1) {
94
+ var large = (a1 - a0) > 180 ? 1 : 0;
95
+ var p0 = polar(cx, cy, R, a0), p1 = polar(cx, cy, R, a1), p2 = polar(cx, cy, r, a1), p3 = polar(cx, cy, r, a0);
96
+ return 'M' + p0[0].toFixed(2) + ' ' + p0[1].toFixed(2)
97
+ + ' A' + R + ' ' + R + ' 0 ' + large + ' 1 ' + p1[0].toFixed(2) + ' ' + p1[1].toFixed(2)
98
+ + ' L' + p2[0].toFixed(2) + ' ' + p2[1].toFixed(2)
99
+ + ' A' + r + ' ' + r + ' 0 ' + large + ' 0 ' + p3[0].toFixed(2) + ' ' + p3[1].toFixed(2) + ' Z';
100
+ }
101
+
102
+ function renderSparkline(el) {
103
+ var vals = nums(el, 'data-values');
104
+ if (vals.length < 2) return;
105
+ el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
106
+ var W = 128, H = 36, P = 3;
107
+ var min = Math.min.apply(null, vals), max = Math.max.apply(null, vals), span = (max - min) || 1;
108
+ var pts = vals.map(function (v, i) {
109
+ var x = P + (i / (vals.length - 1)) * (W - 2 * P);
110
+ var y = H - P - ((v - min) / span) * (H - 2 * P);
111
+ return [x, y];
112
+ });
113
+ var line = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
114
+ var area = P + ',' + H + ' ' + line + ' ' + (W - P) + ',' + H;
115
+ var last = pts[pts.length - 1];
116
+ el.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '" aria-hidden="true">'
117
+ + '<polygon class="fdy-sparkline__area" points="' + area + '"/>'
118
+ + '<polyline class="fdy-sparkline__line" points="' + line + '" vector-effect="non-scaling-stroke"/>'
119
+ + '<circle class="fdy-sparkline__dot" cx="' + last[0].toFixed(1) + '" cy="' + last[1].toFixed(1) + '" r="2.5"/>'
120
+ + '</svg>';
121
+ ensureImg(el);
122
+ }
123
+
124
+ // Simple single-series flex bar (legacy, unchanged look). Rich/multi-series/stacked bars
125
+ // with axes go through renderCartesian instead.
126
+ function renderBars(el) {
127
+ var vals = nums(el, 'data-values');
128
+ if (!vals.length) return;
129
+ var labels = strs(el, 'data-labels');
130
+ el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
131
+ var max = Math.max.apply(null, vals) || 1;
132
+ el.innerHTML = '';
133
+ var tip = makeTip(el);
134
+ vals.forEach(function (v, i) {
135
+ var col = document.createElement('div'); col.className = 'fdy-bars__col';
136
+ var track = document.createElement('div'); track.className = 'fdy-bars__track';
137
+ var val = document.createElement('span'); val.className = 'fdy-bars__val'; val.textContent = v;
138
+ var bar = document.createElement('div'); bar.className = 'fdy-bars__bar';
139
+ bar.style.height = Math.max(2, (v / max) * 100) + '%';
140
+ var text = (labels[i] || ('#' + (i + 1))) + ': ' + v;
141
+ track.appendChild(val); track.appendChild(bar);
142
+ col.appendChild(track);
143
+ if (labels[i]) {
144
+ var lab = document.createElement('div'); lab.className = 'fdy-bars__label'; lab.textContent = labels[i];
145
+ col.appendChild(lab);
146
+ }
147
+ // Per-column hover so re-render (update) never stacks document/element listeners.
148
+ col.addEventListener('mousemove', function (e) { showTip(tip, el, e.clientX, e.clientY, text); });
149
+ col.addEventListener('mouseleave', function () { hideTip(tip); });
150
+ el.appendChild(col);
151
+ });
152
+ ensureImg(el);
153
+ }
154
+
155
+ // ---- Cartesian engine: line / area / grouped+stacked bar with themed axes + legend ----
156
+ function renderCartesian(el, type) {
157
+ var series = getSeries(el);
158
+ if (!series.length) return;
159
+ var labels = strs(el, 'data-labels');
160
+ var fmt = makeFormat(el.getAttribute('data-fdy-format'));
161
+ var stacked = type === 'bar' && el.hasAttribute('data-fdy-stacked');
162
+ var overrides = strs(el, 'data-fdy-colors');
163
+ var singleColor = el.getAttribute('data-fdy-color');
164
+ function colorFor(i) {
165
+ if (overrides[i]) return colorVar(overrides[i]);
166
+ if (series.length === 1 && singleColor) return colorVar(singleColor);
167
+ return chartSlotVar(i);
168
+ }
169
+
170
+ var n = series.reduce(function (m, s) { return Math.max(m, s.values.length); }, 0);
171
+ if (!n) return;
172
+
173
+ // y-domain (baseline at 0 unless data dips below).
174
+ var dataMax = -Infinity, dataMin = Infinity;
175
+ if (stacked) {
176
+ for (var c = 0; c < n; c++) {
177
+ var sum = 0; series.forEach(function (s) { sum += (s.values[c] || 0); });
178
+ if (sum > dataMax) dataMax = sum; if (sum < dataMin) dataMin = sum;
179
+ }
180
+ } else {
181
+ series.forEach(function (s) { s.values.forEach(function (v) { if (v > dataMax) dataMax = v; if (v < dataMin) dataMin = v; }); });
182
+ }
183
+ if (dataMax === -Infinity) dataMax = 1;
184
+ var base = Math.min(0, dataMin);
185
+ var top = niceCeil(dataMax > base ? dataMax : base + 1);
186
+ if (top <= base) top = base + 1;
187
+
188
+ var W = 320, H = 180, PL = 38, PR = 10, PT = 10, PB = 22;
189
+ var plotW = W - PL - PR, plotH = H - PT - PB;
190
+ function mapY(v) { return PT + plotH * (1 - (v - base) / (top - base)); }
191
+ var slot = plotW / n;
192
+ function xCenter(i) { return PL + slot * (i + 0.5); } // bar / band centre
193
+ function xLine(i) { return PL + (n === 1 ? plotW / 2 : (i / (n - 1)) * plotW); }
194
+
195
+ var svg = svgEl('svg');
196
+ svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
197
+ svg.setAttribute('aria-hidden', 'true');
198
+
199
+ // y gridlines + formatted ticks (bottom line at `base` doubles as the zero baseline).
200
+ var TICKS = 4;
201
+ for (var k = 0; k <= TICKS; k++) {
202
+ var val = base + (top - base) * k / TICKS;
203
+ var gy = mapY(val);
204
+ var gl = svgEl('line');
205
+ gl.setAttribute('x1', PL); gl.setAttribute('x2', PL + plotW);
206
+ gl.setAttribute('y1', gy.toFixed(1)); gl.setAttribute('y2', gy.toFixed(1));
207
+ gl.setAttribute('class', 'fdy-chart-xy__grid');
208
+ svg.appendChild(gl);
209
+ var tk = svgEl('text');
210
+ tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + 3).toFixed(1));
211
+ tk.setAttribute('text-anchor', 'end'); tk.setAttribute('class', 'fdy-chart-xy__tick');
212
+ tk.textContent = fmt(val);
213
+ svg.appendChild(tk);
214
+ }
215
+
216
+ // x category labels with autoskip.
217
+ var maxTicks = Math.max(2, Math.floor(plotW / 40)), step = Math.ceil(n / maxTicks);
218
+ for (var xi = 0; xi < n; xi++) {
219
+ if (xi % step !== 0 && xi !== n - 1) continue;
220
+ var lx = (type === 'bar') ? xCenter(xi) : xLine(xi);
221
+ var xt = svgEl('text');
222
+ xt.setAttribute('x', lx.toFixed(1)); xt.setAttribute('y', H - 6);
223
+ xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
224
+ xt.textContent = labels[xi] || ('#' + (xi + 1));
225
+ svg.appendChild(xt);
226
+ }
227
+
228
+ if (type === 'bar') {
229
+ var S = series.length;
230
+ var groupW = slot * 0.72;
231
+ var barW = stacked ? slot * 0.55 : groupW / S;
232
+ var acc = []; // running top per category (stacked)
233
+ for (var ci = 0; ci < n; ci++) acc[ci] = base;
234
+ series.forEach(function (s, si) {
235
+ s.values.forEach(function (v, ci2) {
236
+ if (v == null || isNaN(v)) return;
237
+ var x, y0, y1;
238
+ if (stacked) {
239
+ x = xCenter(ci2) - barW / 2;
240
+ y0 = mapY(acc[ci2]); y1 = mapY(acc[ci2] + v); acc[ci2] += v;
241
+ } else {
242
+ x = PL + slot * ci2 + (slot - groupW) / 2 + barW * si;
243
+ y0 = mapY(base); y1 = mapY(v);
244
+ }
245
+ var rect = svgEl('rect');
246
+ rect.setAttribute('x', x.toFixed(1)); rect.setAttribute('width', Math.max(1, barW - 1).toFixed(1));
247
+ rect.setAttribute('y', Math.min(y0, y1).toFixed(1)); rect.setAttribute('height', Math.max(0.5, Math.abs(y1 - y0)).toFixed(1));
248
+ rect.setAttribute('rx', '1.5'); rect.setAttribute('class', 'fdy-chart-xy__bar');
249
+ rect.style.fill = colorFor(si);
250
+ svg.appendChild(rect);
251
+ });
252
+ });
253
+ } else {
254
+ // line / area: fill (area) beneath the stroke, then a small dot per vertex.
255
+ series.forEach(function (s, si) {
256
+ var pts = [];
257
+ for (var i = 0; i < n; i++) { var v = s.values[i]; if (v == null || isNaN(v)) continue; pts.push([xLine(i), mapY(v)]); }
258
+ if (!pts.length) return;
259
+ var d = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
260
+ if (type === 'area') {
261
+ var y0 = mapY(base).toFixed(1);
262
+ var poly = svgEl('polygon');
263
+ poly.setAttribute('points', pts[0][0].toFixed(1) + ',' + y0 + ' ' + d + ' ' + pts[pts.length - 1][0].toFixed(1) + ',' + y0);
264
+ poly.setAttribute('class', 'fdy-chart-xy__area');
265
+ poly.style.fill = colorFor(si);
266
+ svg.appendChild(poly);
267
+ }
268
+ if (pts.length >= 2) {
269
+ var pl = svgEl('polyline');
270
+ pl.setAttribute('points', d); pl.setAttribute('class', 'fdy-chart-xy__line');
271
+ pl.style.stroke = colorFor(si);
272
+ svg.appendChild(pl);
273
+ }
274
+ pts.forEach(function (p) {
275
+ var dot = svgEl('circle');
276
+ dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '2.2');
277
+ dot.setAttribute('class', 'fdy-chart-xy__dot'); dot.style.fill = colorFor(si);
278
+ svg.appendChild(dot);
279
+ });
280
+ });
281
+ }
282
+
283
+ // Assemble: optional legend on top, plot below, tooltip layer last.
284
+ var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
285
+ var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
286
+ el.classList.add('fdy-chart-xy');
287
+ el.innerHTML = '';
288
+ if (showLegend) {
289
+ var legend = document.createElement('ul');
290
+ legend.className = 'fdy-chart__legend fdy-chart__legend--row';
291
+ series.forEach(function (s, si) {
292
+ var li = document.createElement('li');
293
+ var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
294
+ li.appendChild(sw);
295
+ li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
296
+ legend.appendChild(li);
297
+ });
298
+ el.appendChild(legend);
299
+ }
300
+ var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
301
+ plot.appendChild(svg);
302
+ el.appendChild(plot);
303
+
304
+ // Hover: one transparent band per category → readout of every series at that x.
305
+ var tip = makeTip(el);
306
+ var bandW = (type === 'bar') ? slot : (n > 1 ? plotW / (n - 1) : plotW);
307
+ for (var b = 0; b < n; b++) {
308
+ var bx = (type === 'bar') ? (PL + slot * b) : (xLine(b) - bandW / 2);
309
+ var band = svgEl('rect');
310
+ band.setAttribute('x', Math.max(0, bx).toFixed(1)); band.setAttribute('y', PT);
311
+ band.setAttribute('width', bandW.toFixed(1)); band.setAttribute('height', plotH);
312
+ band.setAttribute('class', 'fdy-chart-xy__band');
313
+ var parts = series.map(function (s) {
314
+ var v = s.values[b];
315
+ return (v == null || isNaN(v)) ? null : (series.length > 1 ? s.label + ' ' : '') + fmt(v);
316
+ }).filter(Boolean);
317
+ var text = (labels[b] || ('#' + (b + 1))) + (parts.length ? ' · ' + parts.join(', ') : '');
318
+ band.addEventListener('mousemove', (function (t) { return function (e) { showTip(tip, el, e.clientX, e.clientY, t); }; })(text));
319
+ band.addEventListener('mouseleave', function () { hideTip(tip); });
320
+ svg.appendChild(band);
321
+ }
322
+ ensureImg(el);
323
+ }
324
+
325
+ function renderDonut(el) {
326
+ var vals = nums(el, 'data-values');
327
+ if (!vals.length) return;
328
+ var labels = strs(el, 'data-labels');
329
+ var colors = strs(el, 'data-fdy-colors');
330
+ var total = vals.reduce(function (a, b) { return a + b; }, 0) || 1;
331
+ var stops = [], acc = 0;
332
+ var colorAt = function (i) { return colors[i] ? colorVar(colors[i]) : chartSlotVar(i); };
333
+ vals.forEach(function (v, i) {
334
+ var from = (acc / total) * 360, to = ((acc + v) / total) * 360;
335
+ acc += v;
336
+ stops.push(colorAt(i) + ' ' + from.toFixed(1) + 'deg ' + to.toFixed(1) + 'deg');
337
+ });
338
+ el.innerHTML = '';
339
+ var ring = document.createElement('div'); ring.className = 'fdy-donut__ring';
340
+ ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
341
+ var center = document.createElement('div'); center.className = 'fdy-donut__center';
342
+ var centerLabel = el.getAttribute('data-fdy-center');
343
+ center.innerHTML = centerLabel ? '<b></b>' : '<b></b><span>Total</span>';
344
+ center.querySelector('b').textContent = centerLabel != null ? centerLabel : String(total);
345
+ ring.appendChild(center);
346
+ var svg = svgEl('svg');
347
+ svg.setAttribute('class', 'fdy-donut__hit');
348
+ svg.setAttribute('viewBox', '0 0 128 128');
349
+ var ringTip = makeTip(ring), acc2 = 0;
350
+ vals.forEach(function (v, i) {
351
+ var a0 = (acc2 / total) * 360, a1 = ((acc2 + v) / total) * 360; acc2 += v;
352
+ var path = svgEl('path');
353
+ path.setAttribute('class', 'fdy-donut__seg');
354
+ path.setAttribute('d', sectorPath(64, 64, 64, 33, a0, a1));
355
+ var text = (labels[i] || ('#' + (i + 1))) + ': ' + v + ' (' + Math.round((v / total) * 100) + '%)';
356
+ path.addEventListener('mousemove', function (e) { showTip(ringTip, ring, e.clientX, e.clientY, text); });
357
+ path.addEventListener('mouseleave', function () { hideTip(ringTip); });
358
+ svg.appendChild(path);
359
+ });
360
+ ring.appendChild(svg);
361
+ var legend = document.createElement('ul'); legend.className = 'fdy-chart__legend';
362
+ vals.forEach(function (v, i) {
363
+ var li = document.createElement('li');
364
+ var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorAt(i);
365
+ li.appendChild(sw);
366
+ li.appendChild(document.createTextNode((labels[i] || ('#' + (i + 1))) + ' — ' + Math.round((v / total) * 100) + '%'));
367
+ legend.appendChild(li);
368
+ });
369
+ el.appendChild(ring); el.appendChild(legend);
370
+ ensureImg(el);
371
+ }
372
+
373
+ // Full (re-)render from current data attributes. Idempotent — safe to call repeatedly.
374
+ function renderChart(el) {
375
+ var type = el.getAttribute('data-fdy-chart');
376
+ if (type === 'sparkline') renderSparkline(el);
377
+ else if (type === 'donut') renderDonut(el);
378
+ else if (type === 'line' || type === 'area') renderCartesian(el, type);
379
+ else if (type === 'bar') {
380
+ if (el.getAttribute('data-series') || el.hasAttribute('data-fdy-stacked') || el.hasAttribute('data-fdy-axes')) renderCartesian(el, 'bar');
381
+ else renderBars(el);
382
+ }
383
+ }
384
+
385
+ // Auto-init renders once; explicit update() re-renders after data changes.
386
+ function initChart(el) {
387
+ if (el.dataset.fdyChartReady === '1') return;
388
+ el.dataset.fdyChartReady = '1';
389
+ renderChart(el);
390
+ }
391
+
392
+ function initAll(context) {
393
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-chart]'), initChart);
394
+ }
395
+
396
+ if (document.readyState === 'loading') {
397
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
398
+ } else {
399
+ initAll();
400
+ }
401
+
402
+ window.FreedayChart = { init: initChart, initAll: initAll, update: renderChart };
403
+ })();
@@ -0,0 +1,83 @@
1
+ /* Freeday — chip enhancer (optional, zero-dependency).
2
+ * Two behaviours, both progressive enhancement:
3
+ *
4
+ * 1. Removable chips — any .fdy-chip__remove button removes its chip and emits a
5
+ * bubbling "fdy-chip-remove" (detail {value}).
6
+ * 2. Choice / filter groups — a [data-fdy-chips] container of interactive chips
7
+ * (.fdy-chip--choice / .fdy-chip--filter, each a <button>) toggles aria-pressed
8
+ * on click. Add data-single for one-at-a-time (radio-like) selection; filter
9
+ * chips get a leading check when pressed. Emits "fdy-chip-change" on the group
10
+ * (detail {value, pressed, selected}).
11
+ */
12
+ (function () {
13
+ 'use strict';
14
+
15
+ var CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>';
16
+
17
+ function initRemove(btn) {
18
+ if (btn.dataset.fdyChipRmReady === '1') return;
19
+ btn.dataset.fdyChipRmReady = '1';
20
+ btn.addEventListener('click', function () {
21
+ var chip = btn.closest('.fdy-chip');
22
+ if (!chip) return;
23
+ var value = chip.getAttribute('data-value');
24
+ if (!value) {
25
+ // Derive the label from the chip text, minus the remove button glyph.
26
+ var clone = chip.cloneNode(true);
27
+ var rm = clone.querySelector('.fdy-chip__remove');
28
+ if (rm) rm.remove();
29
+ value = (clone.textContent || '').trim();
30
+ }
31
+ chip.dispatchEvent(new CustomEvent('fdy-chip-remove', { bubbles: true, detail: { value: value } }));
32
+ chip.remove();
33
+ });
34
+ }
35
+
36
+ function initGroup(group) {
37
+ if (group.dataset.fdyChipsReady === '1') return;
38
+ group.dataset.fdyChipsReady = '1';
39
+ var single = group.hasAttribute('data-single');
40
+ if (!group.getAttribute('role')) group.setAttribute('role', 'group');
41
+ var groupLabel = group.getAttribute('data-label');
42
+ if (groupLabel && !group.getAttribute('aria-label')) group.setAttribute('aria-label', groupLabel);
43
+
44
+ var chips = Array.prototype.slice.call(group.querySelectorAll('.fdy-chip--choice, .fdy-chip--filter'));
45
+ function valueOf(chip) { return chip.getAttribute('data-value') || (chip.textContent || '').trim(); }
46
+ function selected() {
47
+ return chips.filter(function (c) { return c.getAttribute('aria-pressed') === 'true'; }).map(valueOf);
48
+ }
49
+
50
+ chips.forEach(function (chip) {
51
+ if (!chip.hasAttribute('aria-pressed')) chip.setAttribute('aria-pressed', 'false');
52
+ if (chip.classList.contains('fdy-chip--filter') && !chip.querySelector('.fdy-chip__check')) {
53
+ var check = document.createElement('span');
54
+ check.className = 'fdy-chip__check';
55
+ check.innerHTML = CHECK;
56
+ chip.insertBefore(check, chip.firstChild);
57
+ }
58
+ chip.addEventListener('click', function () {
59
+ var wasPressed = chip.getAttribute('aria-pressed') === 'true';
60
+ if (single) chips.forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
61
+ chip.setAttribute('aria-pressed', wasPressed ? 'false' : 'true');
62
+ group.dispatchEvent(new CustomEvent('fdy-chip-change', {
63
+ bubbles: true,
64
+ detail: { value: valueOf(chip), pressed: !wasPressed, selected: selected() }
65
+ }));
66
+ });
67
+ });
68
+ }
69
+
70
+ function initAll(context) {
71
+ var root = context || document;
72
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-chips]'), initGroup);
73
+ Array.prototype.forEach.call(root.querySelectorAll('.fdy-chip__remove'), initRemove);
74
+ }
75
+
76
+ if (document.readyState === 'loading') {
77
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
78
+ } else {
79
+ initAll();
80
+ }
81
+
82
+ window.FreedayChip = { initGroup: initGroup, initRemove: initRemove, initAll: initAll };
83
+ })();