@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,181 @@
1
+ /* Freeday — form validation enhancer (optional, zero-dependency).
2
+ * Wires the native Constraint Validation API to accessible inline errors on any
3
+ * [data-fdy-validate] <form>. Native-first: constraints come from the standard
4
+ * markup attributes (required, type, pattern, min/max, minlength/maxlength, step);
5
+ * this only handles presentation + the accessible plumbing.
6
+ *
7
+ * Per control it toggles aria-invalid (which the CSS mirrors to the --error border)
8
+ * and shows the message in a linked [data-fdy-error] element (auto-created inside
9
+ * the .fdy-field if absent, and wired via aria-describedby).
10
+ *
11
+ * Custom messages via data attributes on the control (first match wins):
12
+ * data-fdy-msg-required | -type | -pattern | -minlength | -maxlength |
13
+ * data-fdy-msg-min | -max | -step | -mismatch, then generic data-fdy-msg.
14
+ * Cross-field match (e.g. confirm password): data-fdy-match="#otherId".
15
+ *
16
+ * Timing: validates on submit (blocks + focuses the first invalid control); after
17
+ * the first submit attempt each field re-validates on input/blur so errors clear
18
+ * live. Add data-fdy-eager to validate on blur from the start.
19
+ *
20
+ * Emits bubbling CustomEvents on the form: "fdy-form-invalid" (detail {invalid:[]})
21
+ * and "fdy-form-valid". Exposes window.FreedayForm.
22
+ */
23
+ (function () {
24
+ 'use strict';
25
+
26
+ var seq = 0;
27
+
28
+ // ValidityState key -> friendly data-attribute alias.
29
+ var ALIAS = {
30
+ valueMissing: 'required',
31
+ typeMismatch: 'type',
32
+ patternMismatch: 'pattern',
33
+ tooShort: 'minlength',
34
+ tooLong: 'maxlength',
35
+ rangeUnderflow: 'min',
36
+ rangeOverflow: 'max',
37
+ stepMismatch: 'step',
38
+ badInput: 'type',
39
+ customError: 'mismatch'
40
+ };
41
+ var DEFAULTS = {
42
+ required: 'Wajib diisi.',
43
+ type: 'Format tidak valid.',
44
+ pattern: 'Format tidak sesuai.',
45
+ minlength: 'Terlalu pendek.',
46
+ maxlength: 'Terlalu panjang.',
47
+ min: 'Nilai terlalu kecil.',
48
+ max: 'Nilai terlalu besar.',
49
+ step: 'Nilai tidak sesuai kelipatan.',
50
+ mismatch: 'Nilai tidak cocok.'
51
+ };
52
+ var VALIDITY_KEYS = ['valueMissing', 'typeMismatch', 'patternMismatch', 'tooShort', 'tooLong', 'rangeUnderflow', 'rangeOverflow', 'stepMismatch', 'badInput'];
53
+
54
+ function isCandidate(el) {
55
+ if (el.disabled || el.type === 'hidden' || el.type === 'submit' || el.type === 'button' || el.type === 'reset') return false;
56
+ return typeof el.checkValidity === 'function' && el.willValidate;
57
+ }
58
+
59
+ function fieldOf(el) { return el.closest('.fdy-field') || el.parentElement; }
60
+
61
+ function ensureErrorEl(el) {
62
+ var field = fieldOf(el);
63
+ var errEl = field ? field.querySelector('[data-fdy-error]') : null;
64
+ if (!errEl) {
65
+ errEl = document.createElement('p');
66
+ errEl.className = 'fdy-help fdy-help--error';
67
+ errEl.setAttribute('data-fdy-error', '');
68
+ errEl.hidden = true;
69
+ (field || el.parentElement).appendChild(errEl);
70
+ }
71
+ if (!errEl.id) { seq += 1; errEl.id = 'fdy-err-' + seq; }
72
+ // Link the control to its message without clobbering existing describedby.
73
+ var described = (el.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
74
+ if (described.indexOf(errEl.id) === -1) { described.push(errEl.id); el.setAttribute('aria-describedby', described.join(' ')); }
75
+ return errEl;
76
+ }
77
+
78
+ function messageFor(el) {
79
+ // Cross-field match runs first (drives customError via setCustomValidity).
80
+ el.setCustomValidity('');
81
+ var matchSel = el.getAttribute('data-fdy-match');
82
+ if (matchSel) {
83
+ var other = document.querySelector(matchSel);
84
+ if (other && el.value !== other.value) {
85
+ el.setCustomValidity(el.getAttribute('data-fdy-msg-mismatch') || el.getAttribute('data-fdy-msg') || DEFAULTS.mismatch);
86
+ }
87
+ }
88
+ if (el.validity.valid) return '';
89
+ var alias = el.validity.customError ? 'mismatch' : null;
90
+ if (!alias) {
91
+ for (var i = 0; i < VALIDITY_KEYS.length; i++) {
92
+ if (el.validity[VALIDITY_KEYS[i]]) { alias = ALIAS[VALIDITY_KEYS[i]]; break; }
93
+ }
94
+ }
95
+ var custom = alias ? el.getAttribute('data-fdy-msg-' + alias) : null;
96
+ return custom || el.getAttribute('data-fdy-msg') || (alias && DEFAULTS[alias]) || el.validationMessage || 'Tidak valid.';
97
+ }
98
+
99
+ function paint(el, message) {
100
+ var errEl = ensureErrorEl(el);
101
+ if (message) {
102
+ el.setAttribute('aria-invalid', 'true');
103
+ errEl.textContent = message;
104
+ errEl.hidden = false;
105
+ } else {
106
+ el.removeAttribute('aria-invalid');
107
+ errEl.textContent = '';
108
+ errEl.hidden = true;
109
+ }
110
+ }
111
+
112
+ function initForm(form) {
113
+ if (form.dataset.fdyFormReady === '1') return;
114
+ form.dataset.fdyFormReady = '1';
115
+ form.setAttribute('novalidate', '');
116
+ var eager = form.hasAttribute('data-fdy-eager');
117
+ var submitted = false;
118
+
119
+ function controls() {
120
+ return Array.prototype.filter.call(form.elements, isCandidate);
121
+ }
122
+ function validateOne(el) {
123
+ var msg = messageFor(el);
124
+ paint(el, msg);
125
+ return !msg;
126
+ }
127
+ function liveBind(el) {
128
+ if (el.dataset.fdyFieldBound === '1') return;
129
+ el.dataset.fdyFieldBound = '1';
130
+ var live = function () { if (submitted || eager) validateOne(el); };
131
+ el.addEventListener('blur', live, true);
132
+ el.addEventListener('input', function () { if ((submitted || eager) && el.getAttribute('aria-invalid') === 'true') validateOne(el); });
133
+ }
134
+
135
+ Array.prototype.forEach.call(controls(), liveBind);
136
+
137
+ form.addEventListener('submit', function (e) {
138
+ submitted = true;
139
+ var invalid = [];
140
+ Array.prototype.forEach.call(controls(), function (el) {
141
+ liveBind(el);
142
+ if (!validateOne(el)) invalid.push(el);
143
+ });
144
+ if (invalid.length) {
145
+ e.preventDefault();
146
+ invalid[0].focus();
147
+ form.dispatchEvent(new CustomEvent('fdy-form-invalid', { bubbles: true, detail: { invalid: invalid } }));
148
+ } else {
149
+ form.dispatchEvent(new CustomEvent('fdy-form-valid', { bubbles: true }));
150
+ }
151
+ });
152
+
153
+ var api = {
154
+ form: form,
155
+ validate: function () {
156
+ submitted = true;
157
+ var invalid = [];
158
+ Array.prototype.forEach.call(controls(), function (el) { if (!validateOne(el)) invalid.push(el); });
159
+ return invalid;
160
+ },
161
+ reset: function () {
162
+ submitted = false;
163
+ Array.prototype.forEach.call(controls(), function (el) { paint(el, ''); });
164
+ }
165
+ };
166
+ form._fdyForm = api;
167
+ return api;
168
+ }
169
+
170
+ function initAll(context) {
171
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-validate]'), initForm);
172
+ }
173
+
174
+ if (document.readyState === 'loading') {
175
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
176
+ } else {
177
+ initAll();
178
+ }
179
+
180
+ window.FreedayForm = { init: initForm, initAll: initAll };
181
+ })();
@@ -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
+ })();