@cahyo-dimas/freeday 1.50.0 → 1.51.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/CHANGELOG.md CHANGED
@@ -3,6 +3,51 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.51.0] — 2026-08-21
7
+ ### Fixed
8
+ - **A cartesian chart's axis text and dots grew with the container** (#042). `renderCartesian` built
9
+ a fixed `viewBox="0 0 320 180"` and let `.fdy-chart-xy__plot` stretch it to `width:100%`, so every
10
+ size the renderer expressed in user units was multiplied by `plotWidth / 320`. `font-size:9px` on
11
+ a tick is not nine screen pixels at any width but 320px: measured on a real console it rendered
12
+ 13px at a 350px plot and **81px at a 2232px** one, making the chart's smallest, most secondary
13
+ text the largest type on the page. `r="2.2"` did the same to the dots — 4.8px across at 350px,
14
+ 30.7px at 2232px. The chart now measures `__plot` and sizes the `viewBox` to it in CSS pixels, so
15
+ one user unit is one pixel and every declared size means what it says at every width; a
16
+ `ResizeObserver` repaints it when the box changes (a collapsing sidebar moves a plot ~300px at a
17
+ fixed viewport, so this is the common case, not an edge case). A chart that measures 0 — detached,
18
+ or inside `display:none` — falls back to the old box and repaints when it is laid out.
19
+ - **A currency y-axis was clipped by a gutter sized for a percent one** (#042). `PL` was a constant
20
+ 38 user units, which was ~4 characters at one scale and something else at every other. It is now
21
+ derived from the widest formatted tick the axis will actually draw, capped at 35% of the plot.
22
+ ### Changed
23
+ - **Axis type is a real CSS size**, set by `--fdy-chart-tick-size` on `.fdy-chart-xy` (default
24
+ `var(--text-xs)`). `font-size` moved off `.fdy-chart-xy__tick` / `__xlabel` onto
25
+ `.fdy-chart-xy__plot`, which the SVG text inherits — one computed read gives the renderer the real
26
+ pixel size it needs for the y-gutter and for the x-label autoskip, so an override flows to the
27
+ text *and* to the geometry around it. Before this, chart text could not be restyled at all: any
28
+ value set was multiplied by a scale the consumer could not see or compute.
29
+ The dot radius went 2.2 → 3 and the bar corner 1.5 → 2, both retuned against a 1:1 scale.
30
+ ### Docs
31
+ - **`COMPONENTS.md` claimed a `<table>` fallback that no rendered chart has** (#041). It said every
32
+ chart is `role="img"` + `aria-label` "with a `<table>` fallback inside the element"; the renderer
33
+ builds no table and wipes the element on every render path. Worse, `role="img"` makes descendants
34
+ presentational, so the bar values, ticks and legend the renderer *does* draw are not exposed
35
+ either — the author's `aria-label` is the entire text alternative, and the docs now say so and ask
36
+ for one that carries the numbers. The four dead fallback tables in `docs/index.html` and
37
+ `docs/reference-screen.html`, which no reader has ever reached, are gone with it.
38
+ - **The kit's own chart sizing is written down** (#041): `.fdy-chart-xy__plot` is
39
+ `aspect-ratio:16/9` + `min-height:8rem`, `.fdy-bars` a fixed `9rem`, `.fdy-sparkline` an
40
+ inline-block `8rem × 2.25rem` — with the note to override those rather than the chart root, since
41
+ a root `height` fights `aspect-ratio` instead of setting it.
42
+ ### Added — guards
43
+ - **`browser/chart-scale.mjs`** — the axis label box and dot diameter are measured at plot widths of
44
+ 350 / 696 / 1400 / 2232px and must not vary by more than a pixel, must stay smaller than body
45
+ text, and the `viewBox` must equal the measured plot. A second spec widens a container and asserts
46
+ the repaint. This class of bug is invisible to a source read: `font-size:9px` and `r="2.2"` look
47
+ like correct sizes, and the one thing in the plot that *was* protected —
48
+ `vector-effect:non-scaling-stroke` on the line — is what made the other three look protected too.
49
+ Only a real layout tells them apart.
50
+
6
51
  ## [1.50.0] — 2026-08-20
7
52
  ### Fixed
8
53
  - **A toast raised from inside a modal was painted behind it** (#027). `.fdy-toast-region` carried
package/COMPONENTS.md CHANGED
@@ -858,7 +858,20 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
858
858
  validated categorical palette `--chart-1`…`--chart-8`.
859
859
  - Format: `data-fdy-format="number|percent|currency"`; legend `data-fdy-legend` (`none` to drop);
860
860
  axes `data-fdy-axes`
861
- - A11y: every chart is `role="img"` + `aria-label`, with a `<table>` fallback inside the element.
861
+ - Sizing is already set by the kit override **these**, never a `height` on the chart root (a
862
+ root height fights `aspect-ratio` instead of setting it): `.fdy-chart-xy__plot` is `width:100%`
863
+ + `aspect-ratio:16/9` + `min-height:8rem`; `.fdy-bars` is a fixed `height:9rem`; `.fdy-sparkline`
864
+ is an inline-block `8rem × 2.25rem`.
865
+ - Axis type: `--fdy-chart-tick-size` (default `var(--text-xs)`) sets the y-tick and x-label size on
866
+ `.fdy-chart-xy`. It is a real CSS size — a cartesian chart's `viewBox` is measured to its plot, so
867
+ one user unit is one pixel — and the renderer reads it back to size the y-gutter and to decide how
868
+ many x-labels fit. Set the token rather than `font-size` on `__tick`/`__xlabel`, or the gutter and
869
+ the autoskip will still be computed from the old size.
870
+ - A11y: every chart is `role="img"` + an **author-supplied** `aria-label`, and that label is the
871
+ *entire* text alternative — write one that carries the numbers, not just the shape. `role="img"`
872
+ makes descendants presentational, so rendered bar values, axis ticks and legends are not exposed
873
+ either; any `<table>`/text placed inside the element is **pre-render only** and is replaced on
874
+ render. Good: `aria-label="Monthly posting trend: Posted 62% → 88%, Draft 24% → 16%"`.
862
875
  - Internal parts (set by JS, useful for overrides): `.fdy-chart__legend` (+`--row`) `__swatch`
863
876
  `__tip`; `.fdy-bars__col` `__bar` `__track` `__label` `__val`; `.fdy-chart-xy__plot` `__grid`
864
877
  `__tick` `__xlabel` `__line` `__area` `__bar` `__dot` `__band`; `.fdy-donut__ring` `__seg`
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.34.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.34.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.51.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.51.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs — the UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.34.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.34.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.51.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.51.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit — one source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -10,7 +10,9 @@
10
10
  * (var(--color-<name>)) OR a slot "chart-1".."chart-8" (var(--chart-N)) to pin a series to the
11
11
  * validated palette. Both data-fdy-color and data-fdy-colors accept the chart-N form.
12
12
  * Cartesian charts (line/area, multi-series/stacked bar) draw themed axes from --chart-grid /
13
- * --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency).
13
+ * --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency). Their
14
+ * viewBox is sized to the measured plot (1 user unit = 1 CSS pixel) and repainted from a
15
+ * ResizeObserver, so axis type stays in real pixels and is set in CSS via --fdy-chart-tick-size.
14
16
  * Charts are an enhancement — put a table/text fallback inside the element and it will be
15
17
  * replaced on render; give the element role="img" + aria-label.
16
18
  *
@@ -50,6 +52,18 @@
50
52
  function ensureImg(el) { if (!el.hasAttribute('role')) el.setAttribute('role', 'img'); }
51
53
  function svgEl(name) { return document.createElementNS(NS, name); }
52
54
 
55
+ // Cartesian charts size their viewBox to the measured plot, so a width change needs a repaint.
56
+ // Guarded on width alone: a render can change the element's own height (the legend wrapping to a
57
+ // second row) but never its own width, so this settles instead of looping.
58
+ var cartesianRO = typeof ResizeObserver === 'function' ? new ResizeObserver(function (entries) {
59
+ entries.forEach(function (entry) {
60
+ var target = entry.target;
61
+ if (!target.isConnected) { cartesianRO.unobserve(target); return; }
62
+ var w = Math.round(entry.contentRect.width);
63
+ if (w && w !== target.__fdyChartW) renderChart(target);
64
+ });
65
+ }) : null;
66
+
53
67
  // Multi-series model: data-series (JSON) wins; else the data-values single-series shortcut.
54
68
  function getSeries(el) {
55
69
  var raw = el.getAttribute('data-series');
@@ -194,7 +208,46 @@
194
208
  var top = niceCeil(dataMax > base ? dataMax : base + 1);
195
209
  if (top <= base) top = base + 1;
196
210
 
197
- var W = 320, H = 180, PL = 38, PR = 10, PT = 10, PB = 22;
211
+ // Lay the chrome out first so the plot can be measured, then size the viewBox to it in CSS
212
+ // pixels: one user unit is one pixel, so font-size / r / stroke-width mean what they say at
213
+ // every container width. (A fixed 320x180 viewBox stretched by CSS multiplied every declared
214
+ // size by plotWidth/320 instead — a 9px axis label rendered ~39px on a 1728px viewport.)
215
+ var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
216
+ var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
217
+ el.classList.add('fdy-chart-xy');
218
+ el.innerHTML = '';
219
+ if (showLegend) {
220
+ var legend = document.createElement('ul');
221
+ legend.className = 'fdy-chart__legend fdy-chart__legend--row';
222
+ series.forEach(function (s, si) {
223
+ var li = document.createElement('li');
224
+ var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
225
+ li.appendChild(sw);
226
+ li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
227
+ legend.appendChild(li);
228
+ });
229
+ el.appendChild(legend);
230
+ }
231
+ var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
232
+ el.appendChild(plot);
233
+
234
+ // Measured while empty: .fdy-chart-xy__plot carries its own width / aspect-ratio / min-height,
235
+ // so the box is already final. A detached or display:none chart measures 0 — fall back to the
236
+ // legacy box and let the ResizeObserver repaint it once it is actually laid out.
237
+ var box = plot.getBoundingClientRect();
238
+ var W = Math.round(box.width) || 320, H = Math.round(box.height) || 180;
239
+ var FS = parseFloat(getComputedStyle(plot).fontSize) || 12; // axis type size, owned by CSS
240
+ var charW = FS * 0.6; // mono/body advance is ~0.6em
241
+
242
+ // Tick values are known before the gutter is, so size PL to the widest one: a currency axis
243
+ // needs more room than a percent axis, and a fixed gutter only ever suited one of them.
244
+ var TICKS = 4;
245
+ var tickText = [];
246
+ for (var t0 = 0; t0 <= TICKS; t0++) tickText.push(fmt(base + (top - base) * t0 / TICKS));
247
+ var widestTick = 0;
248
+ tickText.forEach(function (t) { widestTick = Math.max(widestTick, t.length * charW); });
249
+ var PL = Math.min(Math.ceil(widestTick) + 10, Math.round(W * 0.35));
250
+ var PR = 10, PT = 10, PB = Math.round(FS * 2.2);
198
251
  var plotW = W - PL - PR, plotH = H - PT - PB;
199
252
  function mapY(v) { return PT + plotH * (1 - (v - base) / (top - base)); }
200
253
  var slot = plotW / n;
@@ -206,7 +259,6 @@
206
259
  svg.setAttribute('aria-hidden', 'true');
207
260
 
208
261
  // y gridlines + formatted ticks (bottom line at `base` doubles as the zero baseline).
209
- var TICKS = 4;
210
262
  for (var k = 0; k <= TICKS; k++) {
211
263
  var val = base + (top - base) * k / TICKS;
212
264
  var gy = mapY(val);
@@ -216,9 +268,9 @@
216
268
  gl.setAttribute('class', 'fdy-chart-xy__grid');
217
269
  svg.appendChild(gl);
218
270
  var tk = svgEl('text');
219
- tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + 3).toFixed(1));
271
+ tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + FS * 0.35).toFixed(1));
220
272
  tk.setAttribute('text-anchor', 'end'); tk.setAttribute('class', 'fdy-chart-xy__tick');
