@cahyo-dimas/freeday 3.4.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 +127 -0
- package/COMPONENTS.md +278 -12
- package/README.id.md +9 -5
- package/README.md +9 -5
- 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 +98 -8
- package/dist/freeday.css +98 -8
- package/dist/freeday.d.ts +17 -2
- package/dist/freeday.js +286 -3
- package/dist/freeday.media.css +27 -0
- package/docs/agent-onboarding.md +10 -0
- package/docs/getting-started.md +1 -1
- package/package.json +6 -2
- package/src/components/avatar.css +2 -0
- package/src/components/breakpoints.css +7 -1
- 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/filterbar.css +8 -1
- package/src/components/selection.css +6 -0
- package/src/components/states.css +7 -0
- package/src/components/table.css +14 -5
- package/tokens/breakpoints.d.ts +4 -0
- package/tokens/breakpoints.mjs +22 -8
package/dist/freeday.js
CHANGED
|
@@ -1924,6 +1924,167 @@
|
|
|
1924
1924
|
window.FreedayChip = { initGroup: initGroup, initRemove: initRemove, initAll: initAll };
|
|
1925
1925
|
})();
|
|
1926
1926
|
|
|
1927
|
+
/* Freeday, confirm API (optional, zero-dependency). Note #060.
|
|
1928
|
+
* Freeday.confirm({ title, message, details, acceptText, rejectText, tone }) opens a .fdy-modal
|
|
1929
|
+
* and resolves a Promise<boolean>: true on accept; false on reject, Escape or a backdrop click.
|
|
1930
|
+
* details: string[] shown as a list under the message, for a confirm that has to NAME its
|
|
1931
|
+
* findings ("these three fields will be refused") instead of folding them into one
|
|
1932
|
+
* long sentence
|
|
1933
|
+
* tone: 'primary' (default) | 'danger', the accept button's variant
|
|
1934
|
+
*
|
|
1935
|
+
* One dialog, one answer: every way out resolves the same Promise, and a Promise settles once, so
|
|
1936
|
+
* a caller never hears "no" after "yes" (a framework dialog that reports reject and then hide, or
|
|
1937
|
+
* hide after accept, does exactly that). Text is set with textContent, never parsed as HTML.
|
|
1938
|
+
* Focus starts on the reject button: Enter on a dialog that just opened must not delete anything.
|
|
1939
|
+
* The dialog is removed when it closes; the native <dialog> returns focus to where it was.
|
|
1940
|
+
* Buttons carry data-fdy-confirm-accept / data-fdy-confirm-reject, stable hooks for tests, because
|
|
1941
|
+
* an accept labelled "Delete" collides by accessible name with every row's own Delete button.
|
|
1942
|
+
*/
|
|
1943
|
+
(function () {
|
|
1944
|
+
'use strict';
|
|
1945
|
+
|
|
1946
|
+
/* User-facing strings, English by default. Per call, acceptText / rejectText override them; a
|
|
1947
|
+
* label that names the action ("Delete order") is better than either default. */
|
|
1948
|
+
var TEXT = {
|
|
1949
|
+
accept: 'Confirm',
|
|
1950
|
+
reject: 'Cancel'
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
var seq = 0;
|
|
1954
|
+
|
|
1955
|
+
function el(tag, className, text) {
|
|
1956
|
+
var node = document.createElement(tag);
|
|
1957
|
+
if (className) node.className = className;
|
|
1958
|
+
if (text != null) node.textContent = text;
|
|
1959
|
+
return node;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function confirm(opts) {
|
|
1963
|
+
opts = opts || {};
|
|
1964
|
+
return new Promise(function (resolve) {
|
|
1965
|
+
seq += 1;
|
|
1966
|
+
var titleId = 'fdy-confirm-title-' + seq;
|
|
1967
|
+
var dialog = el('dialog', 'fdy-modal fdy-modal--sm');
|
|
1968
|
+
dialog.setAttribute('aria-labelledby', titleId);
|
|
1969
|
+
dialog.setAttribute('data-fdy-confirm', '');
|
|
1970
|
+
|
|
1971
|
+
var header = el('header', 'fdy-modal__header');
|
|
1972
|
+
var title = el('h2', 'fdy-modal__title', opts.title || '');
|
|
1973
|
+
title.id = titleId;
|
|
1974
|
+
header.appendChild(title);
|
|
1975
|
+
|
|
1976
|
+
var body = el('div', 'fdy-modal__body');
|
|
1977
|
+
if (opts.message) {
|
|
1978
|
+
var message = el('p', null, opts.message);
|
|
1979
|
+
message.id = titleId + '-desc';
|
|
1980
|
+
dialog.setAttribute('aria-describedby', message.id);
|
|
1981
|
+
body.appendChild(message);
|
|
1982
|
+
}
|
|
1983
|
+
var details = Array.isArray(opts.details) ? opts.details : [];
|
|
1984
|
+
if (details.length > 0) {
|
|
1985
|
+
var list = el('ul');
|
|
1986
|
+
details.forEach(function (line) { list.appendChild(el('li', null, line)); });
|
|
1987
|
+
body.appendChild(list);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
var footer = el('footer', 'fdy-modal__footer');
|
|
1991
|
+
var reject = el('button', 'fdy-btn fdy-btn--ghost', opts.rejectText || TEXT.reject);
|
|
1992
|
+
reject.type = 'button';
|
|
1993
|
+
reject.setAttribute('data-fdy-confirm-reject', '');
|
|
1994
|
+
var accept = el('button', opts.tone === 'danger' ? 'fdy-btn fdy-btn--danger' : 'fdy-btn', opts.acceptText || TEXT.accept);
|
|
1995
|
+
accept.type = 'button';
|
|
1996
|
+
accept.setAttribute('data-fdy-confirm-accept', '');
|
|
1997
|
+
footer.appendChild(reject);
|
|
1998
|
+
footer.appendChild(accept);
|
|
1999
|
+
|
|
2000
|
+
dialog.appendChild(header);
|
|
2001
|
+
dialog.appendChild(body);
|
|
2002
|
+
dialog.appendChild(footer);
|
|
2003
|
+
|
|
2004
|
+
function settle(answer) {
|
|
2005
|
+
resolve(answer);
|
|
2006
|
+
if (dialog.open) dialog.close();
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
accept.addEventListener('click', function () { settle(true); });
|
|
2010
|
+
reject.addEventListener('click', function () { settle(false); });
|
|
2011
|
+
// Escape fires `cancel` then `close`; a backdrop click lands on the <dialog> itself, since its
|
|
2012
|
+
// children fill the box edge to edge. Both mean "no".
|
|
2013
|
+
dialog.addEventListener('close', function () {
|
|
2014
|
+
settle(false);
|
|
2015
|
+
if (dialog.parentNode) dialog.parentNode.removeChild(dialog);
|
|
2016
|
+
});
|
|
2017
|
+
dialog.addEventListener('click', function (e) { if (e.target === dialog) settle(false); });
|
|
2018
|
+
|
|
2019
|
+
document.body.appendChild(dialog);
|
|
2020
|
+
dialog.showModal();
|
|
2021
|
+
reject.focus();
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
window.Freeday = window.Freeday || {};
|
|
2026
|
+
window.Freeday.confirm = confirm;
|
|
2027
|
+
})();
|
|
2028
|
+
|
|
2029
|
+
/* Freeday, copy-to-clipboard enhancer (optional, zero-dependency). Note #062.
|
|
2030
|
+
* A button carrying data-fdy-copy="<value>" copies that value and says so with a toast. For
|
|
2031
|
+
* reference values a reader takes elsewhere: an external document number, an event ID.
|
|
2032
|
+
*
|
|
2033
|
+
* <dd><span class="fdy-mono">INV-2026-00042</span>
|
|
2034
|
+
* <button type="button" class="fdy-btn fdy-btn--ghost fdy-btn--icon fdy-btn--sm"
|
|
2035
|
+
* data-fdy-copy="INV-2026-00042" aria-label="Copy invoice number">…svg…</button></dd>
|
|
2036
|
+
*
|
|
2037
|
+
* The value is the attribute, not a lookup of some neighbour's text: a framework writes it from
|
|
2038
|
+
* the same variable as the visible text, and a row of a table needs no ids to find its own cell.
|
|
2039
|
+
* One delegated listener on the document, so buttons rendered later need no hydration.
|
|
2040
|
+
*
|
|
2041
|
+
* The Clipboard API exists only in a secure context. Over plain http writeText is missing, and a
|
|
2042
|
+
* copy that silently did nothing is the failure this refuses: the reader is told to copy by hand.
|
|
2043
|
+
* Emits bubbling "fdy-copy" {value, ok} on the button. A click inside a clickable table row still
|
|
2044
|
+
* reaches the row's own handler; stop it there if the row should not open. Exposes window.FreedayCopy.
|
|
2045
|
+
*/
|
|
2046
|
+
(function () {
|
|
2047
|
+
'use strict';
|
|
2048
|
+
|
|
2049
|
+
/* User-facing strings, English by default and overridable per button with data-fdy-text-<key>,
|
|
2050
|
+
* like every other enhancer's. */
|
|
2051
|
+
var TEXT = {
|
|
2052
|
+
copied: 'Copied.',
|
|
2053
|
+
failed: 'Could not copy. Select the text and copy it manually.'
|
|
2054
|
+
};
|
|
2055
|
+
function textOf(el, key) {
|
|
2056
|
+
var custom = el.getAttribute('data-fdy-text-' + key);
|
|
2057
|
+
return custom != null && custom !== '' ? custom : TEXT[key];
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function report(btn, value, ok) {
|
|
2061
|
+
if (window.Freeday && typeof window.Freeday.toast === 'function') {
|
|
2062
|
+
window.Freeday.toast(ok
|
|
2063
|
+
? { variant: 'success', message: textOf(btn, 'copied'), key: 'fdy-copy' }
|
|
2064
|
+
: { variant: 'danger', message: textOf(btn, 'failed'), key: 'fdy-copy', timeout: 0 });
|
|
2065
|
+
}
|
|
2066
|
+
btn.dispatchEvent(new CustomEvent('fdy-copy', { bubbles: true, detail: { value: value, ok: ok } }));
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function copy(btn) {
|
|
2070
|
+
var value = btn.getAttribute('data-fdy-copy') || '';
|
|
2071
|
+
if (value === '') return;
|
|
2072
|
+
var clip = navigator.clipboard;
|
|
2073
|
+
if (!clip || typeof clip.writeText !== 'function') { report(btn, value, false); return; }
|
|
2074
|
+
clip.writeText(value).then(
|
|
2075
|
+
function () { report(btn, value, true); },
|
|
2076
|
+
function () { report(btn, value, false); }
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
document.addEventListener('click', function (e) {
|
|
2081
|
+
var btn = e.target.closest && e.target.closest('[data-fdy-copy]');
|
|
2082
|
+
if (btn && !btn.disabled) copy(btn);
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
window.FreedayCopy = { copy: copy };
|
|
2086
|
+
})();
|
|
2087
|
+
|
|
1927
2088
|
/* Freeday, date picker enhancer (optional, zero-dependency).
|
|
1928
2089
|
* Builds an input-styled trigger + calendar popover from an empty wrapper.
|
|
1929
2090
|
* Locale comes from <html lang> (via Intl), month/weekday/value formatting is not hardcoded.
|
|
@@ -2529,15 +2690,30 @@
|
|
|
2529
2690
|
fromApi.setRange(f, t); // start stays free, picking a later start clears the end below
|
|
2530
2691
|
toApi.setRange(f, t);
|
|
2531
2692
|
}
|
|
2693
|
+
/* An end the new start overtook is cleared, and the clear is ANNOUNCED like a pick: a binding
|
|
2694
|
+
* that only hears picks (Blazor's To) otherwise keeps the end the range no longer has. */
|
|
2695
|
+
function clearEnd(el, api) {
|
|
2696
|
+
api.clear();
|
|
2697
|
+
el.dispatchEvent(new CustomEvent('fdy-datepicker-change', { bubbles: true, detail: { value: '' } }));
|
|
2698
|
+
}
|
|
2699
|
+
/* "Settled" = both ends chosen, or both empty: the only states worth a reload (#063). A pick of
|
|
2700
|
+
* the first end, or a start that overtook the end, is half a range. */
|
|
2701
|
+
function settle() {
|
|
2702
|
+
var f = fromApi.getValue(), t = toApi.getValue();
|
|
2703
|
+
if ((f === '') !== (t === '')) return;
|
|
2704
|
+
root.dispatchEvent(new CustomEvent('fdy-daterange-settled', { bubbles: true, detail: { from: f, to: t } }));
|
|
2705
|
+
}
|
|
2532
2706
|
fromEl.addEventListener('fdy-datepicker-change', function () {
|
|
2533
2707
|
var f = fromApi.getValue(), t = toApi.getValue();
|
|
2534
|
-
if (f && t && t < f) toApi
|
|
2708
|
+
if (f && t && t < f) clearEnd(toEl, toApi);
|
|
2535
2709
|
sync();
|
|
2710
|
+
settle();
|
|
2536
2711
|
});
|
|
2537
2712
|
toEl.addEventListener('fdy-datepicker-change', function () {
|
|
2538
2713
|
var f = fromApi.getValue(), t = toApi.getValue();
|
|
2539
|
-
if (f && t && t < f) fromApi
|
|
2714
|
+
if (f && t && t < f) clearEnd(fromEl, fromApi);
|
|
2540
2715
|
sync();
|
|
2716
|
+
settle();
|
|
2541
2717
|
});
|
|
2542
2718
|
sync();
|
|
2543
2719
|
}
|
|
@@ -2720,7 +2896,7 @@
|
|
|
2720
2896
|
* data-fdy-msg-min | -max | -step | -mismatch, then generic data-fdy-msg.
|
|
2721
2897
|
* Cross-field match (e.g. confirm password): data-fdy-match="#otherId".
|
|
2722
2898
|
*
|
|
2723
|
-
* Timing: validates on submit (blocks + focuses the first invalid control); after
|
|
2899
|
+
* Timing: validates on submit (blocks, reveals + focuses the first invalid control); after
|
|
2724
2900
|
* the first submit attempt each field re-validates on input/blur so errors clear
|
|
2725
2901
|
* live. Add data-fdy-eager to validate on blur from the start.
|
|
2726
2902
|
*
|
|
@@ -2833,6 +3009,34 @@
|
|
|
2833
3009
|
}
|
|
2834
3010
|
}
|
|
2835
3011
|
|
|
3012
|
+
/* focus() on a control that is not rendered does nothing and reports nothing: the field turns red
|
|
3013
|
+
* where nobody can see it, and the submit reads as a dead button (#059). So whatever hides the
|
|
3014
|
+
* control is undone first, each through that component's own control, so its state agrees with
|
|
3015
|
+
* what is shown:
|
|
3016
|
+
* closed <details> opened (accordion, tree, nav group)
|
|
3017
|
+
* hidden [role=tabpanel] its tab is clicked; freeday-tabs selects on click
|
|
3018
|
+
* hidden .fdy-step-panel its step button is clicked. A step not yet reached has a disabled
|
|
3019
|
+
* button, so this only ever goes BACK and never skips a step guard.
|
|
3020
|
+
* Anything else that hides it belongs to the app, and is left alone. */
|
|
3021
|
+
function reveal(el) {
|
|
3022
|
+
for (var node = el.parentElement; node; node = node.parentElement) {
|
|
3023
|
+
if (node.localName === 'details' && !node.open) {
|
|
3024
|
+
node.open = true;
|
|
3025
|
+
} else if (node.hidden && node.getAttribute('role') === 'tabpanel' && node.id) {
|
|
3026
|
+
var tab = Array.prototype.filter.call(document.querySelectorAll('[role="tab"]'), function (t) {
|
|
3027
|
+
return t.getAttribute('aria-controls') === node.id;
|
|
3028
|
+
})[0];
|
|
3029
|
+
if (tab) tab.click();
|
|
3030
|
+
} else if (node.hidden && node.classList.contains('fdy-step-panel')) {
|
|
3031
|
+
var stepper = node.closest('[data-fdy-stepper]');
|
|
3032
|
+
if (!stepper) continue;
|
|
3033
|
+
var index = Array.prototype.indexOf.call(stepper.querySelectorAll('.fdy-step-panel'), node);
|
|
3034
|
+
var btn = stepper.querySelectorAll('.fdy-step__btn')[index];
|
|
3035
|
+
if (btn && !btn.disabled) btn.click();
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
2836
3040
|
function initForm(form) {
|
|
2837
3041
|
if (form.dataset.fdyFormReady === '1') return;
|
|
2838
3042
|
form.dataset.fdyFormReady = '1';
|
|
@@ -2867,6 +3071,7 @@
|
|
|
2867
3071
|
});
|
|
2868
3072
|
if (invalid.length) {
|
|
2869
3073
|
e.preventDefault();
|
|
3074
|
+
reveal(invalid[0]);
|
|
2870
3075
|
invalid[0].focus();
|
|
2871
3076
|
form.dispatchEvent(new CustomEvent('fdy-form-invalid', { bubbles: true, detail: { invalid: invalid } }));
|
|
2872
3077
|
} else {
|
|
@@ -3378,6 +3583,84 @@
|
|
|
3378
3583
|
window.FreedayRating = { init: initRating, initAll: initAll };
|
|
3379
3584
|
})();
|
|
3380
3585
|
|
|
3586
|
+
/* Freeday, list search enhancer (optional, zero-dependency). Note #061.
|
|
3587
|
+
* Turns an .fdy-input-group into a search box that searches while the reader pauses, at once on
|
|
3588
|
+
* Enter, and has a clear button. All three paths end in ONE bubbling event on the group,
|
|
3589
|
+
* "fdy-search" {term}, so an app listens once instead of wiring keyup, a timer and a click.
|
|
3590
|
+
*
|
|
3591
|
+
* Markup (the group, not a new block; the clear button is yours, so is its aria-label):
|
|
3592
|
+
* <div class="fdy-input-group" data-fdy-search [data-fdy-search-delay="400"]>
|
|
3593
|
+
* <span class="fdy-input-group__addon fdy-input-group__addon--icon">…svg…</span>
|
|
3594
|
+
* <input class="fdy-input" type="text" aria-label="Search orders">
|
|
3595
|
+
* <button type="button" class="fdy-input-group__btn" data-fdy-search-clear aria-label="Clear search">×</button>
|
|
3596
|
+
* </div>
|
|
3597
|
+
*
|
|
3598
|
+
* type="text", not "search": WebKit's own × is unthemed and fires no Enter, which is what made
|
|
3599
|
+
* clearing a field leave the old rows on screen until somebody pressed Enter on an empty box.
|
|
3600
|
+
* The clear button is hidden while the box is empty. Clearing writes through the native value
|
|
3601
|
+
* setter and fires a native `input`, so v-model, a React onChange and Blazor @bind all follow.
|
|
3602
|
+
* `term` is trimmed; "" means "show everything again". Exposes window.FreedaySearch.
|
|
3603
|
+
*/
|
|
3604
|
+
(function () {
|
|
3605
|
+
'use strict';
|
|
3606
|
+
|
|
3607
|
+
var DEFAULT_DELAY = 400;
|
|
3608
|
+
var nativeValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
3609
|
+
|
|
3610
|
+
function initSearch(root) {
|
|
3611
|
+
if (root.dataset.fdySearchReady === '1') return;
|
|
3612
|
+
var input = root.querySelector('input');
|
|
3613
|
+
if (!input) return;
|
|
3614
|
+
root.dataset.fdySearchReady = '1';
|
|
3615
|
+
var clearBtn = root.querySelector('[data-fdy-search-clear]');
|
|
3616
|
+
var delayAttr = parseInt(root.getAttribute('data-fdy-search-delay'), 10);
|
|
3617
|
+
var delay = isNaN(delayAttr) ? DEFAULT_DELAY : delayAttr;
|
|
3618
|
+
var timer = null;
|
|
3619
|
+
|
|
3620
|
+
function cancel() { if (timer !== null) { clearTimeout(timer); timer = null; } }
|
|
3621
|
+
function emit() {
|
|
3622
|
+
cancel();
|
|
3623
|
+
root.dispatchEvent(new CustomEvent('fdy-search', { bubbles: true, detail: { term: input.value.trim() } }));
|
|
3624
|
+
}
|
|
3625
|
+
function syncClear() { if (clearBtn) clearBtn.hidden = input.value === ''; }
|
|
3626
|
+
|
|
3627
|
+
input.addEventListener('input', function () {
|
|
3628
|
+
syncClear();
|
|
3629
|
+
cancel();
|
|
3630
|
+
timer = setTimeout(emit, delay);
|
|
3631
|
+
});
|
|
3632
|
+
input.addEventListener('keydown', function (e) {
|
|
3633
|
+
if (e.key !== 'Enter') return;
|
|
3634
|
+
e.preventDefault(); // inside a <form>, Enter would submit it as well
|
|
3635
|
+
emit();
|
|
3636
|
+
});
|
|
3637
|
+
if (clearBtn) {
|
|
3638
|
+
clearBtn.addEventListener('click', function () {
|
|
3639
|
+
nativeValue.call(input, '');
|
|
3640
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3641
|
+
emit(); // at once, and it cancels the pause the input event just started
|
|
3642
|
+
input.focus();
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
syncClear();
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
function initAll(context) {
|
|
3649
|
+
var root = context || document;
|
|
3650
|
+
/* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
|
|
3651
|
+
if (root.matches && root.matches('[data-fdy-search]')) initSearch(root);
|
|
3652
|
+
Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-search]'), initSearch);
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
if (document.readyState === 'loading') {
|
|
3656
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
3657
|
+
} else {
|
|
3658
|
+
initAll();
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
window.FreedaySearch = { init: initSearch, initAll: initAll };
|
|
3662
|
+
})();
|
|
3663
|
+
|
|
3381
3664
|
/* Freeday, combobox enhancer (optional, zero-dependency).
|
|
3382
3665
|
* Implements the WAI-ARIA APG "select-only combobox" pattern for every
|
|
3383
3666
|
* [data-fdy-combo] on the page. Progressive enhancement: the markup is the
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Freeday breakpoints as @custom-media. GENERATED by tokens/build.mjs. Do not edit by hand.
|
|
2
|
+
*
|
|
3
|
+
* Requires postcss-custom-media (or any PostCSS setup that includes postcss-preset-env); without it
|
|
4
|
+
* a browser ignores the at-rules and every @media (--fdy-*) in your CSS never matches. Import it
|
|
5
|
+
* ONCE, above the rules that use it:
|
|
6
|
+
*
|
|
7
|
+
* @import '@cahyo-dimas/freeday/media';
|
|
8
|
+
* @media (--fdy-below-sm) { .page-header { display: block } }
|
|
9
|
+
*
|
|
10
|
+
* This file carries no rules of its own, so importing it costs nothing at runtime; it is not part
|
|
11
|
+
* of freeday.css or freeday.bundle.css. */
|
|
12
|
+
@custom-media --fdy-below-sm (max-width: 599.98px);
|
|
13
|
+
@custom-media --fdy-from-sm (min-width: 600px);
|
|
14
|
+
@custom-media --fdy-below-md (max-width: 959.98px);
|
|
15
|
+
@custom-media --fdy-from-md (min-width: 960px);
|
|
16
|
+
@custom-media --fdy-below-lg (max-width: 1279.98px);
|
|
17
|
+
@custom-media --fdy-from-lg (min-width: 1280px);
|
|
18
|
+
@custom-media --fdy-below-xl (max-width: 1919.98px);
|
|
19
|
+
@custom-media --fdy-from-xl (min-width: 1920px);
|
|
20
|
+
|
|
21
|
+
/* NOT ramp steps. Each mirrors one hard-wired rule in the kit, and a rule that must fire with it
|
|
22
|
+
* uses THIS condition, never the nearest ramp step: aligning shell-dependent code to --fdy-below-md
|
|
23
|
+
* leaves 721–959px broken, and aligning a filter-bar rule to --fdy-below-sm leaves 600–640px
|
|
24
|
+
* half-stacked. See tokens/breakpoints.mjs for what each one governs. */
|
|
25
|
+
@custom-media --fdy-nav-drawer (max-width: 720px);
|
|
26
|
+
@custom-media --fdy-nav-static (min-width: 721px);
|
|
27
|
+
@custom-media --fdy-filterbar-stacked (max-width: 640px);
|
package/docs/agent-onboarding.md
CHANGED
|
@@ -96,6 +96,7 @@ Blazor). Then verify the agent can actually read those files. An agent that can'
|
|
|
96
96
|
| `docs/agent-onboarding.md` | This file. |
|
|
97
97
|
| `CHANGELOG.md` | **What changed between the version this project had and the one it has now.** Read it after every upgrade; each entry says what broke, what is new, and why. |
|
|
98
98
|
| `dist/` | Built CSS + enhancers. **`freeday.bundle.css` = tokens + components** (what `@cahyo-dimas/freeday/css` resolves to); `freeday.css` is components **only**, `freeday.tokens.css` tokens only, so linking `freeday.css` alone leaves every `var(--…)` unresolved. Plus `freeday-*.js` and the `.d.ts` files. |
|
|
99
|
+
| `dist/freeday.media.css` | The breakpoints as `@custom-media` (`@cahyo-dimas/freeday/media`), so a responsive rule in your CSS states the kit's condition instead of a retyped literal. Needs PostCSS; carries no rules, and is **not** in the bundle. |
|
|
99
100
|
| `src/components/*.css` | The authoritative source for every class, when a doc is ambiguous. |
|
|
100
101
|
| `tokens/tokens.json` | Every token in W3C DTCG format, machine-readable. |
|
|
101
102
|
| `adapters/vue` · `adapters/react` · `adapters/blazor` | Typed wrappers, 11 components each (plus `FdyTableFooter`). |
|
|
@@ -136,8 +137,17 @@ Recent additions most likely to replace something an app hand-rolled (all detail
|
|
|
136
137
|
| `size="cfl"` on `FdyModal` | writing `.fdy-modal--cfl` by hand to reach the 46rem width |
|
|
137
138
|
| `dialogOnly` + `open()` on `FdyCfl` | a hand-rolled picker, because the trigger is a chip or a table cell and not a 22rem field |
|
|
138
139
|
| `.fdy-field--full` | `max-width: none` on the form container, once per form, because the fields are a flex column and not a `.fdy-form-grid` |
|
|
140
|
+
| `.fdy-page__columns` | a hand-rolled 8/4 or 3/6/3 grid per screen, with a summary rail that falls under every section on a phone |
|
|
141
|
+
| `.fdy-dl--grid` | a facts grid whose columns follow the window, so a side rail gets two 110px columns |
|
|
142
|
+
| `.fdy-card__head` | a card title row with its "View all" link at the bottom, moving every time the content grows |
|
|
143
|
+
| `settled` on `FdyDateRange` | reloading a list on each end of a range, the first time with the start alone |
|
|
144
|
+
| `{ filtered, clearFilters }` on `FdyTable`'s empty state | one "No data" sentence for both "nothing matches" and "nothing yet" |
|
|
145
|
+
| `Freeday.confirm()` | a confirm dialog assembled from a modal, two buttons and a flag so it answers only once |
|
|
146
|
+
| `[data-fdy-search]` | a search box that only searches on Enter, so emptying it leaves the old rows on screen |
|
|
147
|
+
| `[data-fdy-copy]` | selecting an ID by hand, or a copy button that silently does nothing over http |
|
|
139
148
|
| `.fdy-nav--flat` | undoing the caret, the pointer cursor and the group divider yourself, on a sidebar whose group labels never collapse |
|
|
140
149
|
| `--fdy-app-sidebar-w` | overriding `width` on `.fdy-app__sidebar`, which is also the rule that animates the collapse |
|
|
150
|
+
| `@media (--fdy-filterbar-stacked)` / `(--fdy-nav-drawer)`, from `@cahyo-dimas/freeday/media` | a literal `640px` or `720px` retyped into your stylesheet, which no longer matches the kit's rule the day the kit retunes it — and which you could only have found by reading `filterbar.css` |
|
|
141
151
|
|
|
142
152
|
## 3. Starting a new screen
|
|
143
153
|
|
package/docs/getting-started.md
CHANGED
|
@@ -191,7 +191,7 @@ live docs also have a copy button per component.
|
|
|
191
191
|
```bash
|
|
192
192
|
npm i @cahyo-dimas/freeday
|
|
193
193
|
```
|
|
194
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.
|
|
194
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.6.0"` (public npm package). `dist/` is
|
|
195
195
|
committed and published → no build step; `npm ci` runs without auth.
|
|
196
196
|
|
|
197
197
|
### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,10 @@
|
|
|
54
54
|
"types": "./dist/asset.d.ts",
|
|
55
55
|
"default": "./dist/freeday.tokens.css"
|
|
56
56
|
},
|
|
57
|
+
"./media": {
|
|
58
|
+
"types": "./dist/asset.d.ts",
|
|
59
|
+
"default": "./dist/freeday.media.css"
|
|
60
|
+
},
|
|
57
61
|
"./breakpoints": {
|
|
58
62
|
"types": "./tokens/breakpoints.d.ts",
|
|
59
63
|
"default": "./tokens/breakpoints.mjs"
|
|
@@ -92,7 +96,7 @@
|
|
|
92
96
|
"scripts": {
|
|
93
97
|
"build": "node tokens/build.mjs",
|
|
94
98
|
"test": "node --test",
|
|
95
|
-
"test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/cfl-narrow.mjs browser/harness-contract.mjs",
|
|
99
|
+
"test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/page-columns.mjs browser/table-hover.mjs browser/harness-contract.mjs",
|
|
96
100
|
"prepack": "node tokens/build.mjs",
|
|
97
101
|
"version": "node tokens/build.mjs && git add dist",
|
|
98
102
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
.fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
|
|
9
9
|
.fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
|
|
10
10
|
.fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
|
|
11
|
+
/* Square: a mark that is not a person (a company logo, a card's icon chip, #070). */
|
|
12
|
+
.fdy-avatar--square{border-radius:var(--radius-md);}
|
|
11
13
|
.fdy-avatar-group{display:inline-flex;}
|
|
12
14
|
.fdy-avatar-group .fdy-avatar{border:2px solid var(--color-surface);margin-left:calc(var(--space-2)*-1);}
|
|
13
15
|
.fdy-avatar-group .fdy-avatar:first-child{margin-left:0;}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
|
|
2
2
|
* Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
|
|
3
|
+
* Two further widths exist and are NOT ramp steps: `nav` (app-shell.css, off-canvas drawer vs
|
|
4
|
+
* static sidebar) and `filterbar` (filterbar.css, the width its fields stack at). Both are named
|
|
5
|
+
* in tokens/breakpoints.mjs; a rule that has to fire with one of them aligns to it, not to the
|
|
6
|
+
* nearest ramp step, which is a band of broken widths in both directions.
|
|
3
7
|
* NOTE: media-query breakpoints must be literal. CSS can't read custom properties
|
|
4
|
-
* inside @media, so these px are structural, not stray magic numbers.
|
|
8
|
+
* inside @media, so these px are structural, not stray magic numbers. An app that runs PostCSS
|
|
9
|
+
* need not retype them: `@cahyo-dimas/freeday/media` declares every width above as a
|
|
10
|
+
* @custom-media, so it can write `@media (--fdy-below-sm)` and never drift (#058 §2). */
|
|
5
11
|
.fdy-hidden{display:none !important;}
|
|
6
12
|
|
|
7
13
|
/* hide-below-X, hidden when the viewport is narrower than X (content shows at X and up) */
|
package/src/components/card.css
CHANGED
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
|
|
33
33
|
: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;}
|
|
34
34
|
.fdy-card__body{padding:var(--space-5);}
|
|
35
|
+
/* Header band (#070): an identity mark, the title, and the card's way out pinned right. Beside the
|
|
36
|
+
title the action sits in one place on every card; under the content it moved each time the
|
|
37
|
+
content grew. Outside __body's padding, so its rule spans the card edge to edge. The mark is an
|
|
38
|
+
.fdy-avatar (--sm --square --tone-N), so its tints are the ones the contrast gate already covers. */
|
|
39
|
+
.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);}
|
|
40
|
+
.fdy-card__head .fdy-card__title{flex:1 1 auto;min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
41
|
+
.fdy-card__action{flex:none;display:inline-flex;align-items:center;gap:var(--space-2);}
|
|
35
42
|
.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);}
|
|
36
43
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
37
44
|
.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;}
|
|
@@ -95,3 +95,39 @@
|
|
|
95
95
|
.fdy-text-success{color:var(--color-success-strong);}
|
|
96
96
|
.fdy-text-warning{color:var(--color-warning-strong);}
|
|
97
97
|
.fdy-text-danger{color:var(--color-danger-strong);}
|
|
98
|
+
/* Document columns (#065): a form or record page with an optional identity rail on the left
|
|
99
|
+
* (__lead) and a summary rail on the right (__aside). The widths follow which rails are PRESENT,
|
|
100
|
+
* so the layout is decided once here and not again by every screen:
|
|
101
|
+
* __main alone one column
|
|
102
|
+
* __main + __aside 8 / 4 (from md)
|
|
103
|
+
* __lead + __main 3 / 9 (from md)
|
|
104
|
+
* all three 3 / 6 / 3 (from lg; between md and lg both rails share the left)
|
|
105
|
+
* Stacked, the summary rail comes FIRST: last, it sits under every section, and its button with it.
|
|
106
|
+
* From md each rail is sticky and scrolls on its own, so the total and the status stay answerable
|
|
107
|
+
* while the main column scrolls with the page, and a rail taller than the screen never hangs its
|
|
108
|
+
* end off it. Screen only: a printed page is not held to a window. */
|
|
109
|
+
.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;}
|
|
110
|
+
/* Under the shell the sticky topbar is in the way; --fdy-page-sticky-top overrides either default. */
|
|
111
|
+
.fdy-app .fdy-page__columns{--_fdy-rail-top:var(--fdy-page-sticky-top,calc(var(--space-16) + var(--space-4)));}
|
|
112
|
+
.fdy-page__columns > *{min-width:0;}
|
|
113
|
+
@media (max-width:959.98px){.fdy-page__aside{order:-1;}}
|
|
114
|
+
@media (min-width:960px){
|
|
115
|
+
.fdy-page__columns:has(> .fdy-page__aside){grid-template-columns:minmax(0,2fr) minmax(0,1fr);}
|
|
116
|
+
.fdy-page__columns:has(> .fdy-page__lead){grid-template-columns:minmax(0,1fr) minmax(0,3fr);}
|
|
117
|
+
/* Both rails, two columns: they stack on the left while __main spans both rows on the right. */
|
|
118
|
+
.fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:span 2;}
|
|
119
|
+
}
|
|
120
|
+
@media (min-width:1280px){
|
|
121
|
+
.fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside){grid-template-columns:minmax(0,1fr) minmax(0,2fr) minmax(0,1fr);}
|
|
122
|
+
.fdy-page__columns:has(> .fdy-page__lead):has(> .fdy-page__aside) > .fdy-page__main{grid-row:auto;}
|
|
123
|
+
}
|
|
124
|
+
/* The padding, taken back by the margin, is room for a focus ring at a card's edge: a scrolling box
|
|
125
|
+
clips whatever draws outside it. */
|
|
126
|
+
@media screen and (min-width:960px){
|
|
127
|
+
.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);}
|
|
128
|
+
}
|
|
129
|
+
/* Two sticky rails in ONE column would slide over each other: between md and lg, with both rails
|
|
130
|
+
present, they scroll with the page instead. */
|
|
131
|
+
@media screen and (min-width:960px) and (max-width:1279.98px){
|
|
132
|
+
.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;}
|
|
133
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/* Freeday. Description list (key-value detail view) */
|
|
2
2
|
.fdy-dl{display:grid;grid-template-columns:auto 1fr;gap:var(--space-2) var(--space-5);margin:0;}
|
|
3
3
|
.fdy-dl dt{color:var(--color-text-muted);font-size:var(--text-sm);}
|
|
4
|
-
|
|
4
|
+
/* overflow-wrap: an email, an order code or a 13-digit phone number is one unbreakable token, and
|
|
5
|
+
without it the value prints over whatever sits beside it (#066). */
|
|
6
|
+
.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;}
|
|
5
7
|
.fdy-dl--rows{grid-template-columns:1fr;gap:var(--space-1);}
|
|
6
8
|
.fdy-dl--rows dt{margin-top:var(--space-2);}
|
|
9
|
+
/* Facts grid (#066): each pair a <div> (allowed inside <dl>), caption above value, as many columns
|
|
10
|
+
as the list's OWN width holds, never more than three. Sized by the element rather than the
|
|
11
|
+
window, so a grid in a narrow side rail stays one column with no modifier: a window breakpoint
|
|
12
|
+
gave a rail two columns of about 110px. min(100%, …) keeps a single column from overflowing a
|
|
13
|
+
rail narrower than the column floor. */
|
|
14
|
+
.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);}
|
|
15
|
+
.fdy-dl--grid > div{min-width:0;}
|
|
16
|
+
.fdy-dl--grid dd{margin-top:var(--space-1);}
|
|
@@ -41,7 +41,14 @@
|
|
|
41
41
|
/* Opt-in: sit the actions inline right after the last field instead of at the far edge, so when the
|
|
42
42
|
* bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
|
|
43
43
|
.fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
|
|
44
|
-
/* Stack on narrow screens so nothing overflows the row.
|
|
44
|
+
/* Stack on narrow screens so nothing overflows the row.
|
|
45
|
+
* 640 is deliberately not a ramp step (sm is 600): it is a real phone-landscape width, and moving
|
|
46
|
+
* it now would reflow every filter bar in every consuming app. So it is NAMED instead — `filterbar`
|
|
47
|
+
* in tokens/breakpoints.mjs, `--fdy-filterbar-stacked` in the media sheet — because an app rule
|
|
48
|
+
* that must fire with the stack (releasing a --w-grow field's max-width, say) has to match this
|
|
49
|
+
* exact condition, and aligning it to sm from the docs alone leaves 600–640px half-stacked: the
|
|
50
|
+
* fields are already full width while the app still caps the growing one. Raised from
|
|
51
|
+
* IDU_AI_DOC_SAPB1_CLIENT and IDU_AI_DOC_SAAS (#058 §1). */
|
|
45
52
|
@media (max-width:640px){
|
|
46
53
|
.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%;}
|
|
47
54
|
.fdy-filterbar__actions{width:100%;margin-left:0;}
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
.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);}
|
|
22
22
|
.fdy-switch input:checked{background:var(--color-primary);}
|
|
23
23
|
.fdy-switch input:checked::before{transform:translateX(calc(var(--control-switch-w) - var(--control-switch-h)));}
|
|
24
|
+
/* State hint (#068): one line saying what the CURRENT state means, never "on means X, off means Y",
|
|
25
|
+
which leaves the reader to work out which half applies. The control's aria-describedby points at
|
|
26
|
+
the .fdy-help WRAPPING both lines: a hidden child of a referenced node is skipped, so a screen
|
|
27
|
+
reader hears only the line on screen. Point it at the two children and both are read. */
|
|
28
|
+
.fdy-field:has(:is(.fdy-switch,.fdy-check) input:checked) [data-fdy-when="off"],
|
|
29
|
+
.fdy-field:not(:has(:is(.fdy-switch,.fdy-check) input:checked)) [data-fdy-when="on"]{display:none;}
|
|
24
30
|
|
|
25
31
|
/* Standalone checkbox (e.g. table row/select-all), supports :indeterminate */
|
|
26
32
|
.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);}
|
|
@@ -6,3 +6,10 @@
|
|
|
6
6
|
.fdy-state__text{font-size:var(--text-sm);color:var(--color-text-muted);max-width:24rem;line-height:var(--leading-normal);}
|
|
7
7
|
.fdy-state__actions{margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap;justify-content:center;}
|
|
8
8
|
.fdy-state--danger .fdy-state__icon{color:var(--color-danger-strong);}
|
|
9
|
+
/* Compact (#069): the same state inside a small card, one line of icon and text instead of a
|
|
10
|
+
page-sized block. Danger matters most here: a widget that renders zero for a failed request
|
|
11
|
+
reads as "nothing to do", the opposite of what happened. */
|
|
12
|
+
.fdy-state--compact{flex-direction:row;align-items:flex-start;text-align:left;gap:var(--space-2);padding:var(--space-2) 0;}
|
|
13
|
+
.fdy-state--compact .fdy-state__icon{width:1rem;height:1rem;flex:none;}
|
|
14
|
+
.fdy-state--compact .fdy-state__text{font-size:var(--text-xs);max-width:none;}
|
|
15
|
+
.fdy-state--compact .fdy-state__actions{margin:0 0 0 auto;}
|
package/src/components/table.css
CHANGED
|
@@ -15,8 +15,17 @@
|
|
|
15
15
|
itself, for the same reason: hover paints the full step, so the two must not be the same colour.
|
|
16
16
|
Exposed as a Tier-3 token through the var() FALLBACK, never as a declaration — a custom property
|
|
17
17
|
set here would outrank a host's own :root override. */
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/* The resting colour is also published as --_fdy-row-rest, so a row that must not answer the
|
|
19
|
+
pointer can hover back to exactly what it was (see --hover-actionable). */
|
|
20
|
+
.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);}
|
|
21
|
+
.fdy-table tbody tr:hover{background:var(--_fdy-row-hover,var(--color-surface-2));}
|
|
22
|
+
/* Opt-in (#064): only a row that DOES something answers the pointer. A tint on a row with no click
|
|
23
|
+
handler promises a target that is not there: people click the row, nothing happens, and the
|
|
24
|
+
button they had to aim at was elsewhere in it. Striping, not hover, is what keeps the eye on a
|
|
25
|
+
line. The inert row's hover resolves to its own resting colour, so a stripe survives it; a frozen
|
|
26
|
+
cell falls back to its opaque surface, because it scrolls over its neighbours. */
|
|
27
|
+
.fdy-table--hover-actionable tbody tr:not(.fdy-table__row--activatable){--_fdy-row-hover:var(--_fdy-row-rest,transparent);}
|
|
28
|
+
.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));}
|
|
20
29
|
/* Monospace data, ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
|
|
21
30
|
or inline (`<span class="fdy-mono">`), not just numeric columns. */
|
|
22
31
|
.fdy-mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
|
|
@@ -25,8 +34,8 @@
|
|
|
25
34
|
.fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
|
|
26
35
|
/* Loading / empty state cell (FdyTable spans it across all columns). */
|
|
27
36
|
.fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
|
|
28
|
-
/* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint
|
|
29
|
-
|
|
37
|
+
/* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint comes from
|
|
38
|
+
the base `.fdy-table tbody tr:hover` rule, which --hover-actionable keeps for these rows only. */
|
|
30
39
|
.fdy-table__row--activatable{cursor:pointer;}
|
|
31
40
|
.fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
|
|
32
41
|
/* Expandable detail row (FdyTable row-detail slot): a muted panel under its row, no hover tint. */
|
|
@@ -86,7 +95,7 @@
|
|
|
86
95
|
(0,4,3) and beat the hover rule below. At (0,3,3) it ties, and the later rule wins, so a hovered
|
|
87
96
|
row still bands unbroken across the freeze line. */
|
|
88
97
|
.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)));}
|
|
89
|
-
.fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
|
|
98
|
+
.fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--_fdy-row-hover,var(--color-surface-2));}
|
|
90
99
|
/* A frozen header cell is stuck on both axes wherever the head is frozen too, so
|
|
91
100
|
it has to outrank both. */
|
|
92
101
|
.fdy-table--sticky thead .fdy-table__freeze{z-index:3;}
|
package/tokens/breakpoints.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export declare const breakpoints: {
|
|
|
4
4
|
/** Width at which .fdy-app's sidebar turns from off-canvas drawer into a static column.
|
|
5
5
|
* Not part of the sm/md/lg/xl ramp; it mirrors app-shell.css and nothing else. */
|
|
6
6
|
nav: number;
|
|
7
|
+
/** Width at or below which .fdy-filterbar stacks every field full-width.
|
|
8
|
+
* Not part of the sm/md/lg/xl ramp; it mirrors filterbar.css and nothing else. A rule that
|
|
9
|
+
* must fire with the stack uses this, not `sm` — `sm` leaves 600–640px half-stacked. */
|
|
10
|
+
filterbar: number;
|
|
7
11
|
sm: number;
|
|
8
12
|
md: number;
|
|
9
13
|
lg: number;
|