@cahyo-dimas/freeday 1.50.0 → 1.51.1

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,78 @@
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.1] — 2026-08-24
7
+ ### Docs
8
+ - **`COMPONENTS.md` never said whether two overlays may be open at once** (#046). Modal and Drawer
9
+ were documented in isolation, so "can a confirm modal open over an open drawer?" had no answer in
10
+ the file — and the built CSS does not answer it either: the only overlay `z-index` in the bundle
11
+ belongs to the toast region. Both entries now state the rule, because both components are
12
+ `<dialog>` opened with `showModal()` and therefore enter the **top layer**, where paint order is
13
+ the order they were opened: the later modal is above the drawer with no `z-index` from either,
14
+ Escape closes the topmost first (so cancelling returns to the still-open drawer), and closing them
15
+ out of order leaves the survivor painted and dismissible.
16
+ - **The one caveat is the browser's, and it is now written down**: open each overlay from the
17
+ gesture that asked for it. A `showModal()` with no user activation — from a timer, or after an
18
+ `await` that outlived the click — has its close watcher GROUPED with the dialog below it, and a
19
+ single Escape then closes both. Measured, not assumed: two untrusted `.click()` opens, one Escape,
20
+ both dialogs shut.
21
+ ### Added — guards
22
+ - `browser/overlay-stack.mjs` (3 tests) proves that paragraph instead of restating it — the #041
23
+ lesson applied to a documentation change. Paint order is asserted in **pixels** at the overlap of
24
+ the two, because a modal dialog makes the rest of the document inert and `elementFromPoint` will
25
+ name the dialog whatever is really on top; the overlays are opened with **trusted** clicks, or the
26
+ close-watcher grouping above would fail the Escape assertion for a reason that is not the kit's.
27
+ - **`pressKey` in the browser harness sent every key but Tab with virtual-key code 0** — delivered
28
+ to the page, but not acted on by the browser: Escape reached a `keydown` listener yet never closed
29
+ a native `<dialog>`. A dismissal guard written with it would have passed for the wrong reason and
30
+ reported a broken kit as fine. It now carries the real `windowsVirtualKeyCode` /
31
+ `nativeVirtualKeyCode` for the keys the suite presses.
32
+
33
+ ## [1.51.0] — 2026-08-21
34
+ ### Fixed
35
+ - **A cartesian chart's axis text and dots grew with the container** (#042). `renderCartesian` built
36
+ a fixed `viewBox="0 0 320 180"` and let `.fdy-chart-xy__plot` stretch it to `width:100%`, so every
37
+ size the renderer expressed in user units was multiplied by `plotWidth / 320`. `font-size:9px` on
38
+ a tick is not nine screen pixels at any width but 320px: measured on a real console it rendered
39
+ 13px at a 350px plot and **81px at a 2232px** one, making the chart's smallest, most secondary
40
+ text the largest type on the page. `r="2.2"` did the same to the dots — 4.8px across at 350px,
41
+ 30.7px at 2232px. The chart now measures `__plot` and sizes the `viewBox` to it in CSS pixels, so
42
+ one user unit is one pixel and every declared size means what it says at every width; a
43
+ `ResizeObserver` repaints it when the box changes (a collapsing sidebar moves a plot ~300px at a
44
+ fixed viewport, so this is the common case, not an edge case). A chart that measures 0 — detached,
45
+ or inside `display:none` — falls back to the old box and repaints when it is laid out.
46
+ - **A currency y-axis was clipped by a gutter sized for a percent one** (#042). `PL` was a constant
47
+ 38 user units, which was ~4 characters at one scale and something else at every other. It is now
48
+ derived from the widest formatted tick the axis will actually draw, capped at 35% of the plot.
49
+ ### Changed
50
+ - **Axis type is a real CSS size**, set by `--fdy-chart-tick-size` on `.fdy-chart-xy` (default
51
+ `var(--text-xs)`). `font-size` moved off `.fdy-chart-xy__tick` / `__xlabel` onto
52
+ `.fdy-chart-xy__plot`, which the SVG text inherits — one computed read gives the renderer the real
53
+ pixel size it needs for the y-gutter and for the x-label autoskip, so an override flows to the
54
+ text *and* to the geometry around it. Before this, chart text could not be restyled at all: any
55
+ value set was multiplied by a scale the consumer could not see or compute.
56
+ The dot radius went 2.2 → 3 and the bar corner 1.5 → 2, both retuned against a 1:1 scale.
57
+ ### Docs
58
+ - **`COMPONENTS.md` claimed a `<table>` fallback that no rendered chart has** (#041). It said every
59
+ chart is `role="img"` + `aria-label` "with a `<table>` fallback inside the element"; the renderer
60
+ builds no table and wipes the element on every render path. Worse, `role="img"` makes descendants
61
+ presentational, so the bar values, ticks and legend the renderer *does* draw are not exposed
62
+ either — the author's `aria-label` is the entire text alternative, and the docs now say so and ask
63
+ for one that carries the numbers. The four dead fallback tables in `docs/index.html` and
64
+ `docs/reference-screen.html`, which no reader has ever reached, are gone with it.
65
+ - **The kit's own chart sizing is written down** (#041): `.fdy-chart-xy__plot` is
66
+ `aspect-ratio:16/9` + `min-height:8rem`, `.fdy-bars` a fixed `9rem`, `.fdy-sparkline` an
67
+ inline-block `8rem × 2.25rem` — with the note to override those rather than the chart root, since
68
+ a root `height` fights `aspect-ratio` instead of setting it.
69
+ ### Added — guards
70
+ - **`browser/chart-scale.mjs`** — the axis label box and dot diameter are measured at plot widths of
71
+ 350 / 696 / 1400 / 2232px and must not vary by more than a pixel, must stay smaller than body
72
+ text, and the `viewBox` must equal the measured plot. A second spec widens a container and asserts
73
+ the repaint. This class of bug is invisible to a source read: `font-size:9px` and `r="2.2"` look
74
+ like correct sizes, and the one thing in the plot that *was* protected —
75
+ `vector-effect:non-scaling-stroke` on the line — is what made the other three look protected too.
76
+ Only a real layout tells them apart.
77
+
6
78
  ## [1.50.0] — 2026-08-20
7
79
  ### Fixed
8
80
  - **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`
@@ -1036,6 +1049,18 @@ painted on top of a modal, because it will tell you the dialog is, even when it
1036
1049
  `__close`. The body scrolls; the footer never clips. `aria-labelledby` → the title's `id`;
1037
1050
  `data-close` on any button that should close it.
1038
1051
 
1052
+ **Stacking two overlays is supported.** Modal and drawer are both `<dialog>` opened with
1053
+ `showModal()`, so both enter the top layer and paint in the order they were opened — a confirm
1054
+ modal opened from inside an open drawer paints **above** it, and neither needs a `z-index` for
1055
+ that. Escape closes the topmost first, so cancelling returns to the still-open drawer; a second
1056
+ Escape then closes the drawer. Closing them out of order is fine too: closing the drawer while the
1057
+ modal is up leaves the modal open, painted and dismissible.
1058
+
1059
+ One caveat, and it is the browser's rather than the kit's: **open each overlay from the gesture
1060
+ that asked for it.** A `showModal()` that runs with no user activation — from a timer, or after an
1061
+ `await` that outlived the click — has its close watcher grouped with the dialog below it, and then
1062
+ a single Escape closes both at once instead of the topmost only.
1063
+
1039
1064
  ## Drawer — `.fdy-drawer`
1040
1065
  > **Typed wrapper: `<FdyDrawer>`** — Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1041
1066
 
@@ -1043,6 +1068,11 @@ Temporary side panel on native `<dialog>` — left by default, `--right` to flip
1043
1068
  `__title` `__body` `__footer` `__close`. Open it from any
1044
1069
  `<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
1045
1070
 
1071
+ Opened with `showModal()`, so it is in the **top layer** on the same terms as the modal — including
1072
+ opening a modal over an open drawer, which is supported and described under
1073
+ [Modal → *Stacking two overlays*](#modal--fdy-modal). It slides in on `transform` and never animates
1074
+ opacity: wait on its position, not on `opacity`, if you need to know when it has arrived.
1075
+
1046
1076
  ## Accordion — `.fdy-accordion`
1047
1077
  Native `<details>`, zero JS: `.fdy-accordion__item` on each `<details>`, content in
1048
1078
  `.fdy-accordion__panel`, the trigger is the `<summary>`.
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.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.51.1)
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.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.51.1)
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.1",
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 browser/overlay-stack.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;}