221
- tk.textContent = fmt(val);
273
+ tk.textContent = tickText[k];
222
274
  svg.appendChild(tk);
223
275
  }
224
276
 
@@ -226,7 +278,6 @@
226
278
  // of assuming a fixed 40px slot; keep every step-th plus the last, then drop any kept label that
227
279
  // would collide with the one before it — including the forced last one (a fraction-of-a-slot
228
280
  // overlap happens when n-1 lands next to the last kept multiple of step, e.g. n=24 at most widths).
229
- var charW = 5.4; // ~9px font advance (.fdy-chart-xy__xlabel)
230
281
  var widest = 0;
231
282
  for (var wi = 0; wi < n; wi++) widest = Math.max(widest, String(labels[wi] || ('#' + (wi + 1))).length * charW);
232
283
  var minGap = widest + 8;
@@ -242,7 +293,7 @@
242
293
  }
243
294
  kept.forEach(function (xk) {
244
295
  var xt = svgEl('text');
245
- xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - 6);
296
+ xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - Math.round(FS * 0.7));
246
297
  xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
247
298
  xt.textContent = labels[xk] || ('#' + (xk + 1));
248
299
  svg.appendChild(xt);
@@ -268,7 +319,7 @@
268
319
  var rect = svgEl('rect');
269
320
  rect.setAttribute('x', x.toFixed(1)); rect.setAttribute('width', Math.max(1, barW - 1).toFixed(1));
