@cahyo-dimas/freeday 2.1.0 → 3.0.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 (53) hide show
  1. package/CHANGELOG.md +244 -0
  2. package/COMPONENTS.md +191 -18
  3. package/README.id.md +1 -1
  4. package/README.md +1 -1
  5. package/USAGE.md +1 -1
  6. package/adapters/blazor/FdyAppShell.razor +1 -1
  7. package/adapters/blazor/FdyAppShell.razor.cs +35 -0
  8. package/adapters/blazor/FdyAutocomplete.razor +4 -1
  9. package/adapters/blazor/FdyAutocomplete.razor.cs +26 -0
  10. package/adapters/blazor/FdyCascade.razor +6 -1
  11. package/adapters/blazor/FdyCascade.razor.cs +24 -0
  12. package/adapters/blazor/FdyCombo.razor +1 -0
  13. package/adapters/blazor/FdyCombo.razor.cs +13 -0
  14. package/adapters/blazor/FdyDatepicker.razor +15 -1
  15. package/adapters/blazor/FdyDatepicker.razor.cs +46 -0
  16. package/adapters/blazor/FdyTable.razor +44 -4
  17. package/adapters/blazor/FdyTable.razor.cs +110 -0
  18. package/adapters/blazor/freeday-blazor.js +8 -0
  19. package/adapters/react/components/FdyAppShell.tsx +52 -11
  20. package/adapters/react/components/FdyDrawer.tsx +3 -1
  21. package/adapters/react/components/FdyModal.tsx +3 -1
  22. package/adapters/react/components/FdyTable.tsx +123 -2
  23. package/adapters/vue/components/FdyAppShell.vue +41 -11
  24. package/adapters/vue/components/FdyDrawer.vue +4 -1
  25. package/adapters/vue/components/FdyModal.vue +4 -1
  26. package/adapters/vue/components/FdyTable.vue +125 -4
  27. package/dist/freeday-app-shell.js +23 -5
  28. package/dist/freeday-autocomplete.js +17 -1
  29. package/dist/freeday-busy.js +168 -0
  30. package/dist/freeday-cascade.js +53 -3
  31. package/dist/freeday-chart.js +33 -3
  32. package/dist/freeday-datepicker.js +109 -17
  33. package/dist/freeday-select.js +18 -1
  34. package/dist/freeday-stepper.js +54 -4
  35. package/dist/freeday-table.js +4 -2
  36. package/dist/freeday-timepicker.js +19 -1
  37. package/dist/freeday.bundle.css +616 -39
  38. package/dist/freeday.css +93 -11
  39. package/dist/freeday.js +499 -37
  40. package/dist/freeday.tokens.css +523 -28
  41. package/docs/agent-onboarding.md +4 -0
  42. package/docs/getting-started.md +1 -1
  43. package/package.json +4 -3
  44. package/src/components/app-shell.css +29 -5
  45. package/src/components/appbar.css +2 -2
  46. package/src/components/busy.css +33 -0
  47. package/src/components/card.css +1 -1
  48. package/src/components/drawer.css +1 -1
  49. package/src/components/menu.css +1 -1
  50. package/src/components/modal.css +1 -1
  51. package/src/components/stepper.css +11 -0
  52. package/src/components/table.css +13 -0
  53. package/tokens/tokens.json +54 -10
