@cahyo-dimas/freeday 1.9.0 → 1.11.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 +49 -0
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/react/components/FdyTable.tsx +28 -14
- package/adapters/vue/components/FdyTable.vue +23 -13
- package/dist/freeday-chart.js +25 -8
- package/dist/freeday.bundle.css +22 -4
- package/dist/freeday.css +22 -4
- package/dist/freeday.js +25 -8
- package/package.json +1 -1
- package/src/components/badge.css +1 -1
- package/src/components/datepicker.css +3 -1
- package/src/components/filterbar.css +11 -2
- package/src/components/input.css +5 -0
- package/src/components/table.css +2 -0
- package/src/freeday-chart.js +25 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,55 @@
|
|
|
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.11.0] — 2026-07-28
|
|
7
|
+
Follow-ups from the doc-ai-automation migration (improvement notes #31–#33). Additive / corrective.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Badges no longer wrap and break the pill (#31).** `.fdy-badge` had `line-height:1` but no
|
|
11
|
+
`white-space`, so a two-word label (e.g. "Needs review") wrapped and collided with the pill padding,
|
|
12
|
+
turning the capsule into a cramped lozenge. Added `white-space:nowrap` — matching the sibling
|
|
13
|
+
`.fdy-badge-ov`, which already had it. A too-narrow badge now overflows its column (which
|
|
14
|
+
`.fdy-table-scroll` already handles) instead of wrapping.
|
|
15
|
+
- **Donut chart honours `legend="none"` (#33).** `freeday-chart.js` read `data-fdy-legend` only in the
|
|
16
|
+
cartesian renderer; the donut appended its legend unconditionally, so `<FdyChart type="donut"
|
|
17
|
+
legend="none">` still printed the built-in legend even when the caller supplied its own. Gated the
|
|
18
|
+
donut legend on the same attribute (`auto` still shows it — a donut is unreadable without labels).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **`.fdy-filterbar--actions-inline` modifier (#32).** Opt-in: sits the trailing actions inline after
|
|
22
|
+
the last field instead of at the far edge, so a wrapped bar reads as attached to its filters. Also
|
|
23
|
+
**documented** the filterbar's wrap behaviour: a wrapped bar breaks flex lines on each item's
|
|
24
|
+
*flex-basis* (the grow field's `flex:1 1 16rem` reserves 16rem), not its rendered width — so the
|
|
25
|
+
grow basis decides the wrap point; to avoid stranded actions, tune the `--w-grow` basis or drop a
|
|
26
|
+
field. (The note's margin-swap proposal to un-strand the actions was **tested in-browser and does not
|
|
27
|
+
work** — margins don't change flex line-breaking — so only the modifier + docs were taken.)
|
|
28
|
+
|
|
29
|
+
## [1.10.0] — 2026-07-28
|
|
30
|
+
Follow-ups from the doc-ai-automation migration (improvement notes #27–#30). Additive / corrective;
|
|
31
|
+
no breaking changes.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **`<fieldset class="fdy-field">` no longer leaks native browser chrome (#27).** The kit shipped no
|
|
35
|
+
`fieldset` reset, so a grouped field (a radio/checkbox group with a `<legend>`) showed the UA groove
|
|
36
|
+
border, extra padding, and `min-inline-size:min-content` — the last of which also blocked the
|
|
37
|
+
filterbar column widths from shrinking it. Added a scoped `fieldset.fdy-field` reset so it lays out
|
|
38
|
+
identically to the `<div>` form.
|
|
39
|
+
- **Chart x-axis: the last label no longer collides with its neighbour (#29).** `freeday-chart.js`
|
|
40
|
+
force-drew the final label with no spacing check, so on a dense axis (e.g. 24 hourly points) the
|
|
41
|
+
last two labels fused. It now estimates the widest label instead of assuming a fixed 40px slot, and
|
|
42
|
+
drops the penultimate label when the forced-last one would overlap it. Visual only; sparse axes
|
|
43
|
+
render identically.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- **`FdyTable` expandable detail rows (#30).** A `row-detail` slot (Vue) / `renderRowDetail` prop
|
|
47
|
+
(React) renders a full-width row beneath any expanded row, driven by a **controlled** `expandedKeys`
|
|
48
|
+
(matches the sort/filter/page contract; pairs with `row-activate` as the toggle). Adds a
|
|
49
|
+
`.fdy-table__detailrow` class (muted panel, no hover tint). Removes the last interaction that kept a
|
|
50
|
+
table hand-rolled.
|
|
51
|
+
- **`.fdy-field--w-2xl` (25rem) filterbar column (#28).** Wide enough for a two-picker date range, so
|
|
52
|
+
it need not burn the single `--w-grow` slot. The date-range pickers also gained a `min-width` (7rem)
|
|
53
|
+
so a squeezed range compresses and wraps instead of clipping the date (was `min-width:0`).
|
|
54
|
+
|
|
6
55
|
## [1.9.0] — 2026-07-28
|
|
7
56
|
Follow-ups to 1.8.0's `FdyTable` (improvement notes #25/#26). Additive — the #25 change **widens** a
|
|
8
57
|
generic bound, so every call site that compiled before still compiles.
|
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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.11.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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.11.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`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CSSProperties, JSX, ReactNode } from 'react';
|
|
2
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Fragment, useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
cellValue,
|
|
5
5
|
cellText,
|
|
@@ -57,6 +57,10 @@ export interface FdyTableProps<Row extends object> {
|
|
|
57
57
|
rowClass?: (row: Row) => string | undefined;
|
|
58
58
|
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
59
59
|
onRowActivate?: (row: Row) => void;
|
|
60
|
+
/** Controlled: row keys whose detail is shown as a full-width row beneath them. */
|
|
61
|
+
expandedKeys?: ReadonlyArray<string | number>;
|
|
62
|
+
/** Renders the expandable detail row for an expanded row (React equivalent of Vue's `row-detail` slot). */
|
|
63
|
+
renderRowDetail?: (row: Row) => ReactNode;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Element {
|
|
@@ -164,6 +168,9 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
164
168
|
e.preventDefault();
|
|
165
169
|
props.onRowActivate?.(row);
|
|
166
170
|
}
|
|
171
|
+
function isExpanded(row: Row): boolean {
|
|
172
|
+
return props.expandedKeys?.includes(props.rowKey(row)) === true;
|
|
173
|
+
}
|
|
167
174
|
function renderCellContent(col: FdyTableColumn<Row>, row: Row): ReactNode {
|
|
168
175
|
if (props.renderCell !== undefined) {
|
|
169
176
|
const custom: ReactNode = props.renderCell(col, row, cellValue(row, col));
|
|
@@ -213,19 +220,26 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
213
220
|
</tr>
|
|
214
221
|
) : (
|
|
215
222
|
displayRows.map((row: Row): JSX.Element => (
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
<Fragment key={props.rowKey(row)}>
|
|
224
|
+
<tr
|
|
225
|
+
className={rowClassName(row)}
|
|
226
|
+
tabIndex={props.rowActivatable ? 0 : undefined}
|
|
227
|
+
aria-expanded={props.renderRowDetail !== undefined ? (isExpanded(row) ? 'true' : 'false') : undefined}
|
|
228
|
+
onClick={(): void => {
|
|
229
|
+
if (props.rowActivatable === true) props.onRowActivate?.(row);
|
|
230
|
+
}}
|
|
231
|
+
onKeyDown={(e): void => onRowKeydown(e, row)}
|
|
232
|
+
>
|
|
233
|
+
{props.columns.map((col: FdyTableColumn<Row>): JSX.Element => (
|
|
234
|
+
<td key={col.key} className={cellClass(col)} style={alignStyle(col)}>{renderCellContent(col, row)}</td>
|
|
235
|
+
))}
|
|
236
|
+
</tr>
|
|
237
|
+
{props.renderRowDetail !== undefined && isExpanded(row) && (
|
|
238
|
+
<tr className="fdy-table__detailrow">
|
|
239
|
+
<td colSpan={colCount}>{props.renderRowDetail(row)}</td>
|
|
240
|
+
</tr>
|
|
241
|
+
)}
|
|
242
|
+
</Fragment>
|
|
229
243
|
))
|
|
230
244
|
)}
|
|
231
245
|
</tbody>
|
|
@@ -48,6 +48,8 @@ const props = defineProps<{
|
|
|
48
48
|
rowActivatable?: boolean;
|
|
49
49
|
/** Per-row class hook, e.g. to mark a selected row. */
|
|
50
50
|
rowClass?: (row: Row) => string | undefined;
|
|
51
|
+
/** Controlled: row keys whose `row-detail` slot is shown as a full-width row beneath them. */
|
|
52
|
+
expandedKeys?: ReadonlyArray<string | number>;
|
|
51
53
|
}>();
|
|
52
54
|
|
|
53
55
|
const emit = defineEmits<{
|
|
@@ -184,6 +186,9 @@ function onRowKeydown(e: KeyboardEvent, row: Row): void {
|
|
|
184
186
|
e.preventDefault();
|
|
185
187
|
emit('row-activate', row);
|
|
186
188
|
}
|
|
189
|
+
function isExpanded(row: Row): boolean {
|
|
190
|
+
return props.expandedKeys?.includes(props.rowKey(row)) === true;
|
|
191
|
+
}
|
|
187
192
|
</script>
|
|
188
193
|
|
|
189
194
|
<template>
|
|
@@ -224,19 +229,24 @@ function onRowKeydown(e: KeyboardEvent, row: Row): void {
|
|
|
224
229
|
<slot name="empty">{{ emptyText ?? 'No data' }}</slot>
|
|
225
230
|
</td>
|
|
226
231
|
</tr>
|
|
227
|
-
<
|
|
228
|
-
v-for="row in displayRows"
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
232
|
+
<template v-else>
|
|
233
|
+
<template v-for="row in displayRows" :key="rowKey(row)">
|
|
234
|
+
<tr
|
|
235
|
+
:class="rowClasses(row)"
|
|
236
|
+
:tabindex="rowActivatable ? 0 : undefined"
|
|
237
|
+
:aria-expanded="$slots['row-detail'] ? (isExpanded(row) ? 'true' : 'false') : undefined"
|
|
238
|
+
@click="onRowClick(row)"
|
|
239
|
+
@keydown="onRowKeydown($event, row)"
|
|
240
|
+
>
|
|
241
|
+
<td v-for="col in columns" :key="col.key" :class="cellClass(col)" :style="alignStyle(col)">
|
|
242
|
+
<slot :name="`cell-${col.key}`" :row="row" :value="cellValue(row, col)">{{ cellText(row, col) }}</slot>
|
|
243
|
+
</td>
|
|
244
|
+
</tr>
|
|
245
|
+
<tr v-if="$slots['row-detail'] && isExpanded(row)" class="fdy-table__detailrow">
|
|
246
|
+
<td :colspan="columns.length"><slot name="row-detail" :row="row" /></td>
|
|
247
|
+
</tr>
|
|
248
|
+
</template>
|
|
249
|
+
</template>
|
|
240
250
|
</tbody>
|
|
241
251
|
</table>
|
|
242
252
|
</div>
|
package/dist/freeday-chart.js
CHANGED
|
@@ -213,17 +213,31 @@
|
|
|
213
213
|
svg.appendChild(tk);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
// x category labels with autoskip.
|
|
217
|
-
|
|
216
|
+
// x category labels with autoskip. Labels are author-supplied, so estimate the widest one instead
|
|
217
|
+
// of assuming a fixed 40px slot; keep every step-th plus the last, then drop any kept label that
|
|
218
|
+
// would collide with the one before it — including the forced last one (a fraction-of-a-slot
|
|
219
|
+
// overlap happens when n-1 lands next to the last kept multiple of step, e.g. n=24 at most widths).
|
|
220
|
+
var charW = 5.4; // ~9px font advance (.fdy-chart-xy__xlabel)
|
|
221
|
+
var widest = 0;
|
|
222
|
+
for (var wi = 0; wi < n; wi++) widest = Math.max(widest, String(labels[wi] || ('#' + (wi + 1))).length * charW);
|
|
223
|
+
var minGap = widest + 8;
|
|
224
|
+
var maxTicks = Math.max(2, Math.floor(plotW / minGap)), step = Math.ceil(n / maxTicks);
|
|
225
|
+
var xPos = function (i) { return (type === 'bar') ? xCenter(i) : xLine(i); };
|
|
226
|
+
var kept = [];
|
|
218
227
|
for (var xi = 0; xi < n; xi++) {
|
|
219
|
-
if (xi % step
|
|
220
|
-
|
|
228
|
+
if (xi % step === 0 || xi === n - 1) kept.push(xi);
|
|
229
|
+
}
|
|
230
|
+
// The last label is worth forcing, but not on top of its neighbour — drop the penultimate one.
|
|
231
|
+
if (kept.length >= 2 && xPos(kept[kept.length - 1]) - xPos(kept[kept.length - 2]) < minGap) {
|
|
232
|
+
kept.splice(kept.length - 2, 1);
|
|
233
|
+
}
|
|
234
|
+
kept.forEach(function (xk) {
|
|
221
235
|
var xt = svgEl('text');
|
|
222
|
-
xt.setAttribute('x',
|
|
236
|
+
xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - 6);
|
|
223
237
|
xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
|
|
224
|
-
xt.textContent = labels[
|
|
238
|
+
xt.textContent = labels[xk] || ('#' + (xk + 1));
|
|
225
239
|
svg.appendChild(xt);
|
|
226
|
-
}
|
|
240
|
+
});
|
|
227
241
|
|
|
228
242
|
if (type === 'bar') {
|
|
229
243
|
var S = series.length;
|
|
@@ -366,7 +380,10 @@
|
|
|
366
380
|
li.appendChild(document.createTextNode((labels[i] || ('#' + (i + 1))) + ' — ' + Math.round((v / total) * 100) + '%'));
|
|
367
381
|
legend.appendChild(li);
|
|
368
382
|
});
|
|
369
|
-
el.appendChild(ring);
|
|
383
|
+
el.appendChild(ring);
|
|
384
|
+
// Honour data-fdy-legend for the donut too — a caller may supply its own richer legend. 'auto'
|
|
385
|
+
// keeps today's behaviour (a donut is unreadable without labels, so it shows); 'none' suppresses it.
|
|
386
|
+
if ((el.getAttribute('data-fdy-legend') || 'auto') !== 'none') el.appendChild(legend);
|
|
370
387
|
ensureImg(el);
|
|
371
388
|
}
|
|
372
389
|
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -526,7 +526,7 @@ a { color: var(--color-primary); }
|
|
|
526
526
|
.fdy-avatar-group .fdy-avatar:first-child{margin-left:0;}
|
|
527
527
|
|
|
528
528
|
/* Freeday — Badge */
|
|
529
|
-
.fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
|
|
529
|
+
.fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;white-space:nowrap;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
|
|
530
530
|
.fdy-badge--success{color:var(--color-success-strong);background:var(--color-success-soft);border-color:color-mix(in srgb,var(--color-success) 26%,transparent);}
|
|
531
531
|
.fdy-badge--warning{color:var(--color-warning-strong);background:var(--color-warning-soft);border-color:color-mix(in srgb,var(--color-warning) 26%,transparent);}
|
|
532
532
|
.fdy-badge--danger{color:var(--color-danger-strong);background:var(--color-danger-soft);border-color:color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
@@ -875,7 +875,9 @@ a { color: var(--color-primary); }
|
|
|
875
875
|
/* Responsive: each picker may shrink (min-width:0) & wrap so the range never overflows a
|
|
876
876
|
narrow row/phone; inline-flex still hugs to ~23rem at wider widths (look unchanged). */
|
|
877
877
|
.fdy-daterange{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);}
|
|
878
|
-
|
|
878
|
+
/* Each picker shrinks from an 11rem basis but not below ~7rem — enough for "dd/mm/yyyy" + the icon,
|
|
879
|
+
so a squeezed range compresses gracefully (and wraps) instead of clipping the date. */
|
|
880
|
+
.fdy-daterange .fdy-datepicker{flex:0 1 11rem;min-width:7rem;}
|
|
879
881
|
.fdy-daterange__sep{flex:none;color:var(--color-text-subtle);}
|
|
880
882
|
|
|
881
883
|
@media (prefers-reduced-motion:no-preference){.fdy-datepicker__panel:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
|
|
@@ -978,13 +980,22 @@ a { color: var(--color-primary); }
|
|
|
978
980
|
.fdy-filterbar>.fdy-field--w-sm{width:9rem;}
|
|
979
981
|
.fdy-filterbar>.fdy-field--w-lg{width:15rem;}
|
|
980
982
|
.fdy-filterbar>.fdy-field--w-xl{width:18rem;}
|
|
983
|
+
/* Wide enough for a two-picker date range (~25rem) so it need not burn the single --w-grow slot. */
|
|
984
|
+
.fdy-filterbar>.fdy-field--w-2xl{width:25rem;}
|
|
981
985
|
/* One growing field (usually the search) absorbs the remaining width. */
|
|
982
986
|
.fdy-filterbar>.fdy-field--w-grow{flex:1 1 16rem;width:auto;min-width:12rem;}
|
|
983
|
-
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline.
|
|
987
|
+
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline.
|
|
988
|
+
* Wrap note: a wrapped bar breaks flex lines on each item's *flex-basis* (the grow field's
|
|
989
|
+
* `flex:1 1 16rem` reserves 16rem for line-breaking), NOT its rendered width — so the grow basis,
|
|
990
|
+
* not the visible width, decides the wrap point. Margins can't keep the actions off a lonely wrapped
|
|
991
|
+
* row; to avoid stranded actions, tune the --w-grow basis or drop a field so the bar fits one row. */
|
|
984
992
|
.fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
|
|
993
|
+
/* Opt-in: sit the actions inline right after the last field instead of at the far edge — so when the
|
|
994
|
+
* bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
|
|
995
|
+
.fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
|
|
985
996
|
/* Stack on narrow screens so nothing overflows the row. */
|
|
986
997
|
@media (max-width:640px){
|
|
987
|
-
.fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
|
|
998
|
+
.fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
|
|
988
999
|
.fdy-filterbar__actions{width:100%;margin-left:0;}
|
|
989
1000
|
}
|
|
990
1001
|
|
|
@@ -1022,6 +1033,11 @@ a { color: var(--color-primary); }
|
|
|
1022
1033
|
|
|
1023
1034
|
/* Freeday — Field & Input */
|
|
1024
1035
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
1036
|
+
/* A grouped field is legitimately a <fieldset> (it needs a <legend>) — e.g. a radio/checkbox group.
|
|
1037
|
+
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
1038
|
+
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
1039
|
+
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
1040
|
+
fieldset.fdy-field>legend{padding:0;float:none;}
|
|
1025
1041
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
1026
1042
|
.fdy-input,.fdy-textarea{width:100%;height:var(--control-h);padding:0 var(--space-3);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
|
|
1027
1043
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
|
@@ -1223,6 +1239,8 @@ a { color: var(--color-primary); }
|
|
|
1223
1239
|
comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
|
|
1224
1240
|
.fdy-table__row--activatable{cursor:pointer;}
|
|
1225
1241
|
.fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
|
|
1242
|
+
/* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
|
|
1243
|
+
.fdy-table__detailrow>td,.fdy-table__detailrow:hover>td{background:var(--color-surface-2);}
|
|
1226
1244
|
.fdy-table th[aria-sort]{cursor:pointer;}
|
|
1227
1245
|
.fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
|
|
1228
1246
|
.fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
|
package/dist/freeday.css
CHANGED
|
@@ -181,7 +181,7 @@ a { color: var(--color-primary); }
|
|
|
181
181
|
.fdy-avatar-group .fdy-avatar:first-child{margin-left:0;}
|
|
182
182
|
|
|
183
183
|
/* Freeday — Badge */
|
|
184
|
-
.fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
|
|
184
|
+
.fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;white-space:nowrap;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
|
|
185
185
|
.fdy-badge--success{color:var(--color-success-strong);background:var(--color-success-soft);border-color:color-mix(in srgb,var(--color-success) 26%,transparent);}
|
|
186
186
|
.fdy-badge--warning{color:var(--color-warning-strong);background:var(--color-warning-soft);border-color:color-mix(in srgb,var(--color-warning) 26%,transparent);}
|
|
187
187
|
.fdy-badge--danger{color:var(--color-danger-strong);background:var(--color-danger-soft);border-color:color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
@@ -530,7 +530,9 @@ a { color: var(--color-primary); }
|
|
|
530
530
|
/* Responsive: each picker may shrink (min-width:0) & wrap so the range never overflows a
|
|
531
531
|
narrow row/phone; inline-flex still hugs to ~23rem at wider widths (look unchanged). */
|
|
532
532
|
.fdy-daterange{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);}
|
|
533
|
-
|
|
533
|
+
/* Each picker shrinks from an 11rem basis but not below ~7rem — enough for "dd/mm/yyyy" + the icon,
|
|
534
|
+
so a squeezed range compresses gracefully (and wraps) instead of clipping the date. */
|
|
535
|
+
.fdy-daterange .fdy-datepicker{flex:0 1 11rem;min-width:7rem;}
|
|
534
536
|
.fdy-daterange__sep{flex:none;color:var(--color-text-subtle);}
|
|
535
537
|
|
|
536
538
|
@media (prefers-reduced-motion:no-preference){.fdy-datepicker__panel:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
|
|
@@ -633,13 +635,22 @@ a { color: var(--color-primary); }
|
|
|
633
635
|
.fdy-filterbar>.fdy-field--w-sm{width:9rem;}
|
|
634
636
|
.fdy-filterbar>.fdy-field--w-lg{width:15rem;}
|
|
635
637
|
.fdy-filterbar>.fdy-field--w-xl{width:18rem;}
|
|
638
|
+
/* Wide enough for a two-picker date range (~25rem) so it need not burn the single --w-grow slot. */
|
|
639
|
+
.fdy-filterbar>.fdy-field--w-2xl{width:25rem;}
|
|
636
640
|
/* One growing field (usually the search) absorbs the remaining width. */
|
|
637
641
|
.fdy-filterbar>.fdy-field--w-grow{flex:1 1 16rem;width:auto;min-width:12rem;}
|
|
638
|
-
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline.
|
|
642
|
+
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline.
|
|
643
|
+
* Wrap note: a wrapped bar breaks flex lines on each item's *flex-basis* (the grow field's
|
|
644
|
+
* `flex:1 1 16rem` reserves 16rem for line-breaking), NOT its rendered width — so the grow basis,
|
|
645
|
+
* not the visible width, decides the wrap point. Margins can't keep the actions off a lonely wrapped
|
|
646
|
+
* row; to avoid stranded actions, tune the --w-grow basis or drop a field so the bar fits one row. */
|
|
639
647
|
.fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
|
|
648
|
+
/* Opt-in: sit the actions inline right after the last field instead of at the far edge — so when the
|
|
649
|
+
* bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
|
|
650
|
+
.fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
|
|
640
651
|
/* Stack on narrow screens so nothing overflows the row. */
|
|
641
652
|
@media (max-width:640px){
|
|
642
|
-
.fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
|
|
653
|
+
.fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
|
|
643
654
|
.fdy-filterbar__actions{width:100%;margin-left:0;}
|
|
644
655
|
}
|
|
645
656
|
|
|
@@ -677,6 +688,11 @@ a { color: var(--color-primary); }
|
|
|
677
688
|
|
|
678
689
|
/* Freeday — Field & Input */
|
|
679
690
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
691
|
+
/* A grouped field is legitimately a <fieldset> (it needs a <legend>) — e.g. a radio/checkbox group.
|
|
692
|
+
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
693
|
+
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
694
|
+
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
695
|
+
fieldset.fdy-field>legend{padding:0;float:none;}
|
|
680
696
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
681
697
|
.fdy-input,.fdy-textarea{width:100%;height:var(--control-h);padding:0 var(--space-3);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
|
|
682
698
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
|
@@ -878,6 +894,8 @@ a { color: var(--color-primary); }
|
|
|
878
894
|
comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
|
|
879
895
|
.fdy-table__row--activatable{cursor:pointer;}
|
|
880
896
|
.fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
|
|
897
|
+
/* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
|
|
898
|
+
.fdy-table__detailrow>td,.fdy-table__detailrow:hover>td{background:var(--color-surface-2);}
|
|
881
899
|
.fdy-table th[aria-sort]{cursor:pointer;}
|
|
882
900
|
.fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
|
|
883
901
|
.fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
|
package/dist/freeday.js
CHANGED
|
@@ -986,17 +986,31 @@
|
|
|
986
986
|
svg.appendChild(tk);
|
|
987
987
|
}
|
|
988
988
|
|
|
989
|
-
// x category labels with autoskip.
|
|
990
|
-
|
|
989
|
+
// x category labels with autoskip. Labels are author-supplied, so estimate the widest one instead
|
|
990
|
+
// of assuming a fixed 40px slot; keep every step-th plus the last, then drop any kept label that
|
|
991
|
+
// would collide with the one before it — including the forced last one (a fraction-of-a-slot
|
|
992
|
+
// overlap happens when n-1 lands next to the last kept multiple of step, e.g. n=24 at most widths).
|
|
993
|
+
var charW = 5.4; // ~9px font advance (.fdy-chart-xy__xlabel)
|
|
994
|
+
var widest = 0;
|
|
995
|
+
for (var wi = 0; wi < n; wi++) widest = Math.max(widest, String(labels[wi] || ('#' + (wi + 1))).length * charW);
|
|
996
|
+
var minGap = widest + 8;
|
|
997
|
+
var maxTicks = Math.max(2, Math.floor(plotW / minGap)), step = Math.ceil(n / maxTicks);
|
|
998
|
+
var xPos = function (i) { return (type === 'bar') ? xCenter(i) : xLine(i); };
|
|
999
|
+
var kept = [];
|
|
991
1000
|
for (var xi = 0; xi < n; xi++) {
|
|
992
|
-
if (xi % step
|
|
993
|
-
|
|
1001
|
+
if (xi % step === 0 || xi === n - 1) kept.push(xi);
|
|
1002
|
+
}
|
|
1003
|
+
// The last label is worth forcing, but not on top of its neighbour — drop the penultimate one.
|
|
1004
|
+
if (kept.length >= 2 && xPos(kept[kept.length - 1]) - xPos(kept[kept.length - 2]) < minGap) {
|
|
1005
|
+
kept.splice(kept.length - 2, 1);
|
|
1006
|
+
}
|
|
1007
|
+
kept.forEach(function (xk) {
|
|
994
1008
|
var xt = svgEl('text');
|
|
995
|
-
xt.setAttribute('x',
|
|
1009
|
+
xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - 6);
|
|
996
1010
|
xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
|
|
997
|
-
xt.textContent = labels[
|
|
1011
|
+
xt.textContent = labels[xk] || ('#' + (xk + 1));
|
|
998
1012
|
svg.appendChild(xt);
|
|
999
|
-
}
|
|
1013
|
+
});
|
|
1000
1014
|
|
|
1001
1015
|
if (type === 'bar') {
|
|
1002
1016
|
var S = series.length;
|
|
@@ -1139,7 +1153,10 @@
|
|
|
1139
1153
|
li.appendChild(document.createTextNode((labels[i] || ('#' + (i + 1))) + ' — ' + Math.round((v / total) * 100) + '%'));
|
|
1140
1154
|
legend.appendChild(li);
|
|
1141
1155
|
});
|
|
1142
|
-
el.appendChild(ring);
|
|
1156
|
+
el.appendChild(ring);
|
|
1157
|
+
// Honour data-fdy-legend for the donut too — a caller may supply its own richer legend. 'auto'
|
|
1158
|
+
// keeps today's behaviour (a donut is unreadable without labels, so it shows); 'none' suppresses it.
|
|
1159
|
+
if ((el.getAttribute('data-fdy-legend') || 'auto') !== 'none') el.appendChild(legend);
|
|
1143
1160
|
ensureImg(el);
|
|
1144
1161
|
}
|
|
1145
1162
|
|
package/package.json
CHANGED
package/src/components/badge.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Freeday — Badge */
|
|
2
|
-
.fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
|
|
2
|
+
.fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;white-space:nowrap;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
|
|
3
3
|
.fdy-badge--success{color:var(--color-success-strong);background:var(--color-success-soft);border-color:color-mix(in srgb,var(--color-success) 26%,transparent);}
|
|
4
4
|
.fdy-badge--warning{color:var(--color-warning-strong);background:var(--color-warning-soft);border-color:color-mix(in srgb,var(--color-warning) 26%,transparent);}
|
|
5
5
|
.fdy-badge--danger{color:var(--color-danger-strong);background:var(--color-danger-soft);border-color:color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
/* Responsive: each picker may shrink (min-width:0) & wrap so the range never overflows a
|
|
39
39
|
narrow row/phone; inline-flex still hugs to ~23rem at wider widths (look unchanged). */
|
|
40
40
|
.fdy-daterange{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);}
|
|
41
|
-
|
|
41
|
+
/* Each picker shrinks from an 11rem basis but not below ~7rem — enough for "dd/mm/yyyy" + the icon,
|
|
42
|
+
so a squeezed range compresses gracefully (and wraps) instead of clipping the date. */
|
|
43
|
+
.fdy-daterange .fdy-datepicker{flex:0 1 11rem;min-width:7rem;}
|
|
42
44
|
.fdy-daterange__sep{flex:none;color:var(--color-text-subtle);}
|
|
43
45
|
|
|
44
46
|
@media (prefers-reduced-motion:no-preference){.fdy-datepicker__panel:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
|
|
@@ -14,12 +14,21 @@
|
|
|
14
14
|
.fdy-filterbar>.fdy-field--w-sm{width:9rem;}
|
|
15
15
|
.fdy-filterbar>.fdy-field--w-lg{width:15rem;}
|
|
16
16
|
.fdy-filterbar>.fdy-field--w-xl{width:18rem;}
|
|
17
|
+
/* Wide enough for a two-picker date range (~25rem) so it need not burn the single --w-grow slot. */
|
|
18
|
+
.fdy-filterbar>.fdy-field--w-2xl{width:25rem;}
|
|
17
19
|
/* One growing field (usually the search) absorbs the remaining width. */
|
|
18
20
|
.fdy-filterbar>.fdy-field--w-grow{flex:1 1 16rem;width:auto;min-width:12rem;}
|
|
19
|
-
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline.
|
|
21
|
+
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline.
|
|
22
|
+
* Wrap note: a wrapped bar breaks flex lines on each item's *flex-basis* (the grow field's
|
|
23
|
+
* `flex:1 1 16rem` reserves 16rem for line-breaking), NOT its rendered width — so the grow basis,
|
|
24
|
+
* not the visible width, decides the wrap point. Margins can't keep the actions off a lonely wrapped
|
|
25
|
+
* row; to avoid stranded actions, tune the --w-grow basis or drop a field so the bar fits one row. */
|
|
20
26
|
.fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
|
|
27
|
+
/* Opt-in: sit the actions inline right after the last field instead of at the far edge — so when the
|
|
28
|
+
* bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
|
|
29
|
+
.fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
|
|
21
30
|
/* Stack on narrow screens so nothing overflows the row. */
|
|
22
31
|
@media (max-width:640px){
|
|
23
|
-
.fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
|
|
32
|
+
.fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
|
|
24
33
|
.fdy-filterbar__actions{width:100%;margin-left:0;}
|
|
25
34
|
}
|
package/src/components/input.css
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/* Freeday — Field & Input */
|
|
2
2
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
3
|
+
/* A grouped field is legitimately a <fieldset> (it needs a <legend>) — e.g. a radio/checkbox group.
|
|
4
|
+
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
5
|
+
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
6
|
+
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
7
|
+
fieldset.fdy-field>legend{padding:0;float:none;}
|
|
3
8
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
4
9
|
.fdy-input,.fdy-textarea{width:100%;height:var(--control-h);padding:0 var(--space-3);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
|
|
5
10
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
package/src/components/table.css
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
|
|
20
20
|
.fdy-table__row--activatable{cursor:pointer;}
|
|
21
21
|
.fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
|
|
22
|
+
/* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
|
|
23
|
+
.fdy-table__detailrow>td,.fdy-table__detailrow:hover>td{background:var(--color-surface-2);}
|
|
22
24
|
.fdy-table th[aria-sort]{cursor:pointer;}
|
|
23
25
|
.fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
|
|
24
26
|
.fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
|
package/src/freeday-chart.js
CHANGED
|
@@ -213,17 +213,31 @@
|
|
|
213
213
|
svg.appendChild(tk);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
// x category labels with autoskip.
|
|
217
|
-
|
|
216
|
+
// x category labels with autoskip. Labels are author-supplied, so estimate the widest one instead
|
|
217
|
+
// of assuming a fixed 40px slot; keep every step-th plus the last, then drop any kept label that
|
|
218
|
+
// would collide with the one before it — including the forced last one (a fraction-of-a-slot
|
|
219
|
+
// overlap happens when n-1 lands next to the last kept multiple of step, e.g. n=24 at most widths).
|
|
220
|
+
var charW = 5.4; // ~9px font advance (.fdy-chart-xy__xlabel)
|
|
221
|
+
var widest = 0;
|
|
222
|
+
for (var wi = 0; wi < n; wi++) widest = Math.max(widest, String(labels[wi] || ('#' + (wi + 1))).length * charW);
|
|
223
|
+
var minGap = widest + 8;
|
|
224
|
+
var maxTicks = Math.max(2, Math.floor(plotW / minGap)), step = Math.ceil(n / maxTicks);
|
|
225
|
+
var xPos = function (i) { return (type === 'bar') ? xCenter(i) : xLine(i); };
|
|
226
|
+
var kept = [];
|
|
218
227
|
for (var xi = 0; xi < n; xi++) {
|
|
219
|
-
if (xi % step
|
|
220
|
-
|
|
228
|
+
if (xi % step === 0 || xi === n - 1) kept.push(xi);
|
|
229
|
+
}
|
|
230
|
+
// The last label is worth forcing, but not on top of its neighbour — drop the penultimate one.
|
|
231
|
+
if (kept.length >= 2 && xPos(kept[kept.length - 1]) - xPos(kept[kept.length - 2]) < minGap) {
|
|
232
|
+
kept.splice(kept.length - 2, 1);
|
|
233
|
+
}
|
|
234
|
+
kept.forEach(function (xk) {
|
|
221
235
|
var xt = svgEl('text');
|
|
222
|
-
xt.setAttribute('x',
|
|
236
|
+
xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - 6);
|
|
223
237
|
xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
|
|
224
|
-
xt.textContent = labels[
|
|
238
|
+
xt.textContent = labels[xk] || ('#' + (xk + 1));
|
|
225
239
|
svg.appendChild(xt);
|
|
226
|
-
}
|
|
240
|
+
});
|
|
227
241
|
|
|
228
242
|
if (type === 'bar') {
|
|
229
243
|
var S = series.length;
|
|
@@ -366,7 +380,10 @@
|
|
|
366
380
|
li.appendChild(document.createTextNode((labels[i] || ('#' + (i + 1))) + ' — ' + Math.round((v / total) * 100) + '%'));
|
|
367
381
|
legend.appendChild(li);
|
|
368
382
|
});
|
|
369
|
-
el.appendChild(ring);
|
|
383
|
+
el.appendChild(ring);
|
|
384
|
+
// Honour data-fdy-legend for the donut too — a caller may supply its own richer legend. 'auto'
|
|
385
|
+
// keeps today's behaviour (a donut is unreadable without labels, so it shows); 'none' suppresses it.
|
|
386
|
+
if ((el.getAttribute('data-fdy-legend') || 'auto') !== 'none') el.appendChild(legend);
|
|
370
387
|
ensureImg(el);
|
|
371
388
|
}
|
|
372
389
|
|