270
321
  rect.setAttribute('y', Math.min(y0, y1).toFixed(1)); rect.setAttribute('height', Math.max(0.5, Math.abs(y1 - y0)).toFixed(1));
271
- rect.setAttribute('rx', '1.5'); rect.setAttribute('class', 'fdy-chart-xy__bar');
322
+ rect.setAttribute('rx', '2'); rect.setAttribute('class', 'fdy-chart-xy__bar');
272
323
  rect.style.fill = colorFor(si);
273
324
  svg.appendChild(rect);
274
325
  });
@@ -296,33 +347,14 @@
296
347
  }
297
348
  pts.forEach(function (p) {
298
349
  var dot = svgEl('circle');
299
- dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '2.2');
350
+ dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '3');
300
351
  dot.setAttribute('class', 'fdy-chart-xy__dot'); dot.style.fill = colorFor(si);
301
352
  svg.appendChild(dot);
302
353
  });
303
354
  });
304
355
  }
305
356
 
306
- // Assemble: optional legend on top, plot below, tooltip layer last.
307
- var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
308
- var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
309
- el.classList.add('fdy-chart-xy');
310
- el.innerHTML = '';
311
- if (showLegend) {
312
- var legend = document.createElement('ul');
313
- legend.className = 'fdy-chart__legend fdy-chart__legend--row';
314
- series.forEach(function (s, si) {
315
- var li = document.createElement('li');
316
- var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
317
- li.appendChild(sw);
318
- li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
319
- legend.appendChild(li);
320
- });
321
- el.appendChild(legend);
322
- }
323
- var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
324
357
  plot.appendChild(svg);