@@ -0,0 +1,168 @@
1
+ /* Freeday, busy overlay (optional, zero-dependency).
2
+ *
3
+ * Freeday.busy({ caption, delay, mark }) block the screen while an operation runs
4
+ * Freeday.idle() release it
5
+ *
6
+ * Imperative on purpose, like Freeday.toast(): a component API invites two instances, and two
7
+ * blocking overlays with two captions is the failure this exists to prevent. A second busy() while
8
+ * one is up REPLACES the caption rather than stacking.
9
+ *
10
+ * caption what is happening. Announced politely, so make it a sentence a reader would want read
11
+ * out, not a spinner label. Omitted, it falls back to the kit default, which a page
12
+ * overrides once with `data-fdy-text-caption` on <html>.
13
+ * delay ms to wait before it appears (default 120; 0 shows immediately). An operation that
14
+ * finishes in 80ms should never flash a scrim — that reads as a glitch, not as progress.
15
+ * mark an Element to use instead of the default spinner. Element only, never an HTML string:
16
+ * a string here would be an injection point in every app that passed user text through.
17
+ *
18
+ * Not a dialog. Interaction is removed with `inert` on everything else, so there is nothing to trap
19
+ * focus against and nothing to dismiss. Focus is parked on the panel and given back on idle(),
20
+ * because the element it was on is inert by then and the browser would otherwise drop it to <body>.
21
+ */
22
+ (function () {
23
+ 'use strict';
24
+
25
+ var DEFAULT_DELAY = 120;
26
+
27
+ var TEXT = {
28
+ caption: 'Working…'
29
+ };
30
+ /* The overlay has no root of its own to carry an override — it is created, not hydrated — so the
31
+ lookup goes to <html>, the one element every page has before this runs. Kebab-cased for the
32
+ same reason as everywhere else: HTML lowercases attribute names, so a camelCase key could only
33
+ ever be written run-together and the override would fail silently. */
34
+ function textOf(key) {
35
+ var root = document.documentElement;
36
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
37
+ var custom = kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
38
+ return custom != null && custom !== '' ? custom : TEXT[key];
39
+ }
40
+
41
+ var node = null;
42
+ var showTimer = null;
43
+ var inerted = [];
44
+ var returnFocusTo = null;
45
+
46
+ function build() {
47
+ var el = document.createElement('div');
48
+ el.className = 'fdy-busy';
49
+ el.setAttribute('popover', 'manual');
50
+ el.setAttribute('aria-busy', 'true');
51
+ el.tabIndex = -1;
52
+
53
+ var panel = document.createElement('div');
54
+ panel.className = 'fdy-busy__panel';
55
+
56
+ var mark = document.createElement('div');
57
+ mark.className = 'fdy-busy__mark';
58
+ // aria-hidden: the caption below is the message. A second announcement from the spinner's own
59
+ // role="status" would say "busy" twice and name nothing.
60
+ mark.setAttribute('aria-hidden', 'true');
61
+ mark.appendChild(defaultMark());
62
+
63
+ var caption = document.createElement('p');
64
+ caption.className = 'fdy-busy__caption';
65
+ // role="status" rather than a dialog role: this reports a state, it does not ask a question.
66
+ caption.setAttribute('role', 'status');
67
+
68
+ panel.appendChild(mark);
69
+ panel.appendChild(caption);
70
+ el.appendChild(panel);
71
+ return el;
72
+ }
73
+
74
+ function defaultMark() {
75
+ var spinner = document.createElement('span');
76
+ spinner.className = 'fdy-spinner fdy-spinner--lg';
77
+ return spinner;
78
+ }
79
+
80
+ /** inert everything else, remembering ONLY what we set so an app's own inert is never cleared. */
81
+ function block(on) {
82
+ var i;
83
+ if (on) {
84
+ var kids = document.body.children;
85
+ for (i = 0; i < kids.length; i++) {
86
+ var child = kids[i];
87
+ if (child === node || child.hasAttribute('inert')) continue;
88
+ child.setAttribute('inert', '');
89
+ inerted.push(child);
90
+ }
91
+ return;
92
+ }
93
+ for (i = 0; i < inerted.length; i++) inerted[i].removeAttribute('inert');
94
+ inerted = [];
95
+ }
96
+
97
+ function isOpen() {
98
+ return node !== null && node.classList.contains('is-open');
99
+ }
100
+
101
+ function setCaption(text) {
102
+ node.querySelector('.fdy-busy__caption').textContent = text;
103
+ }
104
+
105
+ function setMark(el) {
106
+ var slot = node.querySelector('.fdy-busy__mark');
107
+ while (slot.firstChild) slot.removeChild(slot.firstChild);
108
+ slot.appendChild(el instanceof Element ? el : defaultMark());
109
+ }
110
+
111
+ function show(opts) {
112
+ showTimer = null;
113
+ setCaption(opts.caption == null ? textOf('caption') : String(opts.caption));
114
+ if (opts.mark !== undefined) setMark(opts.mark);
115
+
116
+ returnFocusTo = document.activeElement;
117
+ document.body.appendChild(node);
118
+ node.classList.add('is-open');
119
+ // Top layer, so it also covers an open <dialog>. Where the API is missing the z-index in
120
+ // busy.css is the fallback; it cannot clear a modal, and that is stated in COMPONENTS.md.
121
+ if (typeof node.showPopover === 'function') {
122
+ try { node.showPopover(); } catch (e) { /* already open, or not connected yet */ }
123
+ }
124
+ block(true);
125
+ node.focus();
126
+ }
127
+
128
+ function busy(options) {
129
+ var opts = options || {};
130
+ if (node === null) node = build();
131
+
132
+ // Already up: this is a second owner talking. Update what it says, do not stack.
133
+ if (isOpen()) {
134
+ if (opts.caption != null) setCaption(String(opts.caption));
135
+ if (opts.mark !== undefined) setMark(opts.mark);
136
+ return node;
137
+ }
138
+
139
+ var delay = opts.delay == null ? DEFAULT_DELAY : Number(opts.delay);
140
+ if (showTimer !== null) clearTimeout(showTimer);
141
+ if (delay > 0) showTimer = setTimeout(function () { show(opts); }, delay);
142
+ else show(opts);
143
+ return node;
144
+ }
145
+
146
+ function idle() {
147
+ // Cancels a pending show too: an operation that beat the delay must leave nothing behind.
148
+ if (showTimer !== null) { clearTimeout(showTimer); showTimer = null; }
149
+ if (node === null || !isOpen()) return;
150
+
151
+ block(false);
152
+ if (typeof node.hidePopover === 'function') {
153
+ try { node.hidePopover(); } catch (e) { /* was never in the top layer */ }
154
+ }
155
+ node.classList.remove('is-open');
156
+ if (node.parentNode !== null) node.parentNode.removeChild(node);
157
+
158
+ // Give focus back to whatever had it, now that its ancestor is no longer inert.
159
+ if (returnFocusTo !== null && typeof returnFocusTo.focus === 'function' && returnFocusTo.isConnected) {
160
+ returnFocusTo.focus();
161
+ }
162
+ returnFocusTo = null;
163
+ }
164
+
165
+ window.Freeday = window.Freeday || {};
166
+ window.Freeday.busy = busy;
167
+ window.Freeday.idle = idle;
168
+ })();
@@ -60,6 +60,8 @@
60
60
  * forking this file. Keeping them in ONE table is also what lets a guard prove none is
