@cahyo-dimas/freeday 2.1.0 → 2.2.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.
package/dist/freeday.js CHANGED
@@ -250,6 +250,9 @@
250
250
  var _pop = null;
251
251
  function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(listbox, input); return _pop; }
252
252
  function open() {
253
+ /* The input carries these natively; a disabled one fires nothing, but a READONLY one still
254
+ takes focus and clicks, and the list used to open over a field nobody can edit. */
255
+ if (input.readOnly || input.disabled) return;
253
256
  if (!listbox.hidden) return;
254
257
  var p = popCtl(); if (p) p.show(); else listbox.hidden = false;
255
258
  input.setAttribute('aria-expanded', 'true');
@@ -344,7 +347,20 @@
344
347
  initAll();
345
348
  }
346
349
 
347
- window.FreedayAutocomplete = { init: initAutocomplete, initAll: initAll };
350
+ /* Same contract as the combo's: the states live on the input natively, and a host that stops
351
+ re-rendering after hydration needs a way to change them afterwards. */
352
+ function setState(root, state) {
353
+ var input = root ? root.querySelector('input') : null;
354
+ if (!input || !state) return;
355
+ if (state.disabled != null) input.disabled = !!state.disabled;
356
+ if (state.readonly != null) input.readOnly = !!state.readonly;
357
+ if (state.invalid != null) {
358
+ if (state.invalid) input.setAttribute('aria-invalid', 'true');
359
+ else input.removeAttribute('aria-invalid');
360
+ }
361
+ }
362
+
363
+ window.FreedayAutocomplete = { init: initAutocomplete, initAll: initAll, setState: setState };
348
364
  })();
349
365
 
350
366
  /* Freeday, breakpoint provider (optional, zero-dependency).
@@ -603,6 +619,8 @@
603
619
  * forking this file. Keeping them in ONE table is also what lets a guard prove none is
604
620
  * hard-coded further down. */
605
621
  var TEXT = {
622
+ label: 'Select',
623
+ placeholder: 'Select…',
606
624
  back: 'Back one level',
607
625
  submenu: '{label}, submenu'
608
626
  };
@@ -632,8 +650,8 @@
632
650
  var root = sourceUl ? parse(sourceUl) : [];
633
651
  if (sourceUl) sourceUl.remove();
634
652
 
635
- var label = wrap.getAttribute('data-label') || 'Select';
636
- var placeholder = wrap.getAttribute('data-placeholder') || 'Select…';
653
+ var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
654
+ var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
637
655
  var sep = wrap.getAttribute('data-separator') || ' / ';
638
656
 
639
657
  var trigger = document.createElement('button');
@@ -748,7 +766,44 @@
748
766
 
749
767
  var _pop = null;
750
768
  function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
769
+
770
+ /* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
771
+ `[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
772
+ the control natively had them. Read from the seed at init and settable afterwards, because
773
+ a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
774
+ express a later change any other way. */
775
+ function flagOf(name) {
776
+ var v = wrap.getAttribute('data-' + name);
777
+ return v != null && v !== 'false';
778
+ }
779
+ /* Named `state*` to match the datepicker, where `is*` collided with an older function. */
780
+ var stateDisabled = flagOf('disabled');
781
+ var stateReadonly = flagOf('readonly');
782
+ var stateInvalid = flagOf('invalid');
783
+ /* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
784
+ has to point its label and its error text at, and the raw path had no way to say so. */
785
+ if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
786
+ if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
787
+
788
+ function applyState() {
789
+ trigger.disabled = stateDisabled;
790
+ if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
791
+ else trigger.removeAttribute('aria-readonly');
792
+ if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
793
+ else trigger.removeAttribute('aria-invalid');
794
+ wrap.classList.toggle('fdy-cascade--error', stateInvalid);
795
+ if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
796
+ }
797
+ function setState(next) {
798
+ if (!next) return;
799
+ if (next.disabled != null) stateDisabled = !!next.disabled;
800
+ if (next.readonly != null) stateReadonly = !!next.readonly;
801
+ if (next.invalid != null) stateInvalid = !!next.invalid;
802
+ applyState();
803
+ }
804
+
751
805
  function open() {
806
+ if (stateDisabled || stateReadonly) return;
752
807
  if (!panel.hidden) return;
753
808
  // Re-open at the selected leaf's level for quick re-selection.
754
809
  var trail = selectedValue ? pathTo(root, selectedValue, []) : null;
@@ -803,8 +858,11 @@
803
858
  valueSpan.classList.add('fdy-cascade__value--placeholder');
804
859
  }