325
- el.appendChild(plot);
326
358
 
327
359
  // Hover: one transparent band per category → readout of every series at that x.
328
360
  var tip = makeTip(el);
@@ -342,6 +374,8 @@
342
374
  band.addEventListener('mouseleave', function () { hideTip(tip); });
343
375
  svg.appendChild(band);
344
376
  }
377
+ el.__fdyChartW = W;
378
+ if (cartesianRO) cartesianRO.observe(el);
345
379
  ensureImg(el);
346
380
  }
347
381
 
@@ -972,12 +972,12 @@ a { color: var(--color-primary); }
972
972
  /* Cartesian charts (line / area / multi-series & stacked bar) — SVG with themed axes.
973
973
  * Axis colour comes from --chart-grid / --chart-tick (both theme-aware); series colours are
974
974
  * set inline by the renderer from --chart-1..8, so the whole chart recolours on data-theme. */
975
- .fdy-chart-xy{position:relative;display:flex;flex-direction:column;gap:var(--space-3);width:100%;}
976
- .fdy-chart-xy__plot{width:100%;aspect-ratio:16/9;min-height:8rem;}
975
+ .fdy-chart-xy{position:relative;display:flex;flex-direction:column;gap:var(--space-3);width:100%;--fdy-chart-tick-size:var(--text-xs);}
976
+ .fdy-chart-xy__plot{width:100%;aspect-ratio:16/9;min-height:8rem;font-size:var(--fdy-chart-tick-size);}
977
977
  .fdy-chart-xy__plot svg{display:block;width:100%;height:100%;overflow:visible;}