61
61
  * hard-coded further down. */
62
62
  var TEXT = {
63
+ label: 'Select',
64
+ placeholder: 'Select…',
63
65
  back: 'Back one level',
64
66
  submenu: '{label}, submenu'
65
67
  };
@@ -89,8 +91,8 @@
89
91
  var root = sourceUl ? parse(sourceUl) : [];
90
92
  if (sourceUl) sourceUl.remove();
91
93
 
92
- var label = wrap.getAttribute('data-label') || 'Select';
93
- var placeholder = wrap.getAttribute('data-placeholder') || 'Select…';
94
+ var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
95
+ var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
94
96
  var sep = wrap.getAttribute('data-separator') || ' / ';
95
97
 
96
98
  var trigger = document.createElement('button');
@@ -205,7 +207,44 @@
205
207
 
206
208
  var _pop = null;
207
209
  function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
210
+
211
+ /* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
212
+ `[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
213
+ the control natively had them. Read from the seed at init and settable afterwards, because
214
+ a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
215
+ express a later change any other way. */
216
+ function flagOf(name) {
217
+ var v = wrap.getAttribute('data-' + name);
218
+ return v != null && v !== 'false';
219
+ }
220
+ /* Named `state*` to match the datepicker, where `is*` collided with an older function. */
221
+ var stateDisabled = flagOf('disabled');
222
+ var stateReadonly = flagOf('readonly');
223
+ var stateInvalid = flagOf('invalid');
224
+ /* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
225
+ has to point its label and its error text at, and the raw path had no way to say so. */
226
+ if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
227
+ if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
228
+
229
+ function applyState() {
230
+ trigger.disabled = stateDisabled;
231
+ if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
232
+ else trigger.removeAttribute('aria-readonly');
233
+ if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
234
+ else trigger.removeAttribute('aria-invalid');
235
+ wrap.classList.toggle('fdy-cascade--error', stateInvalid);
236
+ if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
237
+ }
238
+ function setState(next) {
239
+ if (!next) return;
240
+ if (next.disabled != null) stateDisabled = !!next.disabled;
241
+ if (next.readonly != null) stateReadonly = !!next.readonly;
242
+ if (next.invalid != null) stateInvalid = !!next.invalid;
243
+ applyState();
244
+ }
245
+
208
246
  function open() {
247
+ if (stateDisabled || stateReadonly) return;
209
248
  if (!panel.hidden) return;
210
249
  // Re-open at the selected leaf's level for quick re-selection.
211
250
  var trail = selectedValue ? pathTo(root, selectedValue, []) : null;
@@ -260,8 +299,11 @@
260
299
  valueSpan.classList.add('fdy-cascade__value--placeholder');
261
300
  }
