@cahyo-dimas/freeday 1.8.0 → 1.10.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 +48 -0
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/react/components/FdyTable.tsx +51 -8
- package/adapters/vue/components/FdyTable.vue +46 -6
- package/dist/freeday-chart.js +21 -7
- package/dist/freeday.bundle.css +17 -2
- package/dist/freeday.css +17 -2
- package/dist/freeday.js +21 -7
- package/package.json +1 -1
- package/src/components/datepicker.css +3 -1
- package/src/components/filterbar.css +3 -1
- package/src/components/input.css +5 -0
- package/src/components/table.css +6 -0
- package/src/freeday-chart.js +21 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,54 @@
|
|
|
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.10.0] — 2026-07-28
|
|
7
|
+
Follow-ups from the doc-ai-automation migration (improvement notes #27–#30). Additive / corrective;
|
|
8
|
+
no breaking changes.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`<fieldset class="fdy-field">` no longer leaks native browser chrome (#27).** The kit shipped no
|
|
12
|
+
`fieldset` reset, so a grouped field (a radio/checkbox group with a `<legend>`) showed the UA groove
|
|
13
|
+
border, extra padding, and `min-inline-size:min-content` — the last of which also blocked the
|
|
14
|
+
filterbar column widths from shrinking it. Added a scoped `fieldset.fdy-field` reset so it lays out
|
|
15
|
+
identically to the `<div>` form.
|
|
16
|
+
- **Chart x-axis: the last label no longer collides with its neighbour (#29).** `freeday-chart.js`
|
|
17
|
+
force-drew the final label with no spacing check, so on a dense axis (e.g. 24 hourly points) the
|
|
18
|
+
last two labels fused. It now estimates the widest label instead of assuming a fixed 40px slot, and
|
|
19
|
+
drops the penultimate label when the forced-last one would overlap it. Visual only; sparse axes
|
|
20
|
+
render identically.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`FdyTable` expandable detail rows (#30).** A `row-detail` slot (Vue) / `renderRowDetail` prop
|
|
24
|
+
(React) renders a full-width row beneath any expanded row, driven by a **controlled** `expandedKeys`
|
|
25
|
+
(matches the sort/filter/page contract; pairs with `row-activate` as the toggle). Adds a
|
|
26
|
+
`.fdy-table__detailrow` class (muted panel, no hover tint). Removes the last interaction that kept a
|
|
27
|
+
table hand-rolled.
|
|
28
|
+
- **`.fdy-field--w-2xl` (25rem) filterbar column (#28).** Wide enough for a two-picker date range, so
|
|
29
|
+
it need not burn the single `--w-grow` slot. The date-range pickers also gained a `min-width` (7rem)
|
|
30
|
+
so a squeezed range compresses and wraps instead of clipping the date (was `min-width:0`).
|
|
31
|
+
|
|
32
|
+
## [1.9.0] — 2026-07-28
|
|
33
|
+
Follow-ups to 1.8.0's `FdyTable` (improvement notes #25/#26). Additive — the #25 change **widens** a
|
|
34
|
+
generic bound, so every call site that compiled before still compiles.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- **`FdyTable` now accepts `interface`-typed rows.** The row generic was constrained to
|
|
38
|
+
`Record<string, unknown>`, to which a TypeScript `interface` is never assignable (interfaces get no
|
|
39
|
+
implicit index signature) — so a normally-typed DTO array failed to compile and `FdyTable` was
|
|
40
|
+
effectively unusable in a strict-TS app. Widened the bound to `object` in both adapters; the row type
|
|
41
|
+
now **infers** correctly (`cell-*` slots / `renderCell` are properly typed) and no cast is pushed
|
|
42
|
+
onto consumers. The component never indexes rows directly — all access goes through the
|
|
43
|
+
already-unconstrained core, which is unchanged.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- **`FdyTable` row activation (opt-in).** `rowActivatable` makes rows focusable and emits
|
|
47
|
+
`row-activate` (Vue) / calls `onRowActivate` (React) on click, or Enter/Space while the row itself is
|
|
48
|
+
focused — a control inside a cell keeps its own event (the `target !== currentTarget` guard). Adds a
|
|
49
|
+
`rowClass(row)` hook for per-row state (e.g. a selected row) and a `.fdy-table__row--activatable`
|
|
50
|
+
class (pointer + `:focus-visible` ring; hover tint is already inherited from the base row rule).
|
|
51
|
+
Replaces the hand-rolled `<tr tabindex="0" @click @keydown>` + duplicated row CSS consumers were
|
|
52
|
+
each re-writing.
|
|
53
|
+
|
|
6
54
|
## [1.8.0] — 2026-07-28
|
|
7
55
|
Release **1.8 — framework-safe data table, modal/drawer wrappers, and a monospace utility**.
|
|
8
56
|
Non-breaking, purely additive. No existing selector, markup, or enhancer changed.
|
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.10.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.10.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,
|
|
@@ -29,7 +29,7 @@ import { FdyTableFilter } from './FdyTableFilter';
|
|
|
29
29
|
// `onSortChange` / `onFiltersChange` / `onPageChange` for the caller to feed back into its query.
|
|
30
30
|
// Column filters (text/enum/number/date) apply live; in server mode, debounce the callback if needed.
|
|
31
31
|
|
|
32
|
-
export interface FdyTableProps<Row extends
|
|
32
|
+
export interface FdyTableProps<Row extends object> {
|
|
33
33
|
columns: ReadonlyArray<FdyTableColumn<Row>>;
|
|
34
34
|
rows: ReadonlyArray<Row>;
|
|
35
35
|
rowKey: (row: Row) => string | number;
|
|
@@ -51,9 +51,19 @@ export interface FdyTableProps<Row extends Record<string, unknown>> {
|
|
|
51
51
|
renderCell?: (column: FdyTableColumn<Row>, row: Row, value: unknown) => ReactNode;
|
|
52
52
|
toolbar?: ReactNode;
|
|
53
53
|
empty?: ReactNode;
|
|
54
|
+
/** Opt in to row activation: rows become focusable and call `onRowActivate` on click/Enter/Space. */
|
|
55
|
+
rowActivatable?: boolean;
|
|
56
|
+
/** Per-row class hook, e.g. to mark a selected row. */
|
|
57
|
+
rowClass?: (row: Row) => string | undefined;
|
|
58
|
+
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
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;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
export function FdyTable<Row extends
|
|
66
|
+
export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Element {
|
|
57
67
|
const serverPaged: boolean = props.page != null;
|
|
58
68
|
const sortControlled: boolean = serverPaged || props.sort !== undefined;
|
|
59
69
|
const filtersControlled: boolean = serverPaged || props.filters !== undefined;
|
|
@@ -143,6 +153,24 @@ export function FdyTable<Row extends Record<string, unknown>>(props: FdyTablePro
|
|
|
143
153
|
function alignStyle(col: FdyTableColumn<Row>): CSSProperties | undefined {
|
|
144
154
|
return col.align !== undefined ? { textAlign: col.align } : undefined;
|
|
145
155
|
}
|
|
156
|
+
function rowClassName(row: Row): string | undefined {
|
|
157
|
+
const cls: string = [props.rowClass?.(row), props.rowActivatable === true ? 'fdy-table__row--activatable' : undefined]
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join(' ');
|
|
160
|
+
return cls === '' ? undefined : cls;
|
|
161
|
+
}
|
|
162
|
+
// Enter/Space activate only when the row itself is focused — a control inside a cell keeps its own
|
|
163
|
+
// event (the `event.target !== event.currentTarget` guard). Click relies on inner controls calling
|
|
164
|
+
// stopPropagation, matching the pattern consumers hand-roll today.
|
|
165
|
+
function onRowKeydown(e: React.KeyboardEvent<HTMLTableRowElement>, row: Row): void {
|
|
166
|
+
if (props.rowActivatable !== true || e.target !== e.currentTarget) return;
|
|
167
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
props.onRowActivate?.(row);
|
|
170
|
+
}
|
|
171
|
+
function isExpanded(row: Row): boolean {
|
|
172
|
+
return props.expandedKeys?.includes(props.rowKey(row)) === true;
|
|
173
|
+
}
|
|
146
174
|
function renderCellContent(col: FdyTableColumn<Row>, row: Row): ReactNode {
|
|
147
175
|
if (props.renderCell !== undefined) {
|
|
148
176
|
const custom: ReactNode = props.renderCell(col, row, cellValue(row, col));
|
|
@@ -192,11 +220,26 @@ export function FdyTable<Row extends Record<string, unknown>>(props: FdyTablePro
|
|
|
192
220
|
</tr>
|
|
193
221
|
) : (
|
|
194
222
|
displayRows.map((row: Row): JSX.Element => (
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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>
|
|
200
243
|
))
|
|
201
244
|
)}
|
|
202
245
|
</tbody>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts" generic="Row extends
|
|
1
|
+
<script setup lang="ts" generic="Row extends object">
|
|
2
2
|
import { computed, ref, watch, type ComputedRef, type Ref } from 'vue';
|
|
3
3
|
import {
|
|
4
4
|
cellValue,
|
|
@@ -44,12 +44,20 @@ const props = defineProps<{
|
|
|
44
44
|
loading?: boolean;
|
|
45
45
|
emptyText?: string;
|
|
46
46
|
ariaLabel?: string;
|
|
47
|
+
/** Opt in to row activation: rows become focusable and emit `row-activate` on click/Enter/Space. */
|
|
48
|
+
rowActivatable?: boolean;
|
|
49
|
+
/** Per-row class hook, e.g. to mark a selected row. */
|
|
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>;
|
|
47
53
|
}>();
|
|
48
54
|
|
|
49
55
|
const emit = defineEmits<{
|
|
50
56
|
'update:sort': [sort: FdySortState | null];
|
|
51
57
|
'update:filters': [filters: FdyFilterMap];
|
|
52
58
|
'update:page': [page: FdyPageState];
|
|
59
|
+
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
60
|
+
'row-activate': [row: Row];
|
|
53
61
|
}>();
|
|
54
62
|
|
|
55
63
|
const internalSort: Ref<FdySortState | null> = ref(null);
|
|
@@ -162,6 +170,25 @@ function cellClass(col: FdyTableColumn<Row>): string | undefined {
|
|
|
162
170
|
function alignStyle(col: FdyTableColumn<Row>): Record<string, string> | undefined {
|
|
163
171
|
return col.align !== undefined ? { textAlign: col.align } : undefined;
|
|
164
172
|
}
|
|
173
|
+
|
|
174
|
+
function rowClasses(row: Row): Array<string | undefined> {
|
|
175
|
+
return [props.rowClass?.(row), props.rowActivatable === true ? 'fdy-table__row--activatable' : undefined];
|
|
176
|
+
}
|
|
177
|
+
function onRowClick(row: Row): void {
|
|
178
|
+
if (props.rowActivatable === true) emit('row-activate', row);
|
|
179
|
+
}
|
|
180
|
+
// Enter/Space activate only when the row itself is focused — a control inside a cell keeps its own
|
|
181
|
+
// event (the `event.target !== event.currentTarget` guard). Click relies on inner controls calling
|
|
182
|
+
// stopPropagation, matching the pattern consumers hand-roll today.
|
|
183
|
+
function onRowKeydown(e: KeyboardEvent, row: Row): void {
|
|
184
|
+
if (props.rowActivatable !== true || e.target !== e.currentTarget) return;
|
|
185
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
emit('row-activate', row);
|
|
188
|
+
}
|
|
189
|
+
function isExpanded(row: Row): boolean {
|
|
190
|
+
return props.expandedKeys?.includes(props.rowKey(row)) === true;
|
|
191
|
+
}
|
|
165
192
|
</script>
|
|
166
193
|
|
|
167
194
|
<template>
|
|
@@ -202,11 +229,24 @@ function alignStyle(col: FdyTableColumn<Row>): Record<string, string> | undefine
|
|
|
202
229
|
<slot name="empty">{{ emptyText ?? 'No data' }}</slot>
|
|
203
230
|
</td>
|
|
204
231
|
</tr>
|
|
205
|
-
<
|
|
206
|
-
<
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
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>
|
|
210
250
|
</tbody>
|
|
211
251
|
</table>
|
|
212
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;
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -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,15 @@ 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
987
|
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline. */
|
|
984
988
|
.fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
|
|
985
989
|
/* Stack on narrow screens so nothing overflows the row. */
|
|
986
990
|
@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%;}
|
|
991
|
+
.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
992
|
.fdy-filterbar__actions{width:100%;margin-left:0;}
|
|
989
993
|
}
|
|
990
994
|
|
|
@@ -1022,6 +1026,11 @@ a { color: var(--color-primary); }
|
|
|
1022
1026
|
|
|
1023
1027
|
/* Freeday — Field & Input */
|
|
1024
1028
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
1029
|
+
/* A grouped field is legitimately a <fieldset> (it needs a <legend>) — e.g. a radio/checkbox group.
|
|
1030
|
+
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
1031
|
+
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
1032
|
+
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
1033
|
+
fieldset.fdy-field>legend{padding:0;float:none;}
|
|
1025
1034
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
1026
1035
|
.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
1036
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
|
@@ -1219,6 +1228,12 @@ a { color: var(--color-primary); }
|
|
|
1219
1228
|
.fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
|
|
1220
1229
|
/* Loading / empty state cell (FdyTable spans it across all columns). */
|
|
1221
1230
|
.fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
|
|
1231
|
+
/* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
|
|
1232
|
+
comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
|
|
1233
|
+
.fdy-table__row--activatable{cursor:pointer;}
|
|
1234
|
+
.fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
|
|
1235
|
+
/* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
|
|
1236
|
+
.fdy-table__detailrow>td,.fdy-table__detailrow:hover>td{background:var(--color-surface-2);}
|
|
1222
1237
|
.fdy-table th[aria-sort]{cursor:pointer;}
|
|
1223
1238
|
.fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
|
|
1224
1239
|
.fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
|
package/dist/freeday.css
CHANGED
|
@@ -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,15 @@ 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
642
|
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline. */
|
|
639
643
|
.fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
|
|
640
644
|
/* Stack on narrow screens so nothing overflows the row. */
|
|
641
645
|
@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%;}
|
|
646
|
+
.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
647
|
.fdy-filterbar__actions{width:100%;margin-left:0;}
|
|
644
648
|
}
|
|
645
649
|
|
|
@@ -677,6 +681,11 @@ a { color: var(--color-primary); }
|
|
|
677
681
|
|
|
678
682
|
/* Freeday — Field & Input */
|
|
679
683
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
684
|
+
/* A grouped field is legitimately a <fieldset> (it needs a <legend>) — e.g. a radio/checkbox group.
|
|
685
|
+
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
686
|
+
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
687
|
+
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
688
|
+
fieldset.fdy-field>legend{padding:0;float:none;}
|
|
680
689
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
681
690
|
.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
691
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
|
@@ -874,6 +883,12 @@ a { color: var(--color-primary); }
|
|
|
874
883
|
.fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
|
|
875
884
|
/* Loading / empty state cell (FdyTable spans it across all columns). */
|
|
876
885
|
.fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
|
|
886
|
+
/* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
|
|
887
|
+
comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
|
|
888
|
+
.fdy-table__row--activatable{cursor:pointer;}
|
|
889
|
+
.fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
|
|
890
|
+
/* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
|
|
891
|
+
.fdy-table__detailrow>td,.fdy-table__detailrow:hover>td{background:var(--color-surface-2);}
|
|
877
892
|
.fdy-table th[aria-sort]{cursor:pointer;}
|
|
878
893
|
.fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
|
|
879
894
|
.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;
|
package/package.json
CHANGED
|
@@ -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,14 @@
|
|
|
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
21
|
/* Trailing actions (buttons) pushed to the end, aligned with the controls' baseline. */
|
|
20
22
|
.fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
|
|
21
23
|
/* Stack on narrow screens so nothing overflows the row. */
|
|
22
24
|
@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%;}
|
|
25
|
+
.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
26
|
.fdy-filterbar__actions{width:100%;margin-left:0;}
|
|
25
27
|
}
|
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
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
.fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
|
|
16
16
|
/* Loading / empty state cell (FdyTable spans it across all columns). */
|
|
17
17
|
.fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
|
|
18
|
+
/* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
|
|
19
|
+
comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
|
|
20
|
+
.fdy-table__row--activatable{cursor:pointer;}
|
|
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);}
|
|
18
24
|
.fdy-table th[aria-sort]{cursor:pointer;}
|
|
19
25
|
.fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
|
|
20
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;
|