978
978
  .fdy-chart-xy__grid{stroke:var(--chart-grid);stroke-width:1;shape-rendering:crispEdges;}
979
- .fdy-chart-xy__tick{fill:var(--chart-tick);font-family:var(--font-mono);font-size:9px;}
980
- .fdy-chart-xy__xlabel{fill:var(--chart-tick);font-family:var(--font-body);font-size:9px;}
979
+ .fdy-chart-xy__tick{fill:var(--chart-tick);font-family:var(--font-mono);}
980
+ .fdy-chart-xy__xlabel{fill:var(--chart-tick);font-family:var(--font-body);}
981
981
  .fdy-chart-xy__line{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke;}
982
982
  .fdy-chart-xy__area{stroke:none;opacity:.14;}
983
983
  .fdy-chart-xy__dot{stroke:var(--color-surface);stroke-width:1;}
package/dist/freeday.css CHANGED
@@ -589,12 +589,12 @@ a { color: var(--color-primary); }
589
589
  /* Cartesian charts (line / area / multi-series & stacked bar) — SVG with themed axes.
590
590
  * Axis colour comes from --chart-grid / --chart-tick (both theme-aware); series colours are
591
591
  * set inline by the renderer from --chart-1..8, so the whole chart recolours on data-theme. */
592
- .fdy-chart-xy{position:relative;display:flex;flex-direction:column;gap:var(--space-3);width:100%;}
593
- .fdy-chart-xy__plot{width:100%;aspect-ratio:16/9;min-height:8rem;}
592
+ .fdy-chart-xy{position:relative;display:flex;flex-direction:column;gap:var(--space-3);width:100%;--fdy-chart-tick-size:var(--text-xs);}
593
+ .fdy-chart-xy__plot{width:100%;aspect-ratio:16/9;min-height:8rem;font-size:var(--fdy-chart-tick-size);}
594
594
  .fdy-chart-xy__plot svg{display:block;width:100%;height:100%;overflow:visible;}
595
595
  .fdy-chart-xy__grid{stroke:var(--chart-grid);stroke-width:1;shape-rendering:crispEdges;}
596
- .fdy-chart-xy__tick{fill:var(--chart-tick);font-family:var(--font-mono);font-size:9px;}
597
- .fdy-chart-xy__xlabel{fill:var(--chart-tick);font-family:var(--font-body);font-size:9px;}
596
+ .fdy-chart-xy__tick{fill:var(--chart-tick);font-family:var(--font-mono);}
597
+ .fdy-chart-xy__xlabel{fill:var(--chart-tick);font-family:var(--font-body);}
598
598
  .fdy-chart-xy__line{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke;}
599
599
  .fdy-chart-xy__area{stroke:none;opacity:.14;}
600
600
  .fdy-chart-xy__dot{stroke:var(--color-surface);stroke-width:1;}
package/dist/freeday.js CHANGED
@@ -874,7 +874,9 @@
874
874
  * (var(--color-<name>)) OR a slot "chart-1".."chart-8" (var(--chart-N)) to pin a series to the
875
875
  * validated palette. Both data-fdy-color and data-fdy-colors accept the chart-N form.
876
876
  * Cartesian charts (line/area, multi-series/stacked bar) draw themed axes from --chart-grid /
