@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/COMPONENTS.md +278 -12
  3. package/README.id.md +9 -5
  4. package/README.md +9 -5
  5. package/adapters/blazor/FdyDateRange.razor.cs +10 -0
  6. package/adapters/blazor/FdyTable.razor +6 -2
  7. package/adapters/blazor/FdyTable.razor.cs +21 -0
  8. package/adapters/blazor/TableTypes.cs +4 -0
  9. package/adapters/react/components/FdyDateRange.tsx +8 -2
  10. package/adapters/react/components/FdyTable.tsx +24 -3
  11. package/adapters/react/index.d.ts +1 -1
  12. package/adapters/vue/components/FdyDateRange.vue +11 -5
  13. package/adapters/vue/components/FdyTable.vue +18 -2
  14. package/dist/freeday-confirm.js +101 -0
  15. package/dist/freeday-copy.js +58 -0
  16. package/dist/freeday-datepicker.js +17 -2
  17. package/dist/freeday-form.js +30 -1
  18. package/dist/freeday-search.js +77 -0
  19. package/dist/freeday.bundle.css +98 -8
  20. package/dist/freeday.css +98 -8
  21. package/dist/freeday.d.ts +17 -2
  22. package/dist/freeday.js +286 -3
  23. package/dist/freeday.media.css +27 -0
  24. package/docs/agent-onboarding.md +10 -0
  25. package/docs/getting-started.md +1 -1
  26. package/package.json +6 -2
  27. package/src/components/avatar.css +2 -0
  28. package/src/components/breakpoints.css +7 -1
  29. package/src/components/card.css +7 -0
  30. package/src/components/composition.css +36 -0
  31. package/src/components/description-list.css +11 -1
  32. package/src/components/filterbar.css +8 -1
  33. package/src/components/selection.css +6 -0
  34. package/src/components/states.css +7 -0
  35. package/src/components/table.css +14 -5
  36. package/tokens/breakpoints.d.ts +4 -0
  37. 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.clear();
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.clear();
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);
@@ -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
 
@@ -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.4.0"` (public npm package). `dist/` is
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.4.0",
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) */
@@ -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
- .fdy-dl dd{margin:0;color:var(--color-text);font-size:var(--text-sm);font-weight:var(--weight-medium);}
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;}
@@ -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
- .fdy-table--striped tbody tr:nth-child(even){background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
19
- .fdy-table tbody tr:hover{background:var(--color-surface-2);}
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 already
29
- comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
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;}
@@ -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;