805
860
 
861
+ applyState();
862
+
806
863
  var api = {
807
864
  wrap: wrap,
865
+ setState: setState,
808
866
  getValue: function () { return selectedValue; },
809
867
  clear: function () { selectedValue = ''; valueSpan.textContent = placeholder; valueSpan.classList.add('fdy-cascade__value--placeholder'); }
810
868
  };
@@ -825,7 +883,15 @@
825
883
  initAll();
826
884
  }
827
885
 
828
- window.FreedayCascade = { init: initCascade, initAll: initAll };
886
+ window.FreedayCascade = {
887
+ init: initCascade,
888
+ initAll: initAll,
889
+ /* Same reason as the datepicker's: a seed rendered once still has to be lockable later. */
890
+ setState: function (root, state) {
891
+ var api = root && root._fdyCascade ? root._fdyCascade : null;
892
+ if (api && api.setState) api.setState(state);
893
+ }
894
+ };
829
895
  })();
830
896
 
831
897
  /* Freeday, choose-from-list enhancer (optional, zero-dependency).
@@ -1104,6 +1170,28 @@
1104
1170
 
1105
1171
  var NS = 'http://www.w3.org/2000/svg';
1106
1172
 
1173
+ /* User-facing strings. Two of them, and both shipped wrong until 2.2.0: the legend's fallback
1174
+ * label read `Seri 1` — Indonesian, three months after 2.0.0 turned every enhancer English —
1175
+ * and the donut's centre caption was hard-coded, so no host could rename it. Neither was
1176
+ * reachable by the guards: one goes into the DOM through `createTextNode`, the other through
1177
+ * `innerHTML`, and both guards look for `textContent` / `setAttribute`. Overridable per element
1178
+ * with `data-fdy-text-<key>`, like every other enhancer. */
1179
+ var TEXT = {
1180
+ series: 'Series {n}',
1181
+ total: 'Total'
1182
+ };
1183
+ function textAttr(root, key) {
1184
+ if (!root || !root.getAttribute) return null;
1185
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
1186
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
1187
+ }
1188
+ function textOf(root, key, vars) {
1189
+ var custom = textAttr(root, key);
1190
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
1191
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
1192
+ return s;
1193
+ }
1194
+
1107
1195
  // Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
1108
1196
  // (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
1109
1197
  function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
@@ -1312,7 +1400,7 @@
1312
1400
  var li = document.createElement('li');
1313
1401
  var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
1314
1402
  li.appendChild(sw);
1315
- li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
1403
+ li.appendChild(document.createTextNode(s.label || textOf(el, 'series', { n: si + 1 })));
1316
1404
  legend.appendChild(li);
1317
1405
  });
1318
1406
  el.appendChild(legend);
@@ -1486,8 +1574,16 @@
1486
1574
  ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
1487
1575
  var center = document.createElement('div'); center.className = 'fdy-donut__center';
1488
1576
  var centerLabel = el.getAttribute('data-fdy-center');
1489
- center.innerHTML = centerLabel ? '<b></b>' : '<b></b><span>Total</span>';
1490
- center.querySelector('b').textContent = centerLabel != null ? centerLabel : String(total);
1577
+ var centerValue = document.createElement('b');
1578
+ centerValue.textContent = centerLabel != null ? centerLabel : String(total);
1579
+ center.appendChild(centerValue);
1580
+ /* Built rather than assigned as innerHTML: the caption is overridable now, and an author's
1581
+ string is not markup. */
1582
+ if (!centerLabel) {
1583
+ var centerCaption = document.createElement('span');
1584
+ centerCaption.textContent = textOf(el, 'total');
1585
+ center.appendChild(centerCaption);
1586
+ }
1491
1587
  ring.appendChild(center);