877
- * --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency).
877
+ * --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency). Their
878
+ * viewBox is sized to the measured plot (1 user unit = 1 CSS pixel) and repainted from a
879
+ * ResizeObserver, so axis type stays in real pixels and is set in CSS via --fdy-chart-tick-size.
878
880
  * Charts are an enhancement — put a table/text fallback inside the element and it will be
879
881
  * replaced on render; give the element role="img" + aria-label.
880
882
  *
@@ -914,6 +916,18 @@
914
916
  function ensureImg(el) { if (!el.hasAttribute('role')) el.setAttribute('role', 'img'); }
915
917
  function svgEl(name) { return document.createElementNS(NS, name); }
916
918
 
919
+ // Cartesian charts size their viewBox to the measured plot, so a width change needs a repaint.
920
+ // Guarded on width alone: a render can change the element's own height (the legend wrapping to a
921
+ // second row) but never its own width, so this settles instead of looping.
922
+ var cartesianRO = typeof ResizeObserver === 'function' ? new ResizeObserver(function (entries) {
923
+ entries.forEach(function (entry) {
924
+ var target = entry.target;
925
+ if (!target.isConnected) { cartesianRO.unobserve(target); return; }
926
+ var w = Math.round(entry.contentRect.width);
927
+ if (w && w !== target.__fdyChartW) renderChart(target);
928
+ });
929
+ }) : null;
930
+
917
931
  // Multi-series model: data-series (JSON) wins; else the data-values single-series shortcut.
918
932
  function getSeries(el) {
919
933
  var raw = el.getAttribute('data-series');
@@ -1058,7 +1072,46 @@
1058
1072
  var top = niceCeil(dataMax > base ? dataMax : base + 1);
1059
1073
  if (top <= base) top = base + 1;
1060
1074
 
1061
- var W = 320, H = 180, PL = 38, PR = 10, PT = 10, PB = 22;
1075
+ // Lay the chrome out first so the plot can be measured, then size the viewBox to it in CSS
1076
+ // pixels: one user unit is one pixel, so font-size / r / stroke-width mean what they say at
1077
+ // every container width. (A fixed 320x180 viewBox stretched by CSS multiplied every declared
1078
+ // size by plotWidth/320 instead — a 9px axis label rendered ~39px on a 1728px viewport.)
1079
+ var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
1080
+ var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
1081
+ el.classList.add('fdy-chart-xy');
1082
+ el.innerHTML = '';
1083
+ if (showLegend) {
1084
+ var legend = document.createElement('ul');
1085
+ legend.className = 'fdy-chart__legend fdy-chart__legend--row';
1086
+ series.forEach(function (s, si) {
1087
+ var li = document.createElement('li');
1088
+ var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
1089
+ li.appendChild(sw);
1090
+ li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
1091
+ legend.appendChild(li);
1092
+ });
1093
+ el.appendChild(legend);
1094
+ }
1095
+ var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
1096
+ el.appendChild(plot);
1097
+
1098
+ // Measured while empty: .fdy-chart-xy__plot carries its own width / aspect-ratio / min-height,
1099
+ // so the box is already final. A detached or display:none chart measures 0 — fall back to the
1100
+ // legacy box and let the ResizeObserver repaint it once it is actually laid out.
1101
+ var box = plot.getBoundingClientRect();
1102
+ var W = Math.round(box.width) || 320, H = Math.round(box.height) || 180;
1103
+ var FS = parseFloat(getComputedStyle(plot).fontSize) || 12; // axis type size, owned by CSS
1104
+ var charW = FS * 0.6; // mono/body advance is ~0.6em
1105
+
1106
+ // Tick values are known before the gutter is, so size PL to the widest one: a currency axis
1107
+ // needs more room than a percent axis, and a fixed gutter only ever suited one of them.
1108
+ var TICKS = 4;
1109
+ var tickText = [];
1110
+ for (var t0 = 0; t0 <= TICKS; t0++) tickText.push(fmt(base + (top - base) * t0 / TICKS));
1111
+ var widestTick = 0;
1112
+ tickText.forEach(function (t) { widestTick = Math.max(widestTick, t.length * charW); });
1113
+ var PL = Math.min(Math.ceil(widestTick) + 10, Math.round(W * 0.35));
1114
+ var PR = 10, PT = 10, PB = Math.round(FS * 2.2);
1062
1115
  var plotW = W - PL - PR, plotH = H - PT - PB;
1063
1116
  function mapY(v) { return PT + plotH * (1 - (v - base) / (top - base)); }
1064
1117
  var slot = plotW / n;
@@ -1070,7 +1123,6 @@
1070
1123
  svg.setAttribute('aria-hidden', 'true');
1071
1124
 
1072
1125
  // y gridlines + formatted ticks (bottom line at `base` doubles as the zero baseline).
1073
- var TICKS = 4;
1074
1126
  for (var k = 0; k <= TICKS; k++) {
1075
1127
  var val = base + (top - base) * k / TICKS;
1076
1128
  var gy = mapY(val);
@@ -1080,9 +1132,9 @@
1080
1132
  gl.setAttribute('class', 'fdy-chart-xy__grid');
1081
1133
  svg.appendChild(gl);
1082
1134
  var tk = svgEl('text');
1083
- tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + 3).toFixed(1));
1135
+ tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + FS * 0.35).toFixed(1));
1084
1136
  tk.setAttribute('text-anchor', 'end'); tk.setAttribute('class', 'fdy-chart-xy__tick');
