@cahyo-dimas/freeday 3.5.0 → 3.7.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 (59) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/COMPONENTS.md +251 -8
  3. package/README.id.md +19 -6
  4. package/README.md +17 -5
  5. package/USAGE.md +42 -0
  6. package/adapters/blazor/FdyDateRange.razor.cs +10 -0
  7. package/adapters/blazor/FdyTable.razor +6 -2
  8. package/adapters/blazor/FdyTable.razor.cs +21 -0
  9. package/adapters/blazor/TableTypes.cs +4 -0
  10. package/adapters/react/components/FdyDateRange.tsx +8 -2
  11. package/adapters/react/components/FdyTable.tsx +24 -3
  12. package/adapters/react/index.d.ts +1 -1
  13. package/adapters/vue/components/FdyDateRange.vue +11 -5
  14. package/adapters/vue/components/FdyTable.vue +18 -2
  15. package/dist/freeday-confirm.js +101 -0
  16. package/dist/freeday-copy.js +58 -0
  17. package/dist/freeday-datepicker.js +17 -2
  18. package/dist/freeday-form.js +30 -1
  19. package/dist/freeday-search.js +77 -0
  20. package/dist/freeday.bundle.css +544 -122
  21. package/dist/freeday.css +241 -118
  22. package/dist/freeday.d.ts +17 -2
  23. package/dist/freeday.js +286 -3
  24. package/dist/freeday.tokens.css +303 -4
  25. package/docs/agent-onboarding.md +8 -0
  26. package/docs/getting-started.md +1 -1
  27. package/package.json +2 -2
  28. package/src/base.css +1 -1
  29. package/src/components/accordion.css +1 -1
  30. package/src/components/alert.css +5 -5
  31. package/src/components/app-shell.css +2 -2
  32. package/src/components/appbar.css +3 -3
  33. package/src/components/avatar.css +17 -9
  34. package/src/components/badge.css +18 -14
  35. package/src/components/breadcrumb.css +1 -1
  36. package/src/components/button.css +21 -13
  37. package/src/components/card.css +7 -0
  38. package/src/components/carousel.css +3 -3
  39. package/src/components/cascade.css +4 -4
  40. package/src/components/chip.css +33 -15
  41. package/src/components/combo.css +5 -5
  42. package/src/components/composition.css +36 -0
  43. package/src/components/datepicker.css +8 -8
  44. package/src/components/description-list.css +11 -1
  45. package/src/components/file-upload.css +9 -4
  46. package/src/components/input-group.css +2 -2
  47. package/src/components/input.css +2 -2
  48. package/src/components/menu.css +3 -3
  49. package/src/components/pagination.css +1 -1
  50. package/src/components/selection.css +8 -2
  51. package/src/components/skeleton.css +1 -1
  52. package/src/components/states.css +7 -0
  53. package/src/components/stepper.css +1 -1
  54. package/src/components/table.css +25 -11
  55. package/src/components/tabs.css +1 -1
  56. package/src/components/timepicker.css +3 -3
  57. package/src/components/toast.css +1 -1
  58. package/src/components/tree.css +1 -1
  59. package/tokens/tokens.json +57 -4
package/dist/freeday.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Type surface for the root side-effect import `@cahyo-dimas/freeday`, which loads every
2
- // enhancer and registers the `window.Freeday` global. Authored from src/freeday-toast.js
3
- // Keep it in sync with that file. (The per-component window.Freeday<Name> enhancer namespaces are
2
+ // enhancer and registers the `window.Freeday` global. Authored from src/freeday-toast.js and
3
+ // src/freeday-confirm.js. Keep it in sync with those files. (The per-component window.Freeday<Name> enhancer namespaces are
4
4
  // intentionally not typed here; no consumer reconstructs those.)
5
5
 
6
6
  export interface FreedayToastOptions {
@@ -15,11 +15,26 @@ export interface FreedayToastOptions {
15
15
  key?: string;
16
16
  }
17
17
 
18
+ export interface FreedayConfirmOptions {
19
+ title?: string;
20
+ message?: string;
21
+ /** Findings shown as a list under the message. */
22
+ details?: string[];
23
+ /** Accept label. Default 'Confirm'; name the action instead ('Delete order'). */
24
+ acceptText?: string;
25
+ /** Reject label. Default 'Cancel'. */
26
+ rejectText?: string;
27
+ /** The accept button's variant. Default 'primary'. */
28
+ tone?: 'primary' | 'danger';
29
+ }
30
+
18
31
  export interface FreedayGlobal {
19
32
  /** Show a transient toast in a live region (created on first use). Returns the toast element. */
20
33
  toast(opts?: FreedayToastOptions): HTMLElement;
21
34
  /** Dismiss a toast early: pass the element returned by toast(), or a key string. */
22
35
  dismiss(target: HTMLElement | string): void;
36
+ /** Ask a yes/no question in a modal. true on accept; false on reject, Escape or a backdrop click. */
37
+ confirm(opts?: FreedayConfirmOptions): Promise<boolean>;
23
38
  }
24
39
 
25
40
  declare global {
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