262
301
 
302
+ applyState();
303
+
263
304
  var api = {
264
305
  wrap: wrap,
306
+ setState: setState,
265
307
  getValue: function () { return selectedValue; },
266
308
  clear: function () { selectedValue = ''; valueSpan.textContent = placeholder; valueSpan.classList.add('fdy-cascade__value--placeholder'); }
267
309
  };
@@ -282,5 +324,13 @@
282
324
  initAll();
283
325
  }
284
326
 
285
- window.FreedayCascade = { init: initCascade, initAll: initAll };
327
+ window.FreedayCascade = {
328
+ init: initCascade,
329
+ initAll: initAll,
330
+ /* Same reason as the datepicker's: a seed rendered once still has to be lockable later. */
331
+ setState: function (root, state) {
332
+ var api = root && root._fdyCascade ? root._fdyCascade : null;
333
+ if (api && api.setState) api.setState(state);
334
+ }
335
+ };
286
336
  })();
@@ -29,6 +29,28 @@
29
29
 
30
30
  var NS = 'http://www.w3.org/2000/svg';
31
31
 
32
+ /* User-facing strings. Two of them, and both shipped wrong until 2.2.0: the legend's fallback
33
+ * label read `Seri 1` — Indonesian, three months after 2.0.0 turned every enhancer English —
34
+ * and the donut's centre caption was hard-coded, so no host could rename it. Neither was
35
+ * reachable by the guards: one goes into the DOM through `createTextNode`, the other through
36
+ * `innerHTML`, and both guards look for `textContent` / `setAttribute`. Overridable per element
37
+ * with `data-fdy-text-<key>`, like every other enhancer. */
38
+ var TEXT = {
39
+ series: 'Series {n}',
40
+ total: 'Total'
41
+ };
42
+ function textAttr(root, key) {
43
+ if (!root || !root.getAttribute) return null;
44
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
45
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
46
+ }
47
+ function textOf(root, key, vars) {
48
+ var custom = textAttr(root, key);
49
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
50
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
51
+ return s;
52
+ }
53
+
32
54
  // Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
33
55
  // (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
34
56
  function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
@@ -237,7 +259,7 @@
237
259
  var li = document.createElement('li');
238
260
  var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
239
261
  li.appendChild(sw);
240
- li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
262
+ li.appendChild(document.createTextNode(s.label || textOf(el, 'series', { n: si + 1 })));
241
263
  legend.appendChild(li);
242
264
  });
243
265
  el.appendChild(legend);
@@ -411,8 +433,16 @@
411
433
  ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
412
434
  var center = document.createElement('div'); center.className = 'fdy-donut__center';
413
435
  var centerLabel = el.getAttribute('data-fdy-center');
414
- center.innerHTML = centerLabel ? '<b></b>' : '<b></b><span>Total</span>';
415
- center.querySelector('b').textContent = centerLabel != null ? centerLabel : String(total);
436
+ var centerValue = document.createElement('b');
437
+ centerValue.textContent = centerLabel != null ? centerLabel : String(total);
438
+ center.appendChild(centerValue);
439
+ /* Built rather than assigned as innerHTML: the caption is overridable now, and an author's
440
+ string is not markup. */
441
+ if (!centerLabel) {
442
+ var centerCaption = document.createElement('span');
443
+ centerCaption.textContent = textOf(el, 'total');
444
+ center.appendChild(centerCaption);
445
+ }
416
446
  ring.appendChild(center);