1085
- tk.textContent = fmt(val);
1137
+ tk.textContent = tickText[k];
1086
1138
  svg.appendChild(tk);
1087
1139
  }
1088
1140
 
@@ -1090,7 +1142,6 @@
1090
1142
  // of assuming a fixed 40px slot; keep every step-th plus the last, then drop any kept label that
1091
1143
  // would collide with the one before it — including the forced last one (a fraction-of-a-slot
1092
1144
  // overlap happens when n-1 lands next to the last kept multiple of step, e.g. n=24 at most widths).
1093
- var charW = 5.4; // ~9px font advance (.fdy-chart-xy__xlabel)
1094
1145
  var widest = 0;
1095
1146
  for (var wi = 0; wi < n; wi++) widest = Math.max(widest, String(labels[wi] || ('#' + (wi + 1))).length * charW);
1096
1147
  var minGap = widest + 8;
@@ -1106,7 +1157,7 @@
1106
1157
  }
1107
1158
  kept.forEach(function (xk) {
1108
1159
  var xt = svgEl('text');
1109
- xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - 6);
1160
+ xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - Math.round(FS * 0.7));
1110
1161
  xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
1111
1162
  xt.textContent = labels[xk] || ('#' + (xk + 1));
1112
1163
  svg.appendChild(xt);
@@ -1132,7 +1183,7 @@
1132
1183
  var rect = svgEl('rect');
1133
1184
  rect.setAttribute('x', x.toFixed(1)); rect.setAttribute('width', Math.max(1, barW - 1).toFixed(1));
1134
1185
  rect.setAttribute('y', Math.min(y0, y1).toFixed(1)); rect.setAttribute('height', Math.max(0.5, Math.abs(y1 - y0)).toFixed(1));
1135
- rect.setAttribute('rx', '1.5'); rect.setAttribute('class', 'fdy-chart-xy__bar');
1186
+ rect.setAttribute('rx', '2'); rect.setAttribute('class', 'fdy-chart-xy__bar');
1136
1187
  rect.style.fill = colorFor(si);
1137
1188
  svg.appendChild(rect);
1138
1189
  });
@@ -1160,33 +1211,14 @@
1160
1211
  }
1161
1212
  pts.forEach(function (p) {
1162
1213
  var dot = svgEl('circle');
1163
- dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '2.2');
1214
+ dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '3');
1164
1215
  dot.setAttribute('class', 'fdy-chart-xy__dot'); dot.style.fill = colorFor(si);
1165
1216
  svg.appendChild(dot);
1166
1217
  });
1167
1218
  });
1168
1219
  }
1169
1220
 
