@cahyo-dimas/freeday 1.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 (130) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/adapters/blazor/freeday-blazor.js +79 -0
  5. package/adapters/react/components/FdyAutocomplete.tsx +159 -0
  6. package/adapters/react/components/FdyCascade.tsx +230 -0
  7. package/adapters/react/components/FdyCfl.tsx +362 -0
  8. package/adapters/react/components/FdyChart.tsx +69 -0
  9. package/adapters/react/components/FdyCombo.tsx +166 -0
  10. package/adapters/react/components/FdyDateRange.tsx +66 -0
  11. package/adapters/react/components/FdyDatepicker.tsx +354 -0
  12. package/adapters/react/index.d.ts +37 -0
  13. package/adapters/react/index.js +9 -0
  14. package/adapters/react/tsconfig.json +14 -0
  15. package/adapters/react/useFreeday.js +44 -0
  16. package/adapters/react/usePopover.ts +64 -0
  17. package/adapters/vue/components/FdyAutocomplete.vue +174 -0
  18. package/adapters/vue/components/FdyCascade.vue +241 -0
  19. package/adapters/vue/components/FdyCfl.vue +354 -0
  20. package/adapters/vue/components/FdyChart.vue +89 -0
  21. package/adapters/vue/components/FdyCombo.vue +232 -0
  22. package/adapters/vue/components/FdyDateRange.vue +78 -0
  23. package/adapters/vue/components/FdyDatepicker.vue +333 -0
  24. package/adapters/vue/index.d.ts +44 -0
  25. package/adapters/vue/index.js +8 -0
  26. package/adapters/vue/useFreeday.js +44 -0
  27. package/adapters/vue/usePopover.ts +78 -0
  28. package/dist/freeday-autocomplete.js +135 -0
  29. package/dist/freeday-breakpoint.js +51 -0
  30. package/dist/freeday-carousel.js +111 -0
  31. package/dist/freeday-cascade.js +256 -0
  32. package/dist/freeday-cfl.js +213 -0
  33. package/dist/freeday-chart.js +403 -0
  34. package/dist/freeday-chip.js +83 -0
  35. package/dist/freeday-datepicker.js +321 -0
  36. package/dist/freeday-datetime.js +59 -0
  37. package/dist/freeday-drawer.js +43 -0
  38. package/dist/freeday-form.js +181 -0
  39. package/dist/freeday-mask.js +114 -0
  40. package/dist/freeday-menu.js +93 -0
  41. package/dist/freeday-popover.js +69 -0
  42. package/dist/freeday-rating.js +50 -0
  43. package/dist/freeday-select.js +194 -0
  44. package/dist/freeday-slider.js +34 -0
  45. package/dist/freeday-stepper.js +90 -0
  46. package/dist/freeday-table.js +475 -0
  47. package/dist/freeday-tabs.js +68 -0
  48. package/dist/freeday-timepicker.js +180 -0
  49. package/dist/freeday-toast.js +84 -0
  50. package/dist/freeday-tree.js +94 -0
  51. package/dist/freeday-upload.js +206 -0
  52. package/dist/freeday.bundle.css +1352 -0
  53. package/dist/freeday.css +1007 -0
  54. package/dist/freeday.js +3632 -0
  55. package/dist/freeday.tokens.css +343 -0
  56. package/package.json +72 -0
  57. package/src/base.css +20 -0
  58. package/src/components/.gitkeep +0 -0
  59. package/src/components/accordion.css +13 -0
  60. package/src/components/alert.css +18 -0
  61. package/src/components/app-shell.css +88 -0
  62. package/src/components/appbar.css +15 -0
  63. package/src/components/autocomplete.css +12 -0
  64. package/src/components/avatar.css +8 -0
  65. package/src/components/badge.css +27 -0
  66. package/src/components/breadcrumb.css +9 -0
  67. package/src/components/breakpoints.css +15 -0
  68. package/src/components/button.css +58 -0
  69. package/src/components/card.css +9 -0
  70. package/src/components/carousel.css +17 -0
  71. package/src/components/cascade.css +36 -0
  72. package/src/components/cfl.css +28 -0
  73. package/src/components/chart.css +55 -0
  74. package/src/components/chip.css +18 -0
  75. package/src/components/combo.css +27 -0
  76. package/src/components/datepicker.css +44 -0
  77. package/src/components/datetimepicker.css +8 -0
  78. package/src/components/description-list.css +6 -0
  79. package/src/components/divider.css +3 -0
  80. package/src/components/drawer.css +25 -0
  81. package/src/components/file-upload.css +34 -0
  82. package/src/components/filterbar.css +25 -0
  83. package/src/components/form-grid.css +8 -0
  84. package/src/components/input-group.css +22 -0
  85. package/src/components/input.css +13 -0
  86. package/src/components/kbd.css +2 -0
  87. package/src/components/menu.css +28 -0
  88. package/src/components/modal.css +22 -0
  89. package/src/components/pagination.css +8 -0
  90. package/src/components/progress.css +7 -0
  91. package/src/components/rating.css +10 -0
  92. package/src/components/selection.css +25 -0
  93. package/src/components/skeleton.css +7 -0
  94. package/src/components/slider.css +12 -0
  95. package/src/components/spinner.css +6 -0
  96. package/src/components/states.css +8 -0
  97. package/src/components/stepper.css +21 -0
  98. package/src/components/table.css +55 -0
  99. package/src/components/tabs.css +10 -0
  100. package/src/components/timeline.css +12 -0
  101. package/src/components/timepicker.css +22 -0
  102. package/src/components/toast.css +17 -0
  103. package/src/components/tooltip.css +6 -0
  104. package/src/components/tree.css +21 -0
  105. package/src/freeday-autocomplete.js +135 -0
  106. package/src/freeday-breakpoint.js +51 -0
  107. package/src/freeday-carousel.js +111 -0
  108. package/src/freeday-cascade.js +256 -0
  109. package/src/freeday-cfl.js +213 -0
  110. package/src/freeday-chart.js +403 -0
  111. package/src/freeday-chip.js +83 -0
  112. package/src/freeday-datepicker.js +321 -0
  113. package/src/freeday-datetime.js +59 -0
  114. package/src/freeday-drawer.js +43 -0
  115. package/src/freeday-form.js +181 -0
  116. package/src/freeday-mask.js +114 -0
  117. package/src/freeday-menu.js +93 -0
  118. package/src/freeday-popover.js +69 -0
  119. package/src/freeday-rating.js +50 -0
  120. package/src/freeday-select.js +194 -0
  121. package/src/freeday-slider.js +34 -0
  122. package/src/freeday-stepper.js +90 -0
  123. package/src/freeday-table.js +475 -0
  124. package/src/freeday-tabs.js +68 -0
  125. package/src/freeday-timepicker.js +180 -0
  126. package/src/freeday-toast.js +84 -0
  127. package/src/freeday-tree.js +94 -0
  128. package/src/freeday-upload.js +206 -0
  129. package/tokens/breakpoints.mjs +4 -0
  130. package/tokens/tokens.json +123 -0