417
447
  var svg = svgEl('svg');
418
448
  svg.setAttribute('class', 'fdy-donut__hit');
@@ -3,12 +3,12 @@
3
3
  * Locale comes from <html lang> (via Intl), month/weekday/value formatting is not hardcoded.
4
4
  *
5
5
  * Markup contract:
6
- * - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Tanggal unggah"
7
- * data-placeholder="Pilih tanggal" data-min="2026-01-01" data-max="2026-12-31"></div>
8
- * - Range: <div data-fdy-daterange role="group" aria-label="Rentang tanggal">
9
- * <div data-fdy-datepicker data-role="from" data-placeholder="Dari"></div>
6
+ * - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Upload date"
7
+ * data-placeholder="Choose a date" data-min="2026-01-01" data-max="2026-12-31"></div>
8
+ * - Range: <div data-fdy-daterange role="group" aria-label="Date range">
9
+ * <div data-fdy-datepicker data-role="from" data-placeholder="From"></div>
10
10
  * <span class="fdy-daterange__sep">–</span>
11
- * <div data-fdy-datepicker data-role="to" data-placeholder="Sampai"></div>
11
+ * <div data-fdy-datepicker data-role="to" data-placeholder="To"></div>
12
12
  * </div>
13
13
  * The range links the two: the end can never precede the start (out-of-range days disable).
14
14
  *
@@ -24,6 +24,47 @@
24
24
  weekday names back automatically. The FALLBACK follows the kit's default language, or a
25
25
  page without `lang` would read English labels around Indonesian month names. */
26
26
  var LOCALE = document.documentElement.getAttribute('lang') || 'en';
27
+
28
+ /* User-facing strings. English by default, and every one overridable per element with
29
+ * `data-fdy-text-<key>`, so a host that speaks another language (an Indonesian app on the raw
30
+ * path, and every Blazor app, whose picker IS this enhancer) supplies its own without forking
31
+ * this file.
32
+ *
33
+ * This table arrived late, in 2.2.0: the ten labels below were written as literals passed to
34
+ * `navButton()` / `titleButton()`, so the guard that proves no enhancer string is hard-coded
35
+ * never saw them — it looks for the line that writes to the DOM, and here that line only ever
36
+ * sees a variable. Month and weekday names are NOT here on purpose: they come from `Intl`
37
+ * through the page's `lang`, which is a better hatch than anything the kit could invent.
38
+ * The `{label}` in the three title strings is the period the button drills into. */
39
+ var TEXT = {
40
+ label: 'Date',
41
+ placeholder: 'Choose a date',
42
+ prevMonth: 'Previous month',
43
+ nextMonth: 'Next month',
44
+ prevYear: 'Previous year',
45
+ nextYear: 'Next year',
46
+ prevYears: 'Previous years',
47
+ nextYears: 'Next years',
48
+ chooseMonth: '{label}, choose month',
49
+ chooseYear: '{label}, choose year',
50
+ backToMonths: '{start} to {end}, back to months'
51
+ };
52
+ /* HTML lowercases attribute names, so a camelCase key like `prevMonth` can only ever be written
53
+ as `data-fdy-text-prevmonth`, while the kebab form anybody would reach for,
54
+ `data-fdy-text-prev-month`, becomes a DIFFERENT attribute the enhancer never reads, and the
55
+ override fails silently. So the key is kebab-cased for the lookup; the run-together spelling
56
+ still resolves. */
57
+ function textAttr(root, key) {
58
+ if (!root || !root.getAttribute) return null;
59
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
60
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
61
+ }
62
+ function textOf(root, key, vars) {
63
+ var custom = textAttr(root, key);
64
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
65
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
66
+ return s;
67
+ }
27
68
  var uidSeq = 0;
28
69
  function uid(p) { uidSeq += 1; return p + '-' + uidSeq; }
29
70
  function pad(n) { return n < 10 ? '0' + n : '' + n; }
@@ -61,8 +102,8 @@
61
102
  wrap.dataset.fdyDpReady = '1';
62
103
  wrap.classList.add('fdy-datepicker');
63
104
 
