@cahyo-dimas/freeday 3.5.0 → 3.6.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 +84 -0
- package/COMPONENTS.md +251 -8
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/blazor/FdyDateRange.razor.cs +10 -0
- package/adapters/blazor/FdyTable.razor +6 -2
- package/adapters/blazor/FdyTable.razor.cs +21 -0
- package/adapters/blazor/TableTypes.cs +4 -0
- package/adapters/react/components/FdyDateRange.tsx +8 -2
- package/adapters/react/components/FdyTable.tsx +24 -3
- package/adapters/react/index.d.ts +1 -1
- package/adapters/vue/components/FdyDateRange.vue +11 -5
- package/adapters/vue/components/FdyTable.vue +18 -2
- package/dist/freeday-confirm.js +101 -0
- package/dist/freeday-copy.js +58 -0
- package/dist/freeday-datepicker.js +17 -2
- package/dist/freeday-form.js +30 -1
- package/dist/freeday-search.js +77 -0
- package/dist/freeday.bundle.css +83 -6
- package/dist/freeday.css +83 -6
- package/dist/freeday.d.ts +17 -2
- package/dist/freeday.js +286 -3
- package/docs/agent-onboarding.md +8 -0
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
- package/src/components/avatar.css +2 -0
- package/src/components/card.css +7 -0
- package/src/components/composition.css +36 -0
- package/src/components/description-list.css +11 -1
- package/src/components/selection.css +6 -0
- package/src/components/states.css +7 -0
- package/src/components/table.css +14 -5
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cellText,
|
|
6
6
|
distinctValues,
|
|
7
7
|
filterRows,
|
|
8
|
+
isFilterActive,
|
|
8
9
|
sortRows,
|
|
9
10
|
paginate,
|
|
10
11
|
} from '../../core/table-model.js';
|
|
@@ -29,6 +30,14 @@ import { FdyTableFooter } from './FdyTableFooter';
|
|
|
29
30
|
// `onSortChange` / `onFiltersChange` / `onPageChange` for the caller to feed back into its query.
|
|
30
31
|
// Column filters (text/enum/number/date) apply live; in server mode, debounce the callback if needed.
|
|
31
32
|
|
|
33
|
+
/** What the `empty` render function is told (#069). */
|
|
34
|
+
export interface FdyTableEmptyContext {
|
|
35
|
+
/** An active column filter is what emptied the table. */
|
|
36
|
+
filtered: boolean;
|
|
37
|
+
/** Clears every column filter, through `onFiltersChange` when the filters are controlled. */
|
|
38
|
+
clearFilters: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
export interface FdyTableProps<Row extends object> {
|
|
33
42
|
columns: ReadonlyArray<FdyTableColumn<Row>>;
|
|
34
43
|
rows: ReadonlyArray<Row>;
|
|
@@ -79,7 +88,9 @@ export interface FdyTableProps<Row extends object> {
|
|
|
79
88
|
/** Custom cell renderer; return undefined for a column to use the default text. */
|
|
80
89
|
renderCell?: (column: FdyTableColumn<Row>, row: Row, value: unknown) => ReactNode;
|
|
81
90
|
toolbar?: ReactNode;
|
|
82
|
-
empty
|
|
91
|
+
/** The empty state. As a function it is told whether the column filters are what emptied the
|
|
92
|
+
* table, and how to clear them: "no match" wants a Clear button, "nothing yet" a Create one. */
|
|
93
|
+
empty?: ReactNode | ((ctx: FdyTableEmptyContext) => ReactNode);
|
|
83
94
|
/** Opt in to row activation: rows become focusable and call `onRowActivate` on click/Enter/Space. */
|
|
84
95
|
rowActivatable?: boolean;
|
|
85
96
|
/** Per-row class hook, e.g. to mark a selected row. */
|
|
@@ -96,6 +107,8 @@ export interface FdyTableProps<Row extends object> {
|
|
|
96
107
|
renderRowDetail?: (row: Row) => ReactNode;
|
|
97
108
|
/** Zebra-stripe the body rows. */
|
|
98
109
|
striped?: boolean;
|
|
110
|
+
/** Hover only the rows that do something (`rowActivatable`); inert rows keep their colour. */
|
|
111
|
+
hoverActionable?: boolean;
|
|
99
112
|
/** Render the checkbox column and the bulk bar. */
|
|
100
113
|
selectable?: boolean;
|
|
101
114
|
/** Controlled selection, as `rowKey` values. Provide to own it; omit for internal (the column
|
|
@@ -213,6 +226,14 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
213
226
|
setClientPage(0);
|
|
214
227
|
}
|
|
215
228
|
}
|
|
229
|
+
const filtered: boolean = Object.values(effectiveFilters).some((f: FdyColumnFilter): boolean => isFilterActive(f));
|
|
230
|
+
function clearFilters(): void {
|
|
231
|
+
if (filtersControlled) props.onFiltersChange?.({});
|
|
232
|
+
else {
|
|
233
|
+
setInternalFilters({});
|
|
234
|
+
setClientPage(0);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
216
237
|
function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null): void {
|
|
217
238
|
const nextMap: FdyFilterMap = { ...effectiveFilters };
|
|
218
239
|
if (filter === null) delete nextMap[col.key];
|
|
@@ -352,7 +373,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
352
373
|
|
|
353
374
|
<div className="fdy-table-scroll">
|
|
354
375
|
<table
|
|
355
|
-
className={props.striped === true ? 'fdy-table
|
|
376
|
+
className={['fdy-table', props.striped === true ? 'fdy-table--striped' : '', props.hoverActionable === true ? 'fdy-table--hover-actionable' : ''].filter(Boolean).join(' ')}
|
|
356
377
|
aria-label={props.ariaLabel}
|
|
357
378
|
>
|
|
358
379
|
<thead>
|
|
@@ -402,7 +423,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
402
423
|
</tr>
|
|
403
424
|
) : displayRows.length === 0 ? (
|
|
404
425
|
<tr>
|
|
405
|
-
<td colSpan={colCount} className="fdy-table__state">{props.empty ?? props.emptyText ?? 'No data'}</td>
|
|
426
|
+
<td colSpan={colCount} className="fdy-table__state">{typeof props.empty === 'function' ? props.empty({ filtered, clearFilters }) : (props.empty ?? props.emptyText ?? 'No data')}</td>
|
|
406
427
|
</tr>
|
|
407
428
|
) : (
|
|
408
429
|
displayRows.map((row: Row): JSX.Element => (
|
|
@@ -38,7 +38,7 @@ export { FdyCfl, type FdyCflProps, type FdyCflHandle, type CflColumn, type CflPa
|
|
|
38
38
|
* on the array only a `multiple` field emits. */
|
|
39
39
|
export { singleRow } from '../core/cfl-value';
|
|
40
40
|
export { FdyChart, type FdyChartProps, type FdyChartSeries } from './components/FdyChart';
|
|
41
|
-
export { FdyTable, type FdyTableProps } from './components/FdyTable';
|
|
41
|
+
export { FdyTable, type FdyTableProps, type FdyTableEmptyContext } from './components/FdyTable';
|
|
42
42
|
/** The table's own footer, standalone, for a responsive list whose table is hidden at some
|
|
43
43
|
* breakpoints and so cannot host it. */
|
|
44
44
|
export { FdyTableFooter, type FdyTableFooterProps } from './components/FdyTableFooter';
|
|
@@ -34,20 +34,26 @@ const props = defineProps<{
|
|
|
34
34
|
const emit = defineEmits<{
|
|
35
35
|
'update:modelValue': [value: DateRangeValue];
|
|
36
36
|
change: [value: DateRangeValue];
|
|
37
|
+
/** Both ends chosen, or both cleared: the only states worth reloading a list for (#063). */
|
|
38
|
+
settled: [value: DateRangeValue];
|
|
37
39
|
}>();
|
|
38
40
|
|
|
39
41
|
const startMax: ComputedRef<string | undefined> = computed((): string | undefined => props.modelValue.end ?? props.max);
|
|
40
42
|
const endMin: ComputedRef<string | undefined> = computed((): string | undefined => props.modelValue.start ?? props.min);
|
|
41
43
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
+
function isSettled(range: DateRangeValue): boolean {
|
|
45
|
+
return !range.start === !range.end;
|
|
46
|
+
}
|
|
47
|
+
function commit(next: DateRangeValue): void {
|
|
44
48
|
emit('update:modelValue', next);
|
|
45
49
|
emit('change', next);
|
|
50
|
+
if (isSettled(next)) emit('settled', next);
|
|
51
|
+
}
|
|
52
|
+
function setStart(start: string): void {
|
|
53
|
+
commit({ start, end: props.modelValue.end });
|
|
46
54
|
}
|
|
47
55
|
function setEnd(end: string): void {
|
|
48
|
-
|
|
49
|
-
emit('update:modelValue', next);
|
|
50
|
-
emit('change', next);
|
|
56
|
+
commit({ start: props.modelValue.start, end });
|
|
51
57
|
}
|
|
52
58
|
</script>
|
|
53
59
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cellText,
|
|
6
6
|
distinctValues,
|
|
7
7
|
filterRows,
|
|
8
|
+
isFilterActive,
|
|
8
9
|
sortRows,
|
|
9
10
|
paginate,
|
|
10
11
|
} from '../../core/table-model.js';
|
|
@@ -78,6 +79,8 @@ const props = withDefaults(defineProps<{
|
|
|
78
79
|
expandedKeys?: ReadonlyArray<string | number>;
|
|
79
80
|
/** Zebra-stripe the body rows. */
|
|
80
81
|
striped?: boolean;
|
|
82
|
+
/** Hover only the rows that do something (`rowActivatable`); inert rows keep their colour. */
|
|
83
|
+
hoverActionable?: boolean;
|
|
81
84
|
/** Render the checkbox column and the bulk bar. */
|
|
82
85
|
selectable?: boolean;
|
|
83
86
|
/** Controlled selection, as `rowKey` values. Provide to own it; omit for internal (the column
|
|
@@ -222,6 +225,19 @@ function onSort(col: FdyTableColumn<Row>): void {
|
|
|
222
225
|
setClientPage(0);
|
|
223
226
|
}
|
|
224
227
|
}
|
|
228
|
+
/* "Nothing matches these filters" and "nothing here yet" are different news (#069): the first wants
|
|
229
|
+
a Clear button, the second a Create one. The table owns its column filters, so it is the one
|
|
230
|
+
that knows which it is; the `empty` slot receives both. */
|
|
231
|
+
const filtered: ComputedRef<boolean> = computed((): boolean =>
|
|
232
|
+
Object.values(effectiveFilters.value).some((f: FdyColumnFilter): boolean => isFilterActive(f)),
|
|
233
|
+
);
|
|
234
|
+
function clearFilters(): void {
|
|
235
|
+
if (filtersControlled.value) emit('update:filters', {});
|
|
236
|
+
else {
|
|
237
|
+
internalFilters.value = {};
|
|
238
|
+
setClientPage(0);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
225
241
|
function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null): void {
|
|
226
242
|
const nextMap: FdyFilterMap = { ...effectiveFilters.value };
|
|
227
243
|
if (filter === null) delete nextMap[col.key];
|
|
@@ -367,7 +383,7 @@ const colSpan: ComputedRef<number> = computed((): number =>
|
|
|
367
383
|
</div>
|
|
368
384
|
|
|
369
385
|
<div class="fdy-table-scroll">
|
|
370
|
-
<table class="fdy-table" :class="{ 'fdy-table--striped': striped }" :aria-label="ariaLabel">
|
|
386
|
+
<table class="fdy-table" :class="{ 'fdy-table--striped': striped, 'fdy-table--hover-actionable': hoverActionable }" :aria-label="ariaLabel">
|
|
371
387
|
<thead>
|
|
372
388
|
<tr>
|
|
373
389
|
<th v-if="selectable" class="fdy-table__selcol" scope="col">
|
|
@@ -406,7 +422,7 @@ const colSpan: ComputedRef<number> = computed((): number =>
|
|
|
406
422
|
</tr>
|
|
407
423
|
<tr v-else-if="displayRows.length === 0">
|
|
408
424
|
<td :colspan="colSpan" class="fdy-table__state">
|
|
409
|
-
<slot name="empty">{{ emptyText ?? 'No data' }}</slot>
|
|
425
|
+
<slot name="empty" :filtered="filtered" :clear-filters="clearFilters">{{ emptyText ?? 'No data' }}</slot>
|
|
410
426
|
</td>
|
|
411
427
|
</tr>
|
|
412
428
|
<template v-else>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* Freeday, confirm API (optional, zero-dependency). Note #060.
|
|
2
|
+
* Freeday.confirm({ title, message, details, acceptText, rejectText, tone }) opens a .fdy-modal
|
|
3
|
+
* and resolves a Promise<boolean>: true on accept; false on reject, Escape or a backdrop click.
|
|
4
|
+
* details: string[] shown as a list under the message, for a confirm that has to NAME its
|
|
5
|
+
* findings ("these three fields will be refused") instead of folding them into one
|
|
6
|
+
* long sentence
|
|
7
|
+
* tone: 'primary' (default) | 'danger', the accept button's variant
|
|
8
|
+
*
|
|
9
|
+
* One dialog, one answer: every way out resolves the same Promise, and a Promise settles once, so
|
|
10
|
+
* a caller never hears "no" after "yes" (a framework dialog that reports reject and then hide, or
|
|
11
|
+
* hide after accept, does exactly that). Text is set with textContent, never parsed as HTML.
|
|
12
|
+
* Focus starts on the reject button: Enter on a dialog that just opened must not delete anything.
|
|
13
|
+
* The dialog is removed when it closes; the native <dialog> returns focus to where it was.
|
|
14
|
+
* Buttons carry data-fdy-confirm-accept / data-fdy-confirm-reject, stable hooks for tests, because
|
|
15
|
+
* an accept labelled "Delete" collides by accessible name with every row's own Delete button.
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
/* User-facing strings, English by default. Per call, acceptText / rejectText override them; a
|
|
21
|
+
* label that names the action ("Delete order") is better than either default. */
|
|
22
|
+
var TEXT = {
|
|
23
|
+
accept: 'Confirm',
|
|
24
|
+
reject: 'Cancel'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
var seq = 0;
|
|
28
|
+
|
|
29
|
+
function el(tag, className, text) {
|
|
30
|
+
var node = document.createElement(tag);
|
|
31
|
+
if (className) node.className = className;
|
|
32
|
+
if (text != null) node.textContent = text;
|
|
33
|
+
return node;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function confirm(opts) {
|
|
37
|
+
opts = opts || {};
|
|
38
|
+
return new Promise(function (resolve) {
|
|
39
|
+
seq += 1;
|
|
40
|
+
var titleId = 'fdy-confirm-title-' + seq;
|
|
41
|
+
var dialog = el('dialog', 'fdy-modal fdy-modal--sm');
|
|
42
|
+
dialog.setAttribute('aria-labelledby', titleId);
|
|
43
|
+
dialog.setAttribute('data-fdy-confirm', '');
|
|
44
|
+
|
|
45
|
+
var header = el('header', 'fdy-modal__header');
|
|
46
|
+
var title = el('h2', 'fdy-modal__title', opts.title || '');
|
|
47
|
+
title.id = titleId;
|
|
48
|
+
header.appendChild(title);
|
|
49
|
+
|
|
50
|
+
var body = el('div', 'fdy-modal__body');
|
|
51
|
+
if (opts.message) {
|
|
52
|
+
var message = el('p', null, opts.message);
|
|
53
|
+
message.id = titleId + '-desc';
|
|
54
|
+
dialog.setAttribute('aria-describedby', message.id);
|
|
55
|
+
body.appendChild(message);
|
|
56
|
+
}
|
|
57
|
+
var details = Array.isArray(opts.details) ? opts.details : [];
|
|
58
|
+
if (details.length > 0) {
|
|
59
|
+
var list = el('ul');
|
|
60
|
+
details.forEach(function (line) { list.appendChild(el('li', null, line)); });
|
|
61
|
+
body.appendChild(list);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var footer = el('footer', 'fdy-modal__footer');
|
|
65
|
+
var reject = el('button', 'fdy-btn fdy-btn--ghost', opts.rejectText || TEXT.reject);
|
|
66
|
+
reject.type = 'button';
|
|
67
|
+
reject.setAttribute('data-fdy-confirm-reject', '');
|
|
68
|
+
var accept = el('button', opts.tone === 'danger' ? 'fdy-btn fdy-btn--danger' : 'fdy-btn', opts.acceptText || TEXT.accept);
|
|
69
|
+
accept.type = 'button';
|
|
70
|
+
accept.setAttribute('data-fdy-confirm-accept', '');
|
|
71
|
+
footer.appendChild(reject);
|
|
72
|
+
footer.appendChild(accept);
|
|
73
|
+
|
|
74
|
+
dialog.appendChild(header);
|
|
75
|
+
dialog.appendChild(body);
|
|
76
|
+
dialog.appendChild(footer);
|
|
77
|
+
|
|
78
|
+
function settle(answer) {
|
|
79
|
+
resolve(answer);
|
|
80
|
+
if (dialog.open) dialog.close();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
accept.addEventListener('click', function () { settle(true); });
|
|
84
|
+
reject.addEventListener('click', function () { settle(false); });
|
|
85
|
+
// Escape fires `cancel` then `close`; a backdrop click lands on the <dialog> itself, since its
|
|
86
|
+
// children fill the box edge to edge. Both mean "no".
|
|
87
|
+
dialog.addEventListener('close', function () {
|
|
88
|
+
settle(false);
|
|
89
|
+
if (dialog.parentNode) dialog.parentNode.removeChild(dialog);
|
|
90
|
+
});
|
|
91
|
+
dialog.addEventListener('click', function (e) { if (e.target === dialog) settle(false); });
|
|
92
|
+
|
|
93
|
+
document.body.appendChild(dialog);
|
|
94
|
+
dialog.showModal();
|
|
95
|
+
reject.focus();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.Freeday = window.Freeday || {};
|
|
100
|
+
window.Freeday.confirm = confirm;
|
|
101
|
+
})();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Freeday, copy-to-clipboard enhancer (optional, zero-dependency). Note #062.
|
|
2
|
+
* A button carrying data-fdy-copy="<value>" copies that value and says so with a toast. For
|
|
3
|
+
* reference values a reader takes elsewhere: an external document number, an event ID.
|
|
4
|
+
*
|
|
5
|
+
* <dd><span class="fdy-mono">INV-2026-00042</span>
|
|
6
|
+
* <button type="button" class="fdy-btn fdy-btn--ghost fdy-btn--icon fdy-btn--sm"
|
|
7
|
+
* data-fdy-copy="INV-2026-00042" aria-label="Copy invoice number">…svg…</button></dd>
|
|
8
|
+
*
|
|
9
|
+
* The value is the attribute, not a lookup of some neighbour's text: a framework writes it from
|
|
10
|
+
* the same variable as the visible text, and a row of a table needs no ids to find its own cell.
|
|
11
|
+
* One delegated listener on the document, so buttons rendered later need no hydration.
|
|
12
|
+
*
|
|
13
|
+
* The Clipboard API exists only in a secure context. Over plain http writeText is missing, and a
|
|
14
|
+
* copy that silently did nothing is the failure this refuses: the reader is told to copy by hand.
|
|
15
|
+
* Emits bubbling "fdy-copy" {value, ok} on the button. A click inside a clickable table row still
|
|
16
|
+
* reaches the row's own handler; stop it there if the row should not open. Exposes window.FreedayCopy.
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
/* User-facing strings, English by default and overridable per button with data-fdy-text-<key>,
|
|
22
|
+
* like every other enhancer's. */
|
|
23
|
+
var TEXT = {
|
|
24
|
+
copied: 'Copied.',
|
|
25
|
+
failed: 'Could not copy. Select the text and copy it manually.'
|
|
26
|
+
};
|
|
27
|
+
function textOf(el, key) {
|
|
28
|
+
var custom = el.getAttribute('data-fdy-text-' + key);
|
|
29
|
+
return custom != null && custom !== '' ? custom : TEXT[key];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function report(btn, value, ok) {
|
|
33
|
+
if (window.Freeday && typeof window.Freeday.toast === 'function') {
|
|
34
|
+
window.Freeday.toast(ok
|
|
35
|
+
? { variant: 'success', message: textOf(btn, 'copied'), key: 'fdy-copy' }
|
|
36
|
+
: { variant: 'danger', message: textOf(btn, 'failed'), key: 'fdy-copy', timeout: 0 });
|
|
37
|
+
}
|
|
38
|
+
btn.dispatchEvent(new CustomEvent('fdy-copy', { bubbles: true, detail: { value: value, ok: ok } }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function copy(btn) {
|
|
42
|
+
var value = btn.getAttribute('data-fdy-copy') || '';
|
|
43
|
+
if (value === '') return;
|
|
44
|
+
var clip = navigator.clipboard;
|
|
45
|
+
if (!clip || typeof clip.writeText !== 'function') { report(btn, value, false); return; }
|
|
46
|
+
clip.writeText(value).then(
|
|
47
|
+
function () { report(btn, value, true); },
|
|
48
|
+
function () { report(btn, value, false); }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
document.addEventListener('click', function (e) {
|
|
53
|
+
var btn = e.target.closest && e.target.closest('[data-fdy-copy]');
|
|
54
|
+
if (btn && !btn.disabled) copy(btn);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
window.FreedayCopy = { copy: copy };
|
|
58
|
+
})();
|
|
@@ -603,15 +603,30 @@
|
|
|
603
603
|
fromApi.setRange(f, t); // start stays free, picking a later start clears the end below
|
|
604
604
|
toApi.setRange(f, t);
|
|
605
605
|
}
|
|
606
|
+
/* An end the new start overtook is cleared, and the clear is ANNOUNCED like a pick: a binding
|
|
607
|
+
* that only hears picks (Blazor's To) otherwise keeps the end the range no longer has. */
|
|
608
|
+
function clearEnd(el, api) {
|
|
609
|
+
api.clear();
|
|
610
|
+
el.dispatchEvent(new CustomEvent('fdy-datepicker-change', { bubbles: true, detail: { value: '' } }));
|
|
611
|
+
}
|
|
612
|
+
/* "Settled" = both ends chosen, or both empty: the only states worth a reload (#063). A pick of
|
|
613
|
+
* the first end, or a start that overtook the end, is half a range. */
|
|
614
|
+
function settle() {
|
|
615
|
+
var f = fromApi.getValue(), t = toApi.getValue();
|
|
616
|
+
if ((f === '') !== (t === '')) return;
|
|
617
|
+
root.dispatchEvent(new CustomEvent('fdy-daterange-settled', { bubbles: true, detail: { from: f, to: t } }));
|
|
618
|
+
}
|
|
606
619
|
fromEl.addEventListener('fdy-datepicker-change', function () {
|
|
607
620
|
var f = fromApi.getValue(), t = toApi.getValue();
|
|
608
|
-
if (f && t && t < f) toApi
|
|
621
|
+
if (f && t && t < f) clearEnd(toEl, toApi);
|
|
609
622
|
sync();
|
|
623
|
+
settle();
|
|
610
624
|
});
|
|
611
625
|
toEl.addEventListener('fdy-datepicker-change', function () {
|
|
612
626
|
var f = fromApi.getValue(), t = toApi.getValue();
|
|
613
|
-
if (f && t && t < f) fromApi
|
|
627
|
+
if (f && t && t < f) clearEnd(fromEl, fromApi);
|
|
614
628
|
sync();
|
|
629
|
+
settle();
|
|
615
630
|
});
|
|
616
631
|
sync();
|
|
617
632
|
}
|
package/dist/freeday-form.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* data-fdy-msg-min | -max | -step | -mismatch, then generic data-fdy-msg.
|
|
14
14
|
* Cross-field match (e.g. confirm password): data-fdy-match="#otherId".
|
|
15
15
|
*
|
|
16
|
-
* Timing: validates on submit (blocks + focuses the first invalid control); after
|
|
16
|
+
* Timing: validates on submit (blocks, reveals + focuses the first invalid control); after
|
|
17
17
|
* the first submit attempt each field re-validates on input/blur so errors clear
|
|
18
18
|
* live. Add data-fdy-eager to validate on blur from the start.
|
|
19
19
|
*
|
|
@@ -126,6 +126,34 @@
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/* focus() on a control that is not rendered does nothing and reports nothing: the field turns red
|
|
130
|
+
* where nobody can see it, and the submit reads as a dead button (#059). So whatever hides the
|
|
131
|
+
* control is undone first, each through that component's own control, so its state agrees with
|
|
132
|
+
* what is shown:
|
|
133
|
+
* closed <details> opened (accordion, tree, nav group)
|
|
134
|
+
* hidden [role=tabpanel] its tab is clicked; freeday-tabs selects on click
|
|
135
|
+
* hidden .fdy-step-panel its step button is clicked. A step not yet reached has a disabled
|
|
136
|
+
* button, so this only ever goes BACK and never skips a step guard.
|
|
137
|
+
* Anything else that hides it belongs to the app, and is left alone. */
|
|
138
|
+
function reveal(el) {
|
|
139
|
+
for (var node = el.parentElement; node; node = node.parentElement) {
|
|
140
|
+
if (node.localName === 'details' && !node.open) {
|
|
141
|
+
node.open = true;
|
|
142
|
+
} else if (node.hidden && node.getAttribute('role') === 'tabpanel' && node.id) {
|
|
143
|
+
var tab = Array.prototype.filter.call(document.querySelectorAll('[role="tab"]'), function (t) {
|
|
144
|
+
return t.getAttribute('aria-controls') === node.id;
|
|
145
|
+
})[0];
|
|
146
|
+
if (tab) tab.click();
|
|
147
|
+
} else if (node.hidden && node.classList.contains('fdy-step-panel')) {
|
|
148
|
+
var stepper = node.closest('[data-fdy-stepper]');
|
|
149
|
+
if (!stepper) continue;
|
|
150
|
+
var index = Array.prototype.indexOf.call(stepper.querySelectorAll('.fdy-step-panel'), node);
|
|
151
|
+
var btn = stepper.querySelectorAll('.fdy-step__btn')[index];
|
|
152
|
+
if (btn && !btn.disabled) btn.click();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
function initForm(form) {
|
|
130
158
|
if (form.dataset.fdyFormReady === '1') return;
|
|
131
159
|
form.dataset.fdyFormReady = '1';
|
|
@@ -160,6 +188,7 @@
|
|
|
160
188
|
});
|
|
161
189
|
if (invalid.length) {
|
|
162
190
|
e.preventDefault();
|
|
191
|
+
reveal(invalid[0]);
|
|
163
192
|
invalid[0].focus();
|
|
164
193
|
form.dispatchEvent(new CustomEvent('fdy-form-invalid', { bubbles: true, detail: { invalid: invalid } }));
|
|
165
194
|
} else {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Freeday, list search enhancer (optional, zero-dependency). Note #061.
|
|
2
|
+
* Turns an .fdy-input-group into a search box that searches while the reader pauses, at once on
|
|
3
|
+
* Enter, and has a clear button. All three paths end in ONE bubbling event on the group,
|
|
4
|
+
* "fdy-search" {term}, so an app listens once instead of wiring keyup, a timer and a click.
|
|
5
|
+
*
|
|
6
|
+
* Markup (the group, not a new block; the clear button is yours, so is its aria-label):
|
|
7
|
+
* <div class="fdy-input-group" data-fdy-search [data-fdy-search-delay="400"]>
|
|
8
|
+
* <span class="fdy-input-group__addon fdy-input-group__addon--icon">…svg…</span>
|
|
9
|
+
* <input class="fdy-input" type="text" aria-label="Search orders">
|
|
10
|
+
* <button type="button" class="fdy-input-group__btn" data-fdy-search-clear aria-label="Clear search">×</button>
|
|
11
|
+
* </div>
|
|
12
|
+
*
|
|
13
|
+
* type="text", not "search": WebKit's own × is unthemed and fires no Enter, which is what made
|
|
14
|
+
* clearing a field leave the old rows on screen until somebody pressed Enter on an empty box.
|
|
15
|
+
* The clear button is hidden while the box is empty. Clearing writes through the native value
|
|
16
|
+
* setter and fires a native `input`, so v-model, a React onChange and Blazor @bind all follow.
|
|
17
|
+
* `term` is trimmed; "" means "show everything again". Exposes window.FreedaySearch.
|
|
18
|
+
*/
|
|
19
|
+
(function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
var DEFAULT_DELAY = 400;
|
|
23
|
+
var nativeValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
24
|
+
|
|
25
|
+
function initSearch(root) {
|
|
26
|
+
if (root.dataset.fdySearchReady === '1') return;
|
|
27
|
+
var input = root.querySelector('input');
|
|
28
|
+
if (!input) return;
|
|
29
|
+
root.dataset.fdySearchReady = '1';
|
|
30
|
+
var clearBtn = root.querySelector('[data-fdy-search-clear]');
|
|
31
|
+
var delayAttr = parseInt(root.getAttribute('data-fdy-search-delay'), 10);
|
|
32
|
+
var delay = isNaN(delayAttr) ? DEFAULT_DELAY : delayAttr;
|
|
33
|
+
var timer = null;
|
|
34
|
+
|
|
35
|
+
function cancel() { if (timer !== null) { clearTimeout(timer); timer = null; } }
|
|
36
|
+
function emit() {
|
|
37
|
+
cancel();
|
|
38
|
+
root.dispatchEvent(new CustomEvent('fdy-search', { bubbles: true, detail: { term: input.value.trim() } }));
|
|
39
|
+
}
|
|
40
|
+
function syncClear() { if (clearBtn) clearBtn.hidden = input.value === ''; }
|
|
41
|
+
|
|
42
|
+
input.addEventListener('input', function () {
|
|
43
|
+
syncClear();
|
|
44
|
+
cancel();
|
|
45
|
+
timer = setTimeout(emit, delay);
|
|
46
|
+
});
|
|
47
|
+
input.addEventListener('keydown', function (e) {
|
|
48
|
+
if (e.key !== 'Enter') return;
|
|
49
|
+
e.preventDefault(); // inside a <form>, Enter would submit it as well
|
|
50
|
+
emit();
|
|
51
|
+
});
|
|
52
|
+
if (clearBtn) {
|
|
53
|
+
clearBtn.addEventListener('click', function () {
|
|
54
|
+
nativeValue.call(input, '');
|
|
55
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
56
|
+
emit(); // at once, and it cancels the pause the input event just started
|
|
57
|
+
input.focus();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
syncClear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function initAll(context) {
|
|
64
|
+
var root = context || document;
|
|
65
|
+
/* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
|
|
66
|
+
if (root.matches && root.matches('[data-fdy-search]')) initSearch(root);
|
|
67
|
+
Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-search]'), initSearch);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (document.readyState === 'loading') {
|
|
71
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
72
|
+
} else {
|
|
73
|
+
initAll();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
window.FreedaySearch = { init: initSearch, initAll: initAll };
|
|
77
|
+
})();
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -1151,6 +1151,8 @@ a { color: var(--color-primary); }
|
|
|
1151
1151
|
.fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
|
|
1152
1152
|
.fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
|
|
1153
1153
|
.fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
|
|
1154
|
+
/* Square: a mark that is not a person (a company logo, a card's icon chip, #070). */
|
|
1155
|
+
.fdy-avatar--square{border-radius:var(--radius-md);}
|
|
1154
1156
|
.fdy-avatar-group{display:inline-flex;}
|
|
1155
1157
|
.fdy-avatar-group .fdy-avatar{border:2px solid var(--color-surface);margin-left:calc(var(--space-2)*-1);}
|
|
1156
1158
|
.fdy-avatar-group .fdy-avatar:first-child{margin-left:0;}
|
|
@@ -1487,6 +1489,13 @@ a { color: var(--color-primary); }
|
|
|
1487
1489
|
absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
|
|
1488
1490
|
:where(:is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
|
|
1489
1491
|
.fdy-card__body{padding:var(--space-5);}
|
|
1492
|
+
/* Header band (#070): an identity mark, the title, and the card's way out pinned right. Beside the
|
|
1493
|
+
title the action sits in one place on every card; under the content it moved each time the
|
|
1494
|
+
content grew. Outside __body's padding, so its rule spans the card edge to edge. The mark is an
|
|
1495
|
+
.fdy-avatar (--sm --square --tone-N), so its tints are the ones the contrast gate already covers. */
|
|
1496
|
+
.fdy-card__head{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
1497
|
+
.fdy-card__head .fdy-card__title{flex:1 1 auto;min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
1498
|
+
.fdy-card__action{flex:none;display:inline-flex;align-items:center;gap:var(--space-2);}
|
|
1490
1499
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
1491
1500
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
1492
1501
|
.fdy-card__footer{padding:var(--space-3) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:linear-gradient(180deg,var(--color-surface),var(--color-surface-2));display:flex;gap:var(--space-2);justify-content:flex-end;}
|
|
@@ -1869,6 +1878,42 @@ a { color: var(--color-primary); }
|
|
|
1869
1878
|
.fdy-text-success{color:var(--color-success-strong);}
|
|
1870
1879
|
.fdy-text-warning{color:var(--color-warning-strong);}
|
|
1871
1880
|
.fdy-text-danger{color:var(--color-danger-strong);}
|
|
1881
|
+
/* Document columns (#065): a form or record page with an optional identity rail on the left
|
|
1882
|
+
* (__lead) and a summary rail on the right (__aside). The widths follow which rails are PRESENT,
|
|
1883
|
+
* so the layout is decided once here and not again by every screen:
|
|
1884
|
+
* __main alone one column
|
|
1885
|
+
* __main + __aside 8 / 4 (from md)
|
|
1886
|
+
* __lead + __main 3 / 9 (from md)
|
|
1887
|
+
* all three 3 / 6 / 3 (from lg; between md and lg both rails share the left)
|
|
1888
|
+
* Stacked, the summary rail comes FIRST: last, it sits under every section, and its button with it.
|
|
1889
|
+
* From md each rail is sticky and scrolls on its own, so the total and the status stay answerable
|
|
1890
|
+
* while the main column scrolls with the page, and a rail taller than the screen never hangs its
|
|
1891
|
+
* end off it. Screen only: a printed page is not held to a window. */
|
|
1892
|
+
.fdy-page__columns{--_fdy-rail-top:var(--fdy-page-sticky-top,var(--space-4));display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-6);align-items:start;}
|
|
1893
|
+
/* Under the shell the sticky topbar is in the way; --fdy-page-sticky-top overrides either default. */
|
|
1894
|
+
.fdy-app .fdy-page__columns{--_fdy-rail-top:var(--fdy-page-sticky-top,calc(var(--space-16) + var(--space-4)));}
|
|
1895
|
+
.fdy-page__columns > *{min-width:0;}
|
|
1896
|
+
@media (max-width:959.98px){.fdy-page__aside{order:-1;}}
|
|
1897
|
+
@media (min-width:960px){
|
|
1898
|
+
.fdy-page__columns:has(> .fdy-page__aside){grid-template-columns:minmax(0,2fr) minmax(0,1fr);}
|
|
1899
|
+
.fdy-page__columns:has(> .fdy-page__lead){grid-template-columns:minmax(0,1fr) minmax(0,3fr);}
|
|
1900
|
+
/* Both rails, two columns: they stack on the left while __main spans both rows on the right. */
|
|
1901
|
+
.fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:span 2;}
|
|
1902
|
+
}
|
|
1903
|
+
@media (min-width:1280px){
|
|
1904
|
+
.fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside){grid-template-columns:minmax(0,1fr) minmax(0,2fr) minmax(0,1fr);}
|
|
1905
|
+
.fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:auto;}
|
|
1906
|
+
}
|
|
1907
|
+
/* The padding, taken back by the margin, is room for a focus ring at a card's edge: a scrolling box
|
|
1908
|
+
clips whatever draws outside it. */
|
|
1909
|
+
@media screen and (min-width:960px){
|
|
1910
|
+
.fdy-page__lead,.fdy-page__aside{position:sticky;top:var(--_fdy-rail-top);max-height:calc(100dvh - var(--_fdy-rail-top) - var(--space-4));overflow-y:auto;overscroll-behavior:contain;padding:var(--space-1);margin:calc(var(--space-1) * -1);}
|
|
1911
|
+
}
|
|
1912
|
+
/* Two sticky rails in ONE column would slide over each other: between md and lg, with both rails
|
|
1913
|
+
present, they scroll with the page instead. */
|
|
1914
|
+
@media screen and (min-width:960px) and (max-width:1279.98px){
|
|
1915
|
+
.fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > :is(.fdy-page__lead,.fdy-page__aside){position:static;max-height:none;overflow:visible;}
|
|
1916
|
+
}
|
|
1872
1917
|
|
|
1873
1918
|
/* Freeday. Date picker (input-styled trigger + calendar popover).
|
|
1874
1919
|
* Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
|
|
@@ -1964,9 +2009,19 @@ a { color: var(--color-primary); }
|
|
|
1964
2009
|
/* Freeday. Description list (key-value detail view) */
|
|
1965
2010
|
.fdy-dl{display:grid;grid-template-columns:auto 1fr;gap:var(--space-2) var(--space-5);margin:0;}
|
|
1966
2011
|
.fdy-dl dt{color:var(--color-text-muted);font-size:var(--text-sm);}
|
|
1967
|
-
|
|
2012
|
+
/* overflow-wrap: an email, an order code or a 13-digit phone number is one unbreakable token, and
|
|
2013
|
+
without it the value prints over whatever sits beside it (#066). */
|
|
2014
|
+
.fdy-dl dd{margin:0;min-width:0;color:var(--color-text);font-size:var(--text-sm);font-weight:var(--weight-medium);overflow-wrap:anywhere;}
|
|
1968
2015
|
.fdy-dl--rows{grid-template-columns:1fr;gap:var(--space-1);}
|
|
1969
2016
|
.fdy-dl--rows dt{margin-top:var(--space-2);}
|
|
2017
|
+
/* Facts grid (#066): each pair a <div> (allowed inside <dl>), caption above value, as many columns
|
|
2018
|
+
as the list's OWN width holds, never more than three. Sized by the element rather than the
|
|
2019
|
+
window, so a grid in a narrow side rail stays one column with no modifier: a window breakpoint
|
|
2020
|
+
gave a rail two columns of about 110px. min(100%, …) keeps a single column from overflowing a
|
|
2021
|
+
rail narrower than the column floor. */
|
|
2022
|
+
.fdy-dl--grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,max(var(--fdy-dl-col-min,14rem),calc((100% - 2 * var(--space-6)) / 3))),1fr));gap:var(--space-5) var(--space-6);}
|
|
2023
|
+
.fdy-dl--grid > div{min-width:0;}
|
|
2024
|
+
.fdy-dl--grid dd{margin-top:var(--space-1);}
|
|
1970
2025
|
|
|
1971
2026
|
/* Freeday. Divider */
|
|
1972
2027
|
.fdy-divider{border:0;border-top:var(--bw) solid var(--color-border);margin:var(--space-4) 0;}
|
|
@@ -2358,6 +2413,12 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
2358
2413
|
.fdy-switch input::before{content:"";position:absolute;top:2px;left:2px;width:calc(var(--control-switch-h) - 4px);height:calc(var(--control-switch-h) - 4px);border-radius:50%;background:var(--color-switch-thumb);box-shadow:var(--shadow-1);transition:transform var(--dur-base) var(--ease-spring);}
|
|
2359
2414
|
.fdy-switch input:checked{background:var(--color-primary);}
|
|
2360
2415
|
.fdy-switch input:checked::before{transform:translateX(calc(var(--control-switch-w) - var(--control-switch-h)));}
|
|
2416
|
+
/* State hint (#068): one line saying what the CURRENT state means, never "on means X, off means Y",
|
|
2417
|
+
which leaves the reader to work out which half applies. The control's aria-describedby points at
|
|
2418
|
+
the .fdy-help WRAPPING both lines: a hidden child of a referenced node is skipped, so a screen
|
|
2419
|
+
reader hears only the line on screen. Point it at the two children and both are read. */
|
|
2420
|
+
.fdy-field:has(:is(.fdy-switch,.fdy-check) input:checked) [data-fdy-when="off"],
|
|
2421
|
+
.fdy-field:not(:has(:is(.fdy-switch,.fdy-check) input:checked)) [data-fdy-when="on"]{display:none;}
|
|
2361
2422
|
|
|
2362
2423
|
/* Standalone checkbox (e.g. table row/select-all), supports :indeterminate */
|
|
2363
2424
|
.fdy-checkbox{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
@@ -2409,6 +2470,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
2409
2470
|
.fdy-state__text{font-size:var(--text-sm);color:var(--color-text-muted);max-width:24rem;line-height:var(--leading-normal);}
|
|
2410
2471
|
.fdy-state__actions{margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap;justify-content:center;}
|
|
2411
2472
|
.fdy-state--danger .fdy-state__icon{color:var(--color-danger-strong);}
|
|
2473
|
+
/* Compact (#069): the same state inside a small card, one line of icon and text instead of a
|
|
2474
|
+
page-sized block. Danger matters most here: a widget that renders zero for a failed request
|
|
2475
|
+
reads as "nothing to do", the opposite of what happened. */
|
|
2476
|
+
.fdy-state--compact{flex-direction:row;align-items:flex-start;text-align:left;gap:var(--space-2);padding:var(--space-2) 0;}
|
|
2477
|
+
.fdy-state--compact .fdy-state__icon{width:1rem;height:1rem;flex:none;}
|
|
2478
|
+
.fdy-state--compact .fdy-state__text{font-size:var(--text-xs);max-width:none;}
|
|
2479
|
+
.fdy-state--compact .fdy-state__actions{margin:0 0 0 auto;}
|
|
2412
2480
|
|
|
2413
2481
|
/* Freeday. Stepper / Wizard (progress header + optional panels).
|
|
2414
2482
|
* Enhanced by freeday-stepper.js for prev/next navigation. */
|
|
@@ -2465,8 +2533,17 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
2465
2533
|
itself, for the same reason: hover paints the full step, so the two must not be the same colour.
|
|
2466
2534
|
Exposed as a Tier-3 token through the var() FALLBACK, never as a declaration — a custom property
|
|
2467
2535
|
set here would outrank a host's own :root override. */
|
|
2468
|
-
|
|
2469
|
-
|
|
2536
|
+
/* The resting colour is also published as --_fdy-row-rest, so a row that must not answer the
|
|
2537
|
+
pointer can hover back to exactly what it was (see --hover-actionable). */
|
|
2538
|
+
.fdy-table--striped tbody tr:nth-child(even){--_fdy-row-rest:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));background:var(--_fdy-row-rest);}
|
|
2539
|
+
.fdy-table tbody tr:hover{background:var(--_fdy-row-hover,var(--color-surface-2));}
|
|
2540
|
+
/* Opt-in (#064): only a row that DOES something answers the pointer. A tint on a row with no click
|
|
2541
|
+
handler promises a target that is not there: people click the row, nothing happens, and the
|
|
2542
|
+
button they had to aim at was elsewhere in it. Striping, not hover, is what keeps the eye on a
|
|
2543
|
+
line. The inert row's hover resolves to its own resting colour, so a stripe survives it; a frozen
|
|
2544
|
+
cell falls back to its opaque surface, because it scrolls over its neighbours. */
|
|
2545
|
+
.fdy-table--hover-actionable tbody tr:not(.fdy-table__row--activatable){--_fdy-row-hover:var(--_fdy-row-rest,transparent);}
|
|
2546
|
+
.fdy-table--hover-actionable tbody tr:not(.fdy-table__row--activatable) td.fdy-table__freeze{--_fdy-row-hover:var(--_fdy-row-rest,var(--color-surface));}
|
|
2470
2547
|
/* Monospace data, ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
|
|
2471
2548
|
or inline (`<span class="fdy-mono">`), not just numeric columns. */
|
|
2472
2549
|
.fdy-mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
|
|
@@ -2475,8 +2552,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
2475
2552
|
.fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
|
|
2476
2553
|
/* Loading / empty state cell (FdyTable spans it across all columns). */
|
|
2477
2554
|
.fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
|
|
2478
|
-
/* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint
|
|
2479
|
-
|
|
2555
|
+
/* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint comes from
|
|
2556
|
+
the base `.fdy-table tbody tr:hover` rule, which --hover-actionable keeps for these rows only. */
|
|
2480
2557
|
.fdy-table__row--activatable{cursor:pointer;}
|
|
2481
2558
|
.fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
|
|
2482
2559
|
/* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
|
|
@@ -2536,7 +2613,7 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
2536
2613
|
(0,4,3) and beat the hover rule below. At (0,3,3) it ties, and the later rule wins, so a hovered
|
|
2537
2614
|
row still bands unbroken across the freeze line. */
|
|
2538
2615
|
.fdy-table--striped tbody tr:nth-child(even) td.fdy-table__freeze{background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
|
|
2539
|
-
.fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
|
|
2616
|
+
.fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--_fdy-row-hover,var(--color-surface-2));}
|
|
2540
2617
|
/* A frozen header cell is stuck on both axes wherever the head is frozen too, so
|
|
2541
2618
|
it has to outrank both. */
|
|
2542
2619
|
.fdy-table--sticky thead .fdy-table__freeze{z-index:3;}
|