@@ -0,0 +1,114 @@
1
+ /* Freeday — input mask + password reveal enhancer (optional, zero-dependency).
2
+ *
3
+ * [data-fdy-mask]="pattern" formats a text input as you type. Placeholders:
4
+ * # a digit (0-9) A a letter (a-z/A-Z) * a letter or digit
5
+ * Any other pattern char is a literal, inserted automatically
6
+ * (e.g. "####-####-####", "(###) ###-####", "##/##/####").
7
+ * The raw (unmasked) value is mirrored to el.dataset.fdyRaw and the bubbling
8
+ * "fdy-mask" CustomEvent (detail {value, raw}). A native input event is
9
+ * re-dispatched so validation / framework bindings observe the change.
10
+ *
11
+ * [data-fdy-password] on a password <input class="fdy-input"> wraps it in the
12
+ * input-group chrome and adds a show/hide toggle (type swap + aria-pressed).
13
+ */
14
+ (function () {
15
+ 'use strict';
16
+
17
+ var EYE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
18
+ var EYE_OFF = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.9 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17 17 0 0 1-3.2 4M6.1 6.1A17 17 0 0 0 2 12s3.5 7 10 7a9.7 9.7 0 0 0 4-.85"></path><path d="m3 3 18 18"></path></svg>';
19
+
20
+ function matches(placeholder, ch) {
21
+ if (placeholder === '#') return /\d/.test(ch);
22
+ if (placeholder === 'A') return /[a-zA-Z]/.test(ch);
23
+ if (placeholder === '*') return /[a-zA-Z0-9]/.test(ch);
24
+ return false;
25
+ }
26
+ function isPlaceholder(ch) { return ch === '#' || ch === 'A' || ch === '*'; }
27
+
28
+ // Keep only characters the pattern's placeholders could accept.
29
+ function rawOf(pattern, value) {
30
+ var allowDigit = pattern.indexOf('#') !== -1 || pattern.indexOf('*') !== -1;
31
+ var allowAlpha = pattern.indexOf('A') !== -1 || pattern.indexOf('*') !== -1;
32
+ var out = '';
33
+ for (var i = 0; i < value.length; i++) {
34
+ var c = value[i];
35
+ if ((allowDigit && /\d/.test(c)) || (allowAlpha && /[a-zA-Z]/.test(c))) out += c;
36
+ }
37
+ return out;
38
+ }
39
+ function format(pattern, raw) {
40
+ var out = '', ri = 0;
41
+ for (var pi = 0; pi < pattern.length && ri < raw.length; pi++) {
42
+ var p = pattern[pi];
43
+ if (isPlaceholder(p)) {
44
+ while (ri < raw.length && !matches(p, raw[ri])) ri++;
45
+ if (ri < raw.length) { out += raw[ri]; ri++; }
46
+ } else {
47
+ out += p;
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function initMask(el) {
54
+ if (el.dataset.fdyMaskReady === '1') return;
55
+ el.dataset.fdyMaskReady = '1';
56
+ var pattern = el.getAttribute('data-fdy-mask') || '';
57
+ if (!pattern) return;
58
+ el.setAttribute('inputmode', pattern.indexOf('A') === -1 ? 'numeric' : 'text');
59
+
60
+ function apply() {
61
+ var raw = rawOf(pattern, el.value);
62
+ var formatted = format(pattern, raw);
63
+ if (formatted !== el.value) {
64
+ el.value = formatted;
65
+ // Masked entry is left-to-right; park the caret at the end.
66
+ try { el.setSelectionRange(formatted.length, formatted.length); } catch (err) { /* number/date inputs */ }
67
+ }
68
+ el.dataset.fdyRaw = raw;
69
+ el.dispatchEvent(new CustomEvent('fdy-mask', { bubbles: true, detail: { value: formatted, raw: raw } }));
70
+ }
71
+ el.addEventListener('input', apply);
72
+ if (el.value) apply();
73
+ }
74
+
75
+ function initReveal(input) {
76
+ if (input.dataset.fdyPwReady === '1') return;
77
+ input.dataset.fdyPwReady = '1';
78
+
79
+ var group = document.createElement('span');
80
+ group.className = 'fdy-input-group';
81
+ input.parentNode.insertBefore(group, input);
82
+ group.appendChild(input);
83
+
84
+ var btn = document.createElement('button');
85
+ btn.type = 'button';
86
+ btn.className = 'fdy-input-group__btn';
87
+ btn.setAttribute('aria-pressed', 'false');
88
+ btn.setAttribute('aria-label', 'Tampilkan kata sandi');
89
+ btn.innerHTML = EYE;
90
+ btn.addEventListener('click', function () {
91
+ var reveal = input.type === 'password';
92
+ input.type = reveal ? 'text' : 'password';
93
+ btn.setAttribute('aria-pressed', reveal ? 'true' : 'false');
94
+ btn.setAttribute('aria-label', reveal ? 'Sembunyikan kata sandi' : 'Tampilkan kata sandi');
95
+ btn.innerHTML = reveal ? EYE_OFF : EYE;
96
+ input.focus();
97
+ });
98
+ group.appendChild(btn);
99
+ }
100
+
101
+ function initAll(context) {
102
+ var root = context || document;
103
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-mask]'), initMask);
104
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-password]'), initReveal);
105
+ }
106
+
107
+ if (document.readyState === 'loading') {
108
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
109
+ } else {
110
+ initAll();
111
+ }
112
+
113
+ window.FreedayMask = { initMask: initMask, initReveal: initReveal, initAll: initAll };
114
+ })();
@@ -0,0 +1,93 @@
1
+ /* Freeday — menu enhancer (optional, zero-dependency).
2
+ * Menu-button pattern (WAI-ARIA APG): a trigger with aria-haspopup="menu" toggles a
3
+ * role="menu" popup of role="menuitem" buttons. Also powers the split button
4
+ * (main action + attached caret toggle). Auto-inits [data-fdy-menu].
5
+ *
6
+ * Markup: <div data-fdy-menu><button aria-haspopup="menu" aria-expanded="false">…</button>
7
+ * <ul class="fdy-menu" role="menu" hidden><li role="none">
8
+ * <button class="fdy-menu__item" role="menuitem">…</button></li>…</ul></div>
9
+ * Keyboard: trigger ↓/Enter/Space opens to first item, ↑ opens to last; in the menu
10
+ * ↑/↓/Home/End move, Esc closes (focus → trigger), Tab closes; activating an item closes.
11
+ */
12
+ (function () {
13
+ 'use strict';
14
+
15
+ function menuItems(menu) {
16
+ return Array.prototype.slice.call(menu.querySelectorAll('[role="menuitem"]'))
17
+ .filter(function (i) { return !i.disabled && !i.hidden; });
18
+ }
19
+
20
+ function initMenu(wrap) {
21
+ if (wrap.dataset.fdyMenuReady === '1') return;
22
+ wrap.dataset.fdyMenuReady = '1';
23
+ var trigger = wrap.querySelector('[aria-haspopup="menu"]');
24
+ var menu = wrap.querySelector('.fdy-menu, [role="menu"]');
25
+ if (!trigger || !menu) return;
26
+
27
+ Array.prototype.forEach.call(menu.querySelectorAll('[role="menuitem"]'), function (i) {
28
+ i.setAttribute('tabindex', '-1');
29
+ });
30
+ menu.hidden = true;
31
+ trigger.setAttribute('aria-expanded', 'false');
32
+
33
+ function focusAt(index) {
34
+ var its = menuItems(menu);
35
+ if (!its.length) return;
36
+ its[(index + its.length) % its.length].focus();
37
+ }
38
+ var _pop = null;
39
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(menu, trigger, { matchWidth: false }); return _pop; }
40
+ function open(index) {
41
+ var p = popCtl(); if (p) p.show(); else menu.hidden = false;
42
+ trigger.setAttribute('aria-expanded', 'true');
43
+ document.addEventListener('click', onDocClick, true);
44
+ focusAt(index);
45
+ }
46
+ function close(returnFocus) {
47
+ if (menu.hidden) return;
48
+ var p = popCtl(); if (p) p.hide(); else menu.hidden = true;
49
+ trigger.setAttribute('aria-expanded', 'false');
50
+ document.removeEventListener('click', onDocClick, true);
51
+ if (returnFocus === true) trigger.focus();
52
+ }
53
+ function onDocClick(e) { if (!wrap.contains(e.target)) close(false); }
54
+
55
+ trigger.addEventListener('click', function (e) {
56
+ e.stopPropagation();
57
+ if (menu.hidden) open(0); else close(false);
58
+ });
59
+ trigger.addEventListener('keydown', function (e) {
60
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(0); }
61
+ else if (e.key === 'ArrowUp') { e.preventDefault(); open(-1); }
62
+ });
63
+ menu.addEventListener('keydown', function (e) {
64
+ var its = menuItems(menu);
65
+ var cur = its.indexOf(document.activeElement);
66
+ switch (e.key) {
67
+ case 'ArrowDown': e.preventDefault(); focusAt(cur + 1); break;
68
+ case 'ArrowUp': e.preventDefault(); focusAt(cur - 1); break;
69
+ case 'Home': e.preventDefault(); focusAt(0); break;
70
+ case 'End': e.preventDefault(); focusAt(its.length - 1); break;
71
+ case 'Escape': e.preventDefault(); close(true); break;
72
+ case 'Tab': close(false); break;
73
+ default: break;
74
+ }
75
+ });
76
+ menu.addEventListener('click', function (e) {
77
+ var item = e.target.closest('[role="menuitem"]');
78
+ if (item && !item.disabled) close(true);
79
+ });
80
+ }
81
+
82
+ function initAll(ctx) {
83
+ Array.prototype.forEach.call((ctx || document).querySelectorAll('[data-fdy-menu]'), initMenu);
84
+ }
85
+
86
+ if (document.readyState === 'loading') {
87
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
88
+ } else {
89
+ initAll();
90
+ }
91
+
92
+ window.FreedayMenu = { init: initMenu, initAll: initAll };
93
+ })();
@@ -0,0 +1,69 @@
1
+ /* Freeday — popover positioning helper (optional, zero-dependency).
2
+ * Lifts a dropdown panel into the top layer via the native Popover API so it escapes ANY
3
+ * ancestor overflow clip (a .fdy-card with overflow:hidden, an app-shell main with
4
+ * overflow:auto, …), then positions it `fixed` against its trigger — flipping above when
5
+ * there is no room below, and matching the trigger width. The panel stays a DOM child of its
6
+ * component, so focus, outside-click (.closest), and ARIA relationships keep working unchanged.
7
+ *
8
+ * Used by the select/combo, datepicker, timepicker, cascade, autocomplete and menu enhancers;
9
+ * the freeday/vue adapter ships an equivalent `usePopover` composable.
10
+ *
11
+ * attach(panel, trigger) -> { show(), hide() }. show()/hide() also toggle the panel's [hidden]
12
+ * attribute so callers keep using `!panel.hidden` for open-state and the `:not([hidden])`
13
+ * entrance animation still fires. On browsers without the Popover API it degrades to that
14
+ * [hidden] toggle over the panel's existing absolute-positioned CSS (clipped, but functional).
15
+ */
16
+ (function () {
17
+ 'use strict';
18
+
19
+ var GAP = 4; // ~ --space-1, breathing room between trigger and panel
20
+ var supported = typeof HTMLElement !== 'undefined'
21
+ && typeof HTMLElement.prototype.showPopover === 'function';
22
+
23
+ // Position `panel` (already shown, so it has a measurable box) against `trigger`'s rect.
24
+ // matchWidth (default true) floors the panel to the trigger width — right for input-shaped
25
+ // dropdowns (combo/cascade/date/time), skipped for menus that keep their own min-width.
26
+ function place(panel, trigger, matchWidth) {
27
+ var r = trigger.getBoundingClientRect();
28
+ panel.style.position = 'fixed';
29
+ panel.style.margin = '0';
30
+ panel.style.inset = 'auto'; // clear the UA popover centering
31
+ if (matchWidth !== false) panel.style.minWidth = r.width + 'px'; // at least as wide as the trigger
32
+ var ph = panel.offsetHeight, pw = panel.offsetWidth;
33
+ var vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
34
+ var below = vh - r.bottom - GAP, above = r.top - GAP;
35
+ // Prefer below; flip above only when it doesn't fit below and above has more room.
36
+ var top = (ph <= below || below >= above) ? (r.bottom + GAP) : Math.max(GAP, r.top - GAP - ph);
37
+ var left = r.left;
38
+ if (left + pw > vw - GAP) left = Math.max(GAP, vw - GAP - pw); // keep within the viewport
39
+ panel.style.top = Math.round(top) + 'px';
40
+ panel.style.left = Math.round(left) + 'px';
41
+ }
42
+
43
+ function attach(panel, trigger, opts) {
44
+ var matchWidth = !(opts && opts.matchWidth === false);
45
+ if (supported) panel.setAttribute('popover', 'manual');
46
+ function reposition() { if (supported && panel.matches(':popover-open')) place(panel, trigger, matchWidth); }
47
+ return {
48
+ show: function () {
49
+ panel.hidden = false;
50
+ if (supported) {
51
+ if (!panel.matches(':popover-open')) panel.showPopover();
52
+ place(panel, trigger, matchWidth);
53
+ window.addEventListener('scroll', reposition, true); // capture: any scrolling ancestor
54
+ window.addEventListener('resize', reposition);
55
+ }
56
+ },
57
+ hide: function () {
58
+ if (supported) {
59
+ window.removeEventListener('scroll', reposition, true);
60
+ window.removeEventListener('resize', reposition);
61
+ if (panel.matches(':popover-open')) panel.hidePopover();
62
+ }
63
+ panel.hidden = true;
64
+ }
65
+ };
66
+ }
67
+
68
+ window.FreedayPopover = { attach: attach, place: place, supported: supported };
69
+ })();
@@ -0,0 +1,50 @@
1
+ /* Freeday — rating enhancer (optional, zero-dependency).
2
+ * Fills stars up to the checked radio (and previews on hover) for an interactive
3
+ * [data-fdy-rating] radio group; for [data-fdy-readonly] it fills from data-value.
4
+ * The radios are native — arrow-key selection and form association come for free.
5
+ */
6
+ (function () {
7
+ 'use strict';
8
+
9
+ function initRating(root) {
10
+ if (root.dataset.fdyRatingReady === '1') return;
11
+ root.dataset.fdyRatingReady = '1';
12
+
13
+ var stars = Array.prototype.slice.call(root.querySelectorAll('.fdy-rating__star'));
14
+ if (!stars.length) return;
15
+
16
+ function fill(n) {
17
+ stars.forEach(function (s, i) { s.classList.toggle('is-filled', i < n); });
18
+ }
19
+
20
+ if (root.hasAttribute('data-fdy-readonly')) {
21
+ fill(parseInt(root.getAttribute('data-value'), 10) || 0);
22
+ return;
23
+ }
24
+
25
+ var inputs = stars.map(function (s) { return s.querySelector('input'); });
26
+ function current() {
27
+ for (var i = 0; i < inputs.length; i++) { if (inputs[i] && inputs[i].checked) return i + 1; }
28
+ return 0;
29
+ }
30
+
31
+ fill(current());
32
+ root.addEventListener('change', function () { fill(current()); });
33
+ stars.forEach(function (s, i) {
34
+ s.addEventListener('mouseenter', function () { fill(i + 1); });
35
+ });
36
+ root.addEventListener('mouseleave', function () { fill(current()); });
37
+ }
38
+
39
+ function initAll(context) {
40
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-rating]'), initRating);
41
+ }
42
+
43
+ if (document.readyState === 'loading') {
44
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
45
+ } else {
46
+ initAll();
47
+ }
48
+
49
+ window.FreedayRating = { init: initRating, initAll: initAll };
50
+ })();
@@ -0,0 +1,194 @@
1
+ /* Freeday — combobox enhancer (optional, zero-dependency).
2
+ * Implements the WAI-ARIA APG "select-only combobox" pattern for every
3
+ * [data-fdy-combo] on the page. Progressive enhancement: the markup is the
4
+ * component, this script wires behavior. In a Vue/React/Blazor app you would
5
+ * bind your own state instead — this is the framework-agnostic reference.
6
+ *
7
+ * Expected markup:
8
+ * <div class="fdy-combo" data-fdy-combo data-value="x">
9
+ * <button type="button" class="fdy-combo__button" role="combobox"
10
+ * aria-haspopup="listbox" aria-expanded="false"
11
+ * aria-labelledby="LBL VAL">
12
+ * <span class="fdy-combo__value" id="VAL">Label</span>
13
+ * </button>
14
+ * <ul class="fdy-combo__listbox" role="listbox" hidden>
15
+ * <li class="fdy-combo__option" role="option" data-value="x"
16
+ * aria-selected="true"><span class="fdy-combo__check">✓</span>Label</li>
17
+ * </ul>
18
+ * </div>
19
+ *
20
+ * Emits a bubbling "fdy-change" CustomEvent on the root with detail.value.
21
+ */
22
+ (function () {
23
+ 'use strict';
24
+
25
+ var seq = 0;
26
+ var openClose = null; // close() of the currently-open combo, or null
27
+
28
+ function optionLabel(opt) {
29
+ var check = opt.querySelector('.fdy-combo__check');
30
+ var node = check && check.nextSibling ? check.nextSibling.textContent : opt.textContent;
31
+ return (node || '').trim();
32
+ }
33
+
34
+ function initCombo(root) {
35
+ if (root.dataset.fdyComboReady === '1') return; // idempotent
36
+ var button = root.querySelector('.fdy-combo__button');
37
+ var listbox = root.querySelector('.fdy-combo__listbox');
38
+ var valueEl = root.querySelector('.fdy-combo__value');
39
+ var options = Array.prototype.slice.call(root.querySelectorAll('.fdy-combo__option'));
40
+ if (!button || !listbox || !valueEl || options.length === 0) return;
41
+ root.dataset.fdyComboReady = '1';
42
+
43
+ seq += 1;
44
+ if (!listbox.id) listbox.id = 'fdy-combo-list-' + seq;
45
+ button.setAttribute('aria-controls', listbox.id);
46
+ options.forEach(function (opt, i) {
47
+ if (!opt.id) opt.id = listbox.id + '-opt-' + i;
48
+ });
49
+
50
+ var typed = '';
51
+ var typedTimer = null;
52
+
53
+ function highlightedIndex() {
54
+ for (var i = 0; i < options.length; i++) {
55
+ if (options[i].classList.contains('is-highlighted')) return i;
56
+ }
57
+ return -1;
58
+ }
59
+ function setHighlight(i) {
60
+ options.forEach(function (opt, idx) {
61
+ opt.classList.toggle('is-highlighted', idx === i);
62
+ });
63
+ if (i >= 0) {
64
+ button.setAttribute('aria-activedescendant', options[i].id);
65
+ options[i].scrollIntoView({ block: 'nearest' });
66
+ } else {
67
+ button.removeAttribute('aria-activedescendant');
68
+ }
69
+ }
70
+ function isOpen() {
71
+ return !listbox.hidden;
72
+ }
73
+ var _pop = null;
74
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(listbox, button); return _pop; }
75
+ function open() {
76
+ if (openClose && openClose !== close) openClose();
77
+ var p = popCtl(); if (p) p.show(); else listbox.hidden = false;
78
+ button.classList.add('is-open');
79
+ button.setAttribute('aria-expanded', 'true');
80
+ var selected = options.findIndex(function (opt) {
81
+ return opt.getAttribute('aria-selected') === 'true';
82
+ });
83
+ setHighlight(selected < 0 ? 0 : selected);
84
+ openClose = close;
85
+ }
86
+ function close() {
87
+ var p = popCtl(); if (p) p.hide(); else listbox.hidden = true;
88
+ button.classList.remove('is-open');
89
+ button.setAttribute('aria-expanded', 'false');
90
+ button.removeAttribute('aria-activedescendant');
91
+ setHighlight(-1);
92
+ if (openClose === close) openClose = null;
93
+ }
94
+ function choose(opt) {
95
+ options.forEach(function (other) {
96
+ var on = other === opt;
97
+ other.setAttribute('aria-selected', on ? 'true' : 'false');
98
+ var check = other.querySelector('.fdy-combo__check');
99
+ if (check) check.textContent = on ? '✓' : '';
100
+ });
101
+ root.setAttribute('data-value', opt.getAttribute('data-value') || '');
102
+ valueEl.textContent = optionLabel(opt);
103
+ valueEl.classList.remove('fdy-combo__value--placeholder');
104
+ root.dispatchEvent(new CustomEvent('fdy-change', {
105
+ bubbles: true,
106
+ detail: { value: opt.getAttribute('data-value') }
107
+ }));
108
+ close();
109
+ button.focus();
110
+ }
111
+
112
+ button.addEventListener('click', function () {
113
+ isOpen() ? close() : open();
114
+ });
115
+ // Pressing an option must not blur the button: a blur fires the focusout
116
+ // handler below, which closes the listbox before the option's click lands —
117
+ // so the pick is lost (real mouse only; synthetic .click() never blurs).
118
+ // preventDefault on mousedown keeps focus on the button without blocking click.
119
+ listbox.addEventListener('mousedown', function (e) { e.preventDefault(); });
120
+ options.forEach(function (opt, i) {
121
+ opt.addEventListener('click', function () { choose(opt); });
122
+ opt.addEventListener('mousemove', function () { setHighlight(i); });
123
+ });
124
+
125
+ root.addEventListener('keydown', function (e) {
126
+ var hi = highlightedIndex();
127
+ switch (e.key) {
128
+ case 'ArrowDown':
129
+ e.preventDefault();
130
+ isOpen() ? setHighlight((hi + 1) % options.length) : open();
131
+ break;
132
+ case 'ArrowUp':
133
+ e.preventDefault();
134
+ isOpen() ? setHighlight((hi - 1 + options.length) % options.length) : open();
135
+ break;
136
+ case 'Home':
137
+ if (isOpen()) { e.preventDefault(); setHighlight(0); }
138
+ break;
139
+ case 'End':
140
+ if (isOpen()) { e.preventDefault(); setHighlight(options.length - 1); }
141
+ break;
142
+ case 'Enter':
143
+ case ' ':
144
+ e.preventDefault();
145
+ if (isOpen() && hi >= 0) choose(options[hi]); else open();
146
+ break;
147
+ case 'Escape':
148
+ if (isOpen()) { e.preventDefault(); close(); button.focus(); }
149
+ break;
150
+ case 'Tab':
151
+ if (isOpen()) close();
152
+ break;
153
+ default:
154
+ if (e.key.length === 1 && /\S/.test(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey) {
155
+ typed += e.key.toLowerCase();
156
+ clearTimeout(typedTimer);
157
+ typedTimer = setTimeout(function () { typed = ''; }, 500);
158
+ var match = options.findIndex(function (opt) {
159
+ return optionLabel(opt).toLowerCase().indexOf(typed) === 0;
160
+ });
161
+ if (match >= 0) { if (!isOpen()) open(); setHighlight(match); }
162
+ }
163
+ }
164
+ });
165
+
166
+ // Close when focus leaves the whole combo (e.g. Shift+Tab off the button).
167
+ root.addEventListener('focusout', function (e) {
168
+ if (!root.contains(e.relatedTarget)) close();
169
+ });
170
+ }
171
+
172
+ function initAll(context) {
173
+ var scope = context || document;
174
+ Array.prototype.forEach.call(
175
+ scope.querySelectorAll('[data-fdy-combo]'),
176
+ initCombo
177
+ );
178
+ }
179
+
180
+ // One global handler closes the open combo on an outside click.
181
+ document.addEventListener('click', function (e) {
182
+ if (openClose && !(e.target.closest && e.target.closest('.fdy-combo'))) {
183
+ openClose();
184
+ }
185
+ });
186
+
187
+ if (document.readyState === 'loading') {
188
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
189
+ } else {
190
+ initAll();
191
+ }
192
+
193
+ window.FreedayCombo = { init: initCombo, initAll: initAll };
194
+ })();
@@ -0,0 +1,34 @@
1
+ /* Freeday — slider value binding (optional, zero-dependency).
2
+ * Mirrors a [data-fdy-slider] range input's value into <output for="inputId">, with an
3
+ * optional prefix/suffix on the output (data-fdy-prefix / data-fdy-suffix). The range itself
4
+ * is a native input — keyboard and ARIA are built in; this only updates the visible readout.
5
+ */
6
+ (function () {
7
+ 'use strict';
8
+
9
+ function formatted(out, value) {
10
+ return (out.getAttribute('data-fdy-prefix') || '') + value + (out.getAttribute('data-fdy-suffix') || '');
11
+ }
12
+
13
+ function initSlider(input) {
14
+ if (input.dataset.fdySliderReady === '1') return;
15
+ input.dataset.fdySliderReady = '1';
16
+ var out = input.id ? document.querySelector('output[for="' + input.id + '"]') : null;
17
+ if (!out) return;
18
+ function sync() { out.textContent = formatted(out, input.value); }
19
+ input.addEventListener('input', sync);
20
+ sync();
21
+ }
22
+
23
+ function initAll(context) {
24
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-slider]'), initSlider);
25
+ }
26
+
27
+ if (document.readyState === 'loading') {
28
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
29
+ } else {
30
+ initAll();
31
+ }
32
+
33
+ window.FreedaySlider = { init: initSlider, initAll: initAll };
34
+ })();
@@ -0,0 +1,90 @@
1
+ /* Freeday — stepper/wizard enhancer (optional, zero-dependency).
2
+ * Drives a step header + panels: prev/next navigation, completed/active state, and
3
+ * clicking a reached step to revisit it (linear — can't skip ahead). Auto-inits
4
+ * [data-fdy-stepper].
5
+ *
6
+ * Markup: <div data-fdy-stepper>
7
+ * <ol class="fdy-stepper">
8
+ * <li class="fdy-step"><button class="fdy-step__btn"><span class="fdy-step__marker">1</span>
9
+ * <span class="fdy-step__label">…</span></button></li>… </ol>
10
+ * <div class="fdy-step-panels">
11
+ * <div class="fdy-step-panel">…</div>… </div>
12
+ * <div class="fdy-step-nav"><button data-fdy-step-prev>…</button>
13
+ * <button data-fdy-step-next>…</button></div></div>
14
+ * Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step).
15
+ */
16
+ (function () {
17
+ 'use strict';
18
+
19
+ var CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>';
20
+
21
+ function initStepper(root) {
22
+ if (root.dataset.fdyStepperReady === '1') return;
23
+ root.dataset.fdyStepperReady = '1';
24
+
25
+ var steps = Array.prototype.slice.call(root.querySelectorAll('.fdy-step'));
26
+ var panels = Array.prototype.slice.call(root.querySelectorAll('.fdy-step-panel'));
27
+ var prevBtn = root.querySelector('[data-fdy-step-prev]');
28
+ var nextBtn = root.querySelector('[data-fdy-step-next]');
29
+ if (!steps.length) return;
30
+
31
+ var active = 0;
32
+ for (var i = 0; i < steps.length; i++) {
33
+ if (steps[i].classList.contains('is-active')) { active = i; break; }
34
+ }
35
+ var maxReached = active;
36
+ var nextLabel = nextBtn ? nextBtn.textContent : '';
37
+
38
+ steps.forEach(function (s) {
39
+ var marker = s.querySelector('.fdy-step__marker');
40
+ if (marker && marker.dataset.num == null) marker.dataset.num = marker.textContent.trim();
41
+ });
42
+
43
+ function render() {
44
+ steps.forEach(function (s, i) {
45
+ s.classList.toggle('is-active', i === active);
46
+ s.classList.toggle('is-complete', i < active);
47
+ if (i === active) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
48
+ var marker = s.querySelector('.fdy-step__marker');
49
+ if (marker) marker.innerHTML = i < active ? CHECK : (marker.dataset.num || String(i + 1));
50
+ var btn = s.querySelector('.fdy-step__btn');
51
+ if (btn) btn.disabled = i > maxReached;
52
+ });
53
+ panels.forEach(function (p, i) { p.hidden = i !== active; });
54
+ if (prevBtn) prevBtn.disabled = active === 0;
55
+ if (nextBtn) nextBtn.textContent = active === steps.length - 1 ? 'Selesai' : (nextLabel || 'Lanjut');
56
+ root.dispatchEvent(new CustomEvent('fdy-step-change', { bubbles: true, detail: { index: active } }));
57
+ }
58
+
59
+ function go(i) {
60
+ if (i < 0 || i >= steps.length) return;
61
+ active = i;
62
+ if (active > maxReached) maxReached = active;
63
+ render();
64
+ }
65
+
66
+ if (prevBtn) prevBtn.addEventListener('click', function () { go(active - 1); });
67
+ if (nextBtn) nextBtn.addEventListener('click', function () {
68
+ if (active < steps.length - 1) go(active + 1);
69
+ else root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true }));
70
+ });
71
+ steps.forEach(function (s, i) {
72
+ var btn = s.querySelector('.fdy-step__btn');
73
+ if (btn) btn.addEventListener('click', function () { if (i <= maxReached) go(i); });
74
+ });
75
+
76
+ render();
77
+ }
78
+
79
+ function initAll(context) {
80
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-stepper]'), initStepper);
81
+ }
82
+
83
+ if (document.readyState === 'loading') {
84
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
85
+ } else {
86
+ initAll();
87
+ }
88
+
89
+ window.FreedayStepper = { init: initStepper, initAll: initAll };
90
+ })();