1492
1588
  var svg = svgEl('svg');
1493
1589
  svg.setAttribute('class', 'fdy-donut__hit');
@@ -1646,12 +1742,12 @@
1646
1742
  * Locale comes from <html lang> (via Intl), month/weekday/value formatting is not hardcoded.
1647
1743
  *
1648
1744
  * Markup contract:
1649
- * - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Tanggal unggah"
1650
- * data-placeholder="Pilih tanggal" data-min="2026-01-01" data-max="2026-12-31"></div>
1651
- * - Range: <div data-fdy-daterange role="group" aria-label="Rentang tanggal">
1652
- * <div data-fdy-datepicker data-role="from" data-placeholder="Dari"></div>
1745
+ * - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Upload date"
1746
+ * data-placeholder="Choose a date" data-min="2026-01-01" data-max="2026-12-31"></div>
1747
+ * - Range: <div data-fdy-daterange role="group" aria-label="Date range">
1748
+ * <div data-fdy-datepicker data-role="from" data-placeholder="From"></div>
1653
1749
  * <span class="fdy-daterange__sep">–</span>
1654
- * <div data-fdy-datepicker data-role="to" data-placeholder="Sampai"></div>
1750
+ * <div data-fdy-datepicker data-role="to" data-placeholder="To"></div>
1655
1751
  * </div>
1656
1752
  * The range links the two: the end can never precede the start (out-of-range days disable).
1657
1753
  *
@@ -1667,6 +1763,47 @@
1667
1763
  weekday names back automatically. The FALLBACK follows the kit's default language, or a
1668
1764
  page without `lang` would read English labels around Indonesian month names. */
1669
1765
  var LOCALE = document.documentElement.getAttribute('lang') || 'en';
1766
+
1767
+ /* User-facing strings. English by default, and every one overridable per element with
1768
+ * `data-fdy-text-<key>`, so a host that speaks another language (an Indonesian app on the raw
1769
+ * path, and every Blazor app, whose picker IS this enhancer) supplies its own without forking
1770
+ * this file.
1771
+ *
1772
+ * This table arrived late, in 2.2.0: the ten labels below were written as literals passed to
1773
+ * `navButton()` / `titleButton()`, so the guard that proves no enhancer string is hard-coded
1774
+ * never saw them — it looks for the line that writes to the DOM, and here that line only ever
1775
+ * sees a variable. Month and weekday names are NOT here on purpose: they come from `Intl`
1776
+ * through the page's `lang`, which is a better hatch than anything the kit could invent.
1777
+ * The `{label}` in the three title strings is the period the button drills into. */
1778
+ var TEXT = {
1779
+ label: 'Date',
1780
+ placeholder: 'Choose a date',
1781
+ prevMonth: 'Previous month',
1782
+ nextMonth: 'Next month',
1783
+ prevYear: 'Previous year',
1784
+ nextYear: 'Next year',
1785
+ prevYears: 'Previous years',
1786
+ nextYears: 'Next years',
1787
+ chooseMonth: '{label}, choose month',
1788
+ chooseYear: '{label}, choose year',
1789
+ backToMonths: '{start} to {end}, back to months'
1790
+ };
1791
+ /* HTML lowercases attribute names, so a camelCase key like `prevMonth` can only ever be written
1792
+ as `data-fdy-text-prevmonth`, while the kebab form anybody would reach for,
1793
+ `data-fdy-text-prev-month`, becomes a DIFFERENT attribute the enhancer never reads, and the
1794
+ override fails silently. So the key is kebab-cased for the lookup; the run-together spelling
1795
+ still resolves. */
1796
+ function textAttr(root, key) {
1797
+ if (!root || !root.getAttribute) return null;
1798
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
1799
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
1800
+ }
1801
+ function textOf(root, key, vars) {
1802
+ var custom = textAttr(root, key);
1803
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
1804
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
1805
+ return s;
1806
+ }
1670
1807
  var uidSeq = 0;
1671
1808
  function uid(p) { uidSeq += 1; return p + '-' + uidSeq; }
1672
1809
  function pad(n) { return n < 10 ? '0' + n : '' + n; }