1170
- // Assemble: optional legend on top, plot below, tooltip layer last.
1171
- var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
1172
- var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
1173
- el.classList.add('fdy-chart-xy');
1174
- el.innerHTML = '';
1175
- if (showLegend) {
1176
- var legend = document.createElement('ul');
1177
- legend.className = 'fdy-chart__legend fdy-chart__legend--row';
1178
- series.forEach(function (s, si) {
1179
- var li = document.createElement('li');
1180
- var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
1181
- li.appendChild(sw);
1182
- li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
1183
- legend.appendChild(li);
1184
- });
1185
- el.appendChild(legend);
1186
- }
1187
- var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
1188
1221
  plot.appendChild(svg);
1189
- el.appendChild(plot);
1190
1222
 
1191
1223
  // Hover: one transparent band per category → readout of every series at that x.
1192
1224
  var tip = makeTip(el);
@@ -1206,6 +1238,8 @@
1206
1238
  band.addEventListener('mouseleave', function () { hideTip(tip); });
1207
1239
  svg.appendChild(band);
1208
1240
  }
1241
+ el.__fdyChartW = W;
1242
+ if (cartesianRO) cartesianRO.observe(el);
1209
1243
  ensureImg(el);
1210
1244
  }
1211
1245
 
@@ -171,11 +171,6 @@
171
171
  data-fdy-format="number"
172
172
  role="img"
173
173
  aria-label="Invoiced versus collected, in millions: invoiced rose from 62 to 88, collected from 54 to 81">
174
- <table>
175
- <caption>Invoiced vs collected (millions)</caption>
176
- <tr><th scope="col">Month</th><th scope="col">Invoiced</th><th scope="col">Collected</th></tr>
177
- <tr><td>Jul</td><td>88</td><td>81</td></tr>
178
- </table>
179
174
  </div>
180
175
  </div>
181
176
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.50.0",
3
+ "version": "1.51.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@
92
92
  "scripts": {
93
93
  "build": "node tokens/build.mjs",
94
94
  "test": "node --test",
95
- "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs",
95
+ "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs",
96
96
  "prepack": "node tokens/build.mjs",
97
97
  "version": "node tokens/build.mjs && git add dist",
98
98
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
@@ -42,12 +42,12 @@
42
42
  /* Cartesian charts (line / area / multi-series & stacked bar) — SVG with themed axes.
43
43
  * Axis colour comes from --chart-grid / --chart-tick (both theme-aware); series colours are
44
44
  * set inline by the renderer from --chart-1..8, so the whole chart recolours on data-theme. */
45
- .fdy-chart-xy{position:relative;display:flex;flex-direction:column;gap:var(--space-3);width:100%;}
46
- .fdy-chart-xy__plot{width:100%;aspect-ratio:16/9;min-height:8rem;}
45
+ .fdy-chart-xy{position:relative;display:flex;flex-direction:column;gap:var(--space-3);width:100%;--fdy-chart-tick-size:var(--text-xs);}
46
+ .fdy-chart-xy__plot{width:100%;aspect-ratio:16/9;min-height:8rem;font-size:var(--fdy-chart-tick-size);}
47
47
  .fdy-chart-xy__plot svg{display:block;width:100%;height:100%;overflow:visible;}
48
48
  .fdy-chart-xy__grid{stroke:var(--chart-grid);stroke-width:1;shape-rendering:crispEdges;}
49
- .fdy-chart-xy__tick{fill:var(--chart-tick);font-family:var(--font-mono);font-size:9px;}
50
- .fdy-chart-xy__xlabel{fill:var(--chart-tick);font-family:var(--font-body);font-size:9px;}
49
+ .fdy-chart-xy__tick{fill:var(--chart-tick);font-family:var(--font-mono);}
50
+ .fdy-chart-xy__xlabel{fill:var(--chart-tick);font-family:var(--font-body);}
51
51
  .fdy-chart-xy__line{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke;}
52
52
  .fdy-chart-xy__area{stroke:none;opacity:.14;}
53
53
  .fdy-chart-xy__dot{stroke:var(--color-surface);stroke-width:1;}