64
- var placeholder = wrap.getAttribute('data-placeholder') || 'Choose a date';
65
- var label = wrap.getAttribute('data-label') || 'Date';
105
+ var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
106
+ var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
66
107
  var selected = parseISO(wrap.getAttribute('data-value'));
67
108
  var minDate = parseISO(wrap.getAttribute('data-min'));
68
109
  var maxDate = parseISO(wrap.getAttribute('data-max'));
@@ -160,16 +201,16 @@
160
201
  panel.innerHTML = '';
161
202
  var head = document.createElement('div');
162
203
  head.className = 'fdy-cal__head';
163
- var title = titleButton(monthFmt.format(view), monthFmt.format(view) + ', choose month', function () {
204
+ var title = titleButton(monthFmt.format(view), textOf(wrap, 'chooseMonth', { label: monthFmt.format(view) }), function () {
164
205
  mode = 'months';
165
206
  focusMonth = view.getMonth();
166
207
  render();
167
208
  focusMonthCell();
168
209
  });
169
210
  panel.setAttribute('aria-labelledby', title.id);
170
- head.appendChild(navButton('‹', 'Previous month', function () { view = addMonths(view, -1); render(); }));
211
+ head.appendChild(navButton('‹', textOf(wrap, 'prevMonth'), function () { view = addMonths(view, -1); render(); }));
171
212
  head.appendChild(title);
172
- head.appendChild(navButton('›', 'Next month', function () { view = addMonths(view, 1); render(); }));
213
+ head.appendChild(navButton('›', textOf(wrap, 'nextMonth'), function () { view = addMonths(view, 1); render(); }));
173
214
  panel.appendChild(head);
174
215
 
175
216
  var grid = document.createElement('div');
@@ -226,16 +267,16 @@
226
267
  var year = view.getFullYear();
227
268
  var head = document.createElement('div');
228
269
  head.className = 'fdy-cal__head';
229
- var title = titleButton(String(year), year + ', choose year', function () {
270
+ var title = titleButton(String(year), textOf(wrap, 'chooseYear', { label: year }), function () {
230
271
  mode = 'years';
231
272
  focusYear = year;
232
273
  render();
233
274
  focusYearCell();
234
275
  });
235
276
  panel.setAttribute('aria-labelledby', title.id);
236
- head.appendChild(navButton('‹', 'Previous year', function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
277
+ head.appendChild(navButton('‹', textOf(wrap, 'prevYear'), function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
237
278
  head.appendChild(title);
238
- head.appendChild(navButton('›', 'Next year', function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
279
+ head.appendChild(navButton('›', textOf(wrap, 'nextYear'), function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
239
280
  panel.appendChild(head);
240
281
 
241
282
  var grid = document.createElement('div');
@@ -287,16 +328,16 @@
287
328
  var end = start + YEARS_PER_PAGE - 1;
288
329
  var head = document.createElement('div');
289
330
  head.className = 'fdy-cal__head';
290
- var title = titleButton(start + ' – ' + end, start + ' to ' + end + ', back to months', function () {
331
+ var title = titleButton(start + ' – ' + end, textOf(wrap, 'backToMonths', { start: start, end: end }), function () {
291
332
  mode = 'months';
292
333
  focusMonth = view.getMonth();
293
334
  render();
294
335
  focusMonthCell();
295
336
  });
296
337
  panel.setAttribute('aria-labelledby', title.id);
297
- head.appendChild(navButton('‹', 'Previous years', function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
338
+ head.appendChild(navButton('‹', textOf(wrap, 'prevYears'), function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
298
339
  head.appendChild(title);
299
- head.appendChild(navButton('›', 'Next years', function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
340
+ head.appendChild(navButton('›', textOf(wrap, 'nextYears'), function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
300
341
  panel.appendChild(head);
301
342
 
302
343
  var grid = document.createElement('div');
@@ -457,7 +498,46 @@
457
498
 
458
499
  var _pop = null;
459
500
  function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
501
+
502
+ /* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
503
+ `[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
504
+ the control natively had them. Read from the seed at init and settable afterwards, because
505
+ a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
506
+ express a later change any other way. */
507
+ function flagOf(name) {
508
+ var v = wrap.getAttribute('data-' + name);
509
+ return v != null && v !== 'false';
510
+ }
511
+ /* Named `state*`, not `is*`: this file already has an `isDisabled(date)` deciding whether a
512
+ DAY falls outside min/max, and shadowing it with a boolean made the day grid throw on every
513
+ render — silently, since the panel still opened and only its cells went missing. */
514
+ var stateDisabled = flagOf('disabled');
515
+ var stateReadonly = flagOf('readonly');
516
+ var stateInvalid = flagOf('invalid');
517
+ /* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
518
+ has to point its label and its error text at, and the raw path had no way to say so. */
519
+ if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
520
+ if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
521
+
522
+ function applyState() {
523
+ trigger.disabled = stateDisabled;
524
+ if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
525
+ else trigger.removeAttribute('aria-readonly');
526
+ if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
527
+ else trigger.removeAttribute('aria-invalid');
528
+ wrap.classList.toggle('fdy-datepicker--error', stateInvalid);
529
+ if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
530
+ }
531
+ function setState(next) {
532
+ if (!next) return;
533
+ if (next.disabled != null) stateDisabled = !!next.disabled;
534
+ if (next.readonly != null) stateReadonly = !!next.readonly;
535
+ if (next.invalid != null) stateInvalid = !!next.invalid;
536
+ applyState();
537
+ }
538
+
460
539
  function open() {
540
+ if (stateDisabled || stateReadonly) return;
461
541
  if (!panel.hidden) return;
462
542
  mode = 'days';
463
543
  focusDate = selected || focusDate || new Date();
@@ -487,8 +567,11 @@
487
567
 
488
568
  updateDisplay();
489
569
 
570
+ applyState();
571
+
490
572
  var api = {
491
573
  wrap: wrap,
574
+ setState: setState,
492
575
  getValue: function () { return selected ? toISO(selected) : ''; },
493
576
  clear: function () { selected = null; updateDisplay(); if (!panel.hidden) render(); },
494
577
  setMin: function (iso) { minDate = parseISO(iso); if (!panel.hidden) render(); },
@@ -548,5 +631,14 @@
548
631
  initAll();
549
632
  }
550
633
 
551
- window.FreedayDatepicker = { init: initPicker, initAll: initAll };
634
+ window.FreedayDatepicker = {
635
+ init: initPicker,
636
+ initAll: initAll,
637
+ /* A host that rendered its seed once and cannot re-render it (Blazor) still has to be able to
638
+ disable, lock or invalidate the field later. */
639
+ setState: function (root, state) {
640
+ var api = root && root._fdyDp ? root._fdyDp : null;
641
+ if (api && api.setState) api.setState(state);
642
+ }
643
+ };
552
644
  })();
@@ -210,5 +210,22 @@
210
210
  if (root && root._fdyCombo) root._fdyCombo.setValue(value);
211
211
  }
212
212
 
213
- window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue };
213
+ /* Beside setValue for the same reason it exists: a host that renders its markup once (every
214
+ Blazor wrapper, `ShouldRender => false`) cannot express a later state change any other way,
215
+ and a parameter that silently stops working after the first render is worse than none. */
216
+ function setState(root, state) {
217
+ var button = root ? root.querySelector('.fdy-combo__button') : null;
218
+ if (!button || !state) return;
219
+ if (state.disabled != null) button.disabled = !!state.disabled;
220
+ if (state.readonly != null) {
221
+ if (state.readonly) button.setAttribute('aria-readonly', 'true');
222
+ else button.removeAttribute('aria-readonly');
223
+ }
224
+ if (state.invalid != null) {
225
+ if (state.invalid) button.setAttribute('aria-invalid', 'true');
226
+ else button.removeAttribute('aria-invalid');
227
+ }
228
+ }
229
+
230
+ window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue, setState: setState };
214
231
  })();
@@ -11,7 +11,9 @@
11
11
  * <div class="fdy-step-panel">…</div>… </div>
12
12
  * <div class="fdy-step-nav"><button data-fdy-step-prev>…</button>
13
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).
14
+ * Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step), and a
15
+ * cancelable "fdy-step-before-change" {from, to, waitFor} that a guard refuses with
16
+ * preventDefault() or defers by assigning a promise to detail.waitFor.
15
17
  */
16
18
  (function () {
17
19
  'use strict';
@@ -90,14 +92,62 @@
90
92
  render();
91
93
  }
92
94
 
95
+ /* Leaving a step is REFUSABLE, because a wizard whose Next cannot be stopped is a wizard that
96
+ * validates nothing. Two ways to refuse, and the second is why an event alone was not enough:
97
+ *
98
+ * sync handler calls ev.preventDefault() — the answer is already known
99
+ * async handler sets ev.detail.waitFor = promise — the answer is a server round-trip away
100
+ *
101
+ * Resolving to `false` refuses; anything else advances, so a handler that forgets to return is
102
+ * not read as a rejection. HOW validity is decided stays entirely with the app: the kit has no
103
+ * opinion about form libraries and this is the line that keeps it that way. */
104
+ var deciding = false;
105
+
106
+ function lock(on) {
107
+ deciding = on;
108
+ var list = root.querySelector('.fdy-stepper');
109
+ if (list) { if (on) list.setAttribute('aria-busy', 'true'); else list.removeAttribute('aria-busy'); }
110
+ if (prevBtn) prevBtn.disabled = on || active === 0;
111
+ if (nextBtn) nextBtn.disabled = on;
112
+ }
113
+
114
+ function request(to, onAllowed) {
115
+ if (deciding) return;
116
+ var ev = new CustomEvent('fdy-step-before-change', {
117
+ bubbles: true,
118
+ cancelable: true,
119
+ detail: { from: active, to: to, waitFor: null },
120
+ });
121
+ root.dispatchEvent(ev);
122
+ if (ev.defaultPrevented) return;
123
+
124
+ var pending = ev.detail.waitFor;
125
+ if (pending === null || typeof pending.then !== 'function') { onAllowed(); return; }
126
+
127
+ lock(true);
128
+ pending.then(
129
+ function (ok) { lock(false); if (ok !== false) onAllowed(); },
130
+ // A guard that THREW decided nothing, so it must not advance. Staying put with the nav
131
+ // released is the only safe reading; the app's own error handling reports the failure.
132
+ function () { lock(false); },
133
+ );
134
+ }
135
+
93
136
  if (prevBtn) prevBtn.addEventListener('click', function () { go(active - 1); });
94
137
  if (nextBtn) nextBtn.addEventListener('click', function () {
95
- if (active < steps.length - 1) go(active + 1);
96
- else root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true }));
138
+ if (active < steps.length - 1) request(active + 1, function () { go(active + 1); });
139
+ else request(active + 1, function () { root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true })); });
97
140
  });
98
141
  steps.forEach(function (s, i) {
99
142
  var btn = s.querySelector('.fdy-step__btn');
100
- if (btn) btn.addEventListener('click', function () { if (i <= maxReached) go(i); });
143
+ if (!btn) return;
144
+ btn.addEventListener('click', function () {
145
+ if (i > maxReached) return;
146
+ // Going BACK is always allowed — nothing is being committed. Jumping forward to a step
147
+ // already reached still leaves the current one behind, so it asks the guard like Next does.
148
+ if (i <= active) go(i);
149
+ else request(i, function () { go(i); });
150
+ });
101
151
  });
102
152
 
103
153
  render();
@@ -47,6 +47,8 @@
47
47
  filterText: 'Contains text',
48
48
  filterTextPlaceholder: 'Contains…',
49
49
  filterEnum: 'Show values',
50
+ filterMin: 'Min',
51
+ filterMax: 'Max',
50
52
  filterRange: 'Value range',
51
53
  reset: 'Reset',
52
54
  close: 'Close',
@@ -319,8 +321,8 @@
319
321
  pop.appendChild(filterTitle(textOf(root, 'filterRange')));
320
322
  var range = document.createElement('div');
321
323
  range.className = 'fdy-filter__range';
322
- var minI = numberInput('Min', f.min);
323
- var maxI = numberInput('Maks', f.max);
324
+ var minI = numberInput(textOf(root, 'filterMin'), f.min);
325
+ var maxI = numberInput(textOf(root, 'filterMax'), f.max);
324
326
  var applyRange = function () {
325
327
  f.min = minI.value !== '' ? parseNum(minI.value) : null;
326
328
  f.max = maxI.value !== '' ? parseNum(maxI.value) : null;