@@ -1704,8 +1841,8 @@
1704
1841
  wrap.dataset.fdyDpReady = '1';
1705
1842
  wrap.classList.add('fdy-datepicker');
1706
1843
 
1707
- var placeholder = wrap.getAttribute('data-placeholder') || 'Choose a date';
1708
- var label = wrap.getAttribute('data-label') || 'Date';
1844
+ var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
1845
+ var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
1709
1846
  var selected = parseISO(wrap.getAttribute('data-value'));
1710
1847
  var minDate = parseISO(wrap.getAttribute('data-min'));
1711
1848
  var maxDate = parseISO(wrap.getAttribute('data-max'));
@@ -1803,16 +1940,16 @@
1803
1940
  panel.innerHTML = '';
1804
1941
  var head = document.createElement('div');
1805
1942
  head.className = 'fdy-cal__head';
1806
- var title = titleButton(monthFmt.format(view), monthFmt.format(view) + ', choose month', function () {
1943
+ var title = titleButton(monthFmt.format(view), textOf(wrap, 'chooseMonth', { label: monthFmt.format(view) }), function () {
1807
1944
  mode = 'months';
1808
1945
  focusMonth = view.getMonth();
1809
1946
  render();
1810
1947
  focusMonthCell();
1811
1948
  });
1812
1949
  panel.setAttribute('aria-labelledby', title.id);
1813
- head.appendChild(navButton('‹', 'Previous month', function () { view = addMonths(view, -1); render(); }));
1950
+ head.appendChild(navButton('‹', textOf(wrap, 'prevMonth'), function () { view = addMonths(view, -1); render(); }));
1814
1951
  head.appendChild(title);
1815
- head.appendChild(navButton('›', 'Next month', function () { view = addMonths(view, 1); render(); }));
1952
+ head.appendChild(navButton('›', textOf(wrap, 'nextMonth'), function () { view = addMonths(view, 1); render(); }));
1816
1953
  panel.appendChild(head);
1817
1954
 
1818
1955
  var grid = document.createElement('div');
@@ -1869,16 +2006,16 @@
1869
2006
  var year = view.getFullYear();
1870
2007
  var head = document.createElement('div');
1871
2008
  head.className = 'fdy-cal__head';
1872
- var title = titleButton(String(year), year + ', choose year', function () {
2009
+ var title = titleButton(String(year), textOf(wrap, 'chooseYear', { label: year }), function () {
1873
2010
  mode = 'years';
1874
2011
  focusYear = year;
1875
2012
  render();
1876
2013
  focusYearCell();
1877
2014
  });
1878
2015
  panel.setAttribute('aria-labelledby', title.id);
1879
- head.appendChild(navButton('‹', 'Previous year', function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
2016
+ head.appendChild(navButton('‹', textOf(wrap, 'prevYear'), function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
1880
2017
  head.appendChild(title);
1881
- head.appendChild(navButton('›', 'Next year', function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
2018
+ head.appendChild(navButton('›', textOf(wrap, 'nextYear'), function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
1882
2019
  panel.appendChild(head);
1883
2020
 
1884
2021
  var grid = document.createElement('div');
@@ -1930,16 +2067,16 @@
1930
2067
  var end = start + YEARS_PER_PAGE - 1;
1931
2068
  var head = document.createElement('div');
1932
2069
  head.className = 'fdy-cal__head';
1933
- var title = titleButton(start + ' – ' + end, start + ' to ' + end + ', back to months', function () {
2070
+ var title = titleButton(start + ' – ' + end, textOf(wrap, 'backToMonths', { start: start, end: end }), function () {
1934
2071
  mode = 'months';
1935
2072
  focusMonth = view.getMonth();
1936
2073
  render();
1937
2074
  focusMonthCell();
1938
2075
  });
1939
2076
  panel.setAttribute('aria-labelledby', title.id);
1940
- head.appendChild(navButton('‹', 'Previous years', function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
2077
+ head.appendChild(navButton('‹', textOf(wrap, 'prevYears'), function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
1941
2078
  head.appendChild(title);
1942
- head.appendChild(navButton('›', 'Next years', function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
2079
+ head.appendChild(navButton('›', textOf(wrap, 'nextYears'), function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
1943
2080
  panel.appendChild(head);
1944
2081
 
1945
2082
  var grid = document.createElement('div');
@@ -2100,7 +2237,46 @@
2100
2237
 
2101
2238
  var _pop = null;
2102
2239
  function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
2240
+
2241
+ /* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
2242
+ `[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
2243
+ the control natively had them. Read from the seed at init and settable afterwards, because
2244
+ a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
2245
+ express a later change any other way. */
2246
+ function flagOf(name) {
2247
+ var v = wrap.getAttribute('data-' + name);
2248
+ return v != null && v !== 'false';
2249
+ }
2250
+ /* Named `state*`, not `is*`: this file already has an `isDisabled(date)` deciding whether a
2251
+ DAY falls outside min/max, and shadowing it with a boolean made the day grid throw on every
2252
+ render — silently, since the panel still opened and only its cells went missing. */
2253
+ var stateDisabled = flagOf('disabled');
2254
+ var stateReadonly = flagOf('readonly');
2255
+ var stateInvalid = flagOf('invalid');
2256
+ /* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
2257
+ has to point its label and its error text at, and the raw path had no way to say so. */
2258
+ if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
2259
+ if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
2260
+
2261
+ function applyState() {
2262
+ trigger.disabled = stateDisabled;
2263
+ if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
2264
+ else trigger.removeAttribute('aria-readonly');
2265
+ if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
2266
+ else trigger.removeAttribute('aria-invalid');
2267
+ wrap.classList.toggle('fdy-datepicker--error', stateInvalid);
2268
+ if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
2269
+ }
2270
+ function setState(next) {
2271
+ if (!next) return;
2272
+ if (next.disabled != null) stateDisabled = !!next.disabled;
2273
+ if (next.readonly != null) stateReadonly = !!next.readonly;
2274
+ if (next.invalid != null) stateInvalid = !!next.invalid;
2275
+ applyState();
2276
+ }
2277
+
2103
2278
  function open() {
2279
+ if (stateDisabled || stateReadonly) return;
2104
2280
  if (!panel.hidden) return;
2105
2281
  mode = 'days';
2106
2282
  focusDate = selected || focusDate || new Date();
@@ -2130,8 +2306,11 @@
2130
2306
 
2131
2307
  updateDisplay();
2132
2308
 
2309
+ applyState();
2310
+
2133
2311
  var api = {
2134
2312
  wrap: wrap,
2313
+ setState: setState,
2135
2314
  getValue: function () { return selected ? toISO(selected) : ''; },
2136
2315
  clear: function () { selected = null; updateDisplay(); if (!panel.hidden) render(); },
2137
2316
  setMin: function (iso) { minDate = parseISO(iso); if (!panel.hidden) render(); },
@@ -2191,7 +2370,16 @@
2191
2370
  initAll();
2192
2371
  }
2193
2372
 
2194
- window.FreedayDatepicker = { init: initPicker, initAll: initAll };
2373
+ window.FreedayDatepicker = {
2374
+ init: initPicker,
2375
+ initAll: initAll,
2376
+ /* A host that rendered its seed once and cannot re-render it (Blazor) still has to be able to
2377
+ disable, lock or invalidate the field later. */
2378
+ setState: function (root, state) {
2379
+ var api = root && root._fdyDp ? root._fdyDp : null;
2380
+ if (api && api.setState) api.setState(state);
2381
+ }
2382
+ };
2195
2383
  })();
2196
2384
 
2197
2385
  /* Freeday, datetime picker composer (optional, zero-dependency).
@@ -3215,7 +3403,24 @@
3215
3403
  if (root && root._fdyCombo) root._fdyCombo.setValue(value);
3216
3404
  }
3217
3405
 
3218
- window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue };
3406
+ /* Beside setValue for the same reason it exists: a host that renders its markup once (every
3407
+ Blazor wrapper, `ShouldRender => false`) cannot express a later state change any other way,
3408
+ and a parameter that silently stops working after the first render is worse than none. */
3409
+ function setState(root, state) {
3410
+ var button = root ? root.querySelector('.fdy-combo__button') : null;
3411
+ if (!button || !state) return;
3412
+ if (state.disabled != null) button.disabled = !!state.disabled;
3413
+ if (state.readonly != null) {
3414
+ if (state.readonly) button.setAttribute('aria-readonly', 'true');
3415
+ else button.removeAttribute('aria-readonly');
3416
+ }
3417
+ if (state.invalid != null) {
3418
+ if (state.invalid) button.setAttribute('aria-invalid', 'true');
3419
+ else button.removeAttribute('aria-invalid');
3420
+ }
3421
+ }
3422
+
3423
+ window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue, setState: setState };
3219
3424
  })();
3220
3425
 
3221
3426
  /* Freeday, slider value binding (optional, zero-dependency).
@@ -3426,6 +3631,8 @@
3426
3631
  filterText: 'Contains text',
3427
3632
  filterTextPlaceholder: 'Contains…',
3428
3633
  filterEnum: 'Show values',
3634
+ filterMin: 'Min',
3635
+ filterMax: 'Max',
3429
3636
  filterRange: 'Value range',
3430
3637
  reset: 'Reset',
3431
3638
  close: 'Close',
@@ -3698,8 +3905,8 @@
3698
3905
  pop.appendChild(filterTitle(textOf(root, 'filterRange')));
3699
3906
  var range = document.createElement('div');
3700
3907
  range.className = 'fdy-filter__range';
3701
- var minI = numberInput('Min', f.min);
3702
- var maxI = numberInput('Maks', f.max);
3908
+ var minI = numberInput(textOf(root, 'filterMin'), f.min);
3909
+ var maxI = numberInput(textOf(root, 'filterMax'), f.max);
3703
3910
  var applyRange = function () {
3704
3911
  f.min = minI.value !== '' ? parseNum(minI.value) : null;
3705
3912
  f.max = maxI.value !== '' ? parseNum(maxI.value) : null;
@@ -3998,6 +4205,24 @@
3998
4205
  (function () {
3999
4206
  'use strict';
4000
4207
 
4208
+ /* User-facing strings, overridable per element with `data-fdy-text-<key>`. One entry, and it
4209
+ * still needed the table: it reached the DOM as an argument to a helper, which is how it sat
4210
+ * outside every string guard until 2.2.0. */
4211
+ var TEXT = {
4212
+ label: 'Choose a time'
4213
+ };
4214
+ function textAttr(root, key) {
4215
+ if (!root || !root.getAttribute) return null;
4216
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
4217
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
4218
+ }
4219
+ function textOf(root, key, vars) {
4220
+ var custom = textAttr(root, key);
4221
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
4222
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
4223
+ return s;
4224
+ }
4225
+
4001
4226
  var seq = 0;
4002
4227
  var CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>';
4003
4228
 
@@ -4012,7 +4237,7 @@
4012
4237
  wrap.dataset.fdyTpReady = '1';
4013
4238
  wrap.classList.add('fdy-timepicker');
4014
4239
 
4015
- var label = wrap.getAttribute('data-label') || 'Choose a time';
4240
+ var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
4016
4241
  var placeholder = wrap.getAttribute('data-placeholder') || '--:--';
4017
4242
  var step = Math.max(1, parseInt(wrap.getAttribute('data-step') || '30', 10));
4018
4243
  var minM = valid(wrap.getAttribute('data-min')) ? toMin(wrap.getAttribute('data-min')) : 0;
@@ -191,7 +191,7 @@ live docs also have a copy button per component.
191
191
  ```bash
192
192
  npm i @cahyo-dimas/freeday
193
193
  ```
194
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^2.1.0"` (public npm package). `dist/` is
194
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^2.2.0"` (public npm package). `dist/` is
195
195
  committed and published → no build step; `npm ci` runs without auth.
196
196
 
197
197
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@
92
92
  "scripts": {
93
93
  "build": "node tokens/build.mjs",
94
94
  "test": "node --test",
95
- "test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs",
95
+ "test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/picker-states.mjs",
96
96
  "prepack": "node tokens/build.mjs",
97
97
  "version": "node tokens/build.mjs && git add dist",
98
98
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",