@cahyo-dimas/freeday 1.38.0 → 1.40.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/CHANGELOG.md CHANGED
@@ -3,6 +3,65 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.40.0] — 2026-08-19
7
+ One note from the back-office app (IDU_EMATE_APPL_WEB, #017).
8
+ ### Fixed
9
+ - **A picker inside a `.fdy-field` now fills it.** `.fdy-field`, `.fdy-combo` and
10
+ `.fdy-input-group` all cap at 22rem; `.fdy-datepicker` capped at 14rem and `.fdy-timepicker`
11
+ at 11rem. In a two-column form grid that left a **224px** date box beside a **319px** combo —
12
+ measured, not eyeballed — and the row read as ragged with no fix available to the consuming
13
+ app, because the cap sits on the component. `.fdy-field > .fdy-datepicker` / `> .fdy-timepicker`
14
+ / `> .fdy-daterange` drop the cap; a picker standing on its own keeps its narrower width, which
15
+ is right for a short value.
16
+
17
+ ### Added — guards
18
+ - **The enhancer string contract is asserted in a real browser** (#016). 1.39.0 moved every
19
+ user-facing string in **nine** enhancers into a `TEXT` table read through `textOf()`, and only
20
+ **one of the nine** — upload — had any browser coverage at all. The node guard proves the
21
+ literals *sit* in the table; it reads the source as text and cannot prove the wiring *runs*, so a
22
+ `zone` out of scope or a `getAttribute` on a non-element would have thrown at init with all 51
23
+ node tests still green. `browser/text-override.mjs` initialises table, stepper, mask, carousel,
24
+ cascade, form and toast on real markup and asserts **both halves**: the documented Indonesian
25
+ default still renders, and `data-fdy-text-*` wins. The default is a contract too — an enhancer
26
+ that quietly turned English would be a breaking change wearing a bugfix's clothes.
27
+ Mutation-tested: making `textOf()` ignore the attribute fails it by name and value.
28
+
29
+ ## [1.39.0] — 2026-08-19
30
+ The sweep #013 §2 filed and #015 restated: the vanilla enhancers' strings are reachable now.
31
+ ### Added
32
+ - **Every enhancer string is overridable with `data-fdy-text-<key>`** (#016). The defaults stay
33
+ Indonesian — that is documented and deliberate for the raw path, and changing it would break
34
+ every app that adopted it — but they were also *unreachable*, and that is the half that was
35
+ wrong. A Blazor app meets these enhancers through `FreedayBlazor.initAll`, which runs every one
36
+ of them, while COMPONENTS.md promises Blazor is English throughout; both promises can only hold
37
+ if the default stays and the string can be changed. Each enhancer now keeps its strings in one
38
+ `TEXT` table read through `textOf()`. `{n}`, `{from}`, `{to}`, `{total}`, `{name}`, `{max}` and
39
+ `{label}` are substituted where the string supports them. Validation messages take the same shape
40
+ on the `<form>`, narrower than the per-field `data-fdy-msg-<alias>` that still wins;
41
+ `Freeday.toast()` takes `closeLabel`.
42
+ - **34 strings across 9 files**, not the 19 across 7 a word-list scan had counted — `carousel`
43
+ (`Slide {n}`), `cascade` (`Kembali satu tingkat`), `mask` (`Tampilkan kata sandi`), `stepper`
44
+ (`Selesai` / `Lanjut`) and four upload status strings were all invisible to it. #015 had just
45
+ finished making this point about `\bcari\b` and `pencarian`; the recount was found by scanning
46
+ by SINK — what lands in a `textContent`, an `aria-label`, a `title` or a `placeholder` — which
47
+ cannot be fooled by an affix.
48
+ ### Fixed
49
+ - **Blazor's `FdyCascade` rendered Indonesian** (#016). `Label` and `Placeholder` were `string?`
50
+ with no default, so an app that did not set them emitted `data-label=""` and the enhancer fell
51
+ back to `Pilih` / `Pilih…`. Vue and React have defaulted to `Select` / `Select…` since #015 —
52
+ same drift as #009 and #015, third time, and the parameter existing is what made it invisible.
53
+ They are non-nullable with English defaults now, and `BackLabel` / `SubmenuLabel` reach the two
54
+ strings the enhancer builds itself, which had no route from .NET at all.
55
+ - **`Freeday.toast()` from Blazor labelled its close button `Tutup`.** `FreedayBlazor.toast` now
56
+ defaults `closeLabel` to `Close` — in the bridge, not the enhancer, so a hand-written page still
57
+ gets the documented Indonesian.
58
+ ### Added — guards
59
+ - **An enhancer string is overridable, not hard-coded** (#016). `npm test` asserts that a literal
60
+ reaching a `textContent`, `aria-label`, `title` or `placeholder` lives in that file's `TEXT`
61
+ table. Scoped by SINK rather than by a word list, deliberately: a second word list would have the
62
+ same hole one affix over, and this one is also blind to no language. Mutation-tested — putting
63
+ `'Tutup'` back into the toast fails it by file and line.
64
+
6
65
  ## [1.38.0] — 2026-08-19
7
66
  One note from the back-office app (IDU_EMATE_APPL_WEB, #015).
8
67
  ### Fixed
package/COMPONENTS.md CHANGED
@@ -759,14 +759,29 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
759
759
  offer) carrying `data-fdy-table-page-size`; the enhancer only wires it, and listens for both
760
760
  `change` and the `fdy-change` a `.fdy-combo` emits, so either kind works.
761
761
  - Sort values: put the raw value in `data-sort-value` when the cell text is formatted.
762
- - **Language caveat:** every user-visible string the **vanilla enhancers** write is Indonesian
763
- the table's footer and bulk count (`Menampilkan 1–5 dari 7`, `N dipilih`), its pager and filter
764
- UI (`Sebelumnya`, `Berikutnya`, `Filter kolom`, `Berisi teks`, `Reset`, `Tutup`), and the same
765
- applies elsewhere (`Bulan berikutnya`, `Format tidak valid.`). There is no override hook. The
762
+ - **Language caveat:** every user-visible string the **vanilla enhancers** write is Indonesian by
763
+ default — the table's footer and bulk count (`Menampilkan 1–5 dari 7`, `N dipilih`), its pager and
764
+ filter UI (`Sebelumnya`, `Berikutnya`, `Filter kolom`, `Berisi teks`, `Reset`, `Tutup`), and the
765
+ same elsewhere (`Format tidak valid.`, `Tampilkan kata sandi`, `Menunggu server…`). The
766
766
  Vue/React/Blazor components are English throughout, so **an English app should use the typed
767
- wrapper**, not the enhancer. For an English static page, render the footer/bulk nodes yourself
768
- from the `fdy-table-change` event instead of using `data-fdy-table-info` /
769
- `data-fdy-table-bulk-count`; the filter button's `aria-label` has no such escape today.
767
+ wrapper**.
768
+ - **Overriding an enhancer's strings.** Each enhancer keeps its strings in one `TEXT` table and
769
+ reads them through `textOf()`, so any of them can be replaced per element with
770
+ **`data-fdy-text-<key>`** — no forking, and no rendering the nodes yourself:
771
+
772
+ ```html
773
+ <div data-fdy-table
774
+ data-fdy-text-info="Showing {from}–{to} of {total}"
775
+ data-fdy-text-selected="{n} selected"
776
+ data-fdy-text-filter="Column filter">…</div>
777
+ ```
778
+
779
+ `{n}`, `{from}`, `{to}`, `{total}`, `{name}`, `{max}` and `{label}` are substituted where the
780
+ string supports them. Validation messages take the same shape on the `<form>`
781
+ (`data-fdy-text-required`, `data-fdy-text-type`, …), narrower than the per-field
782
+ `data-fdy-msg-<alias>` that still wins. `Freeday.toast()` takes `closeLabel` in its options
783
+ object. `npm test` asserts no enhancer string is written outside its `TEXT` table, so a new one
784
+ arrives overridable or not at all.
770
785
 
771
786
  **Testing note:** a column's filter button and the dialog it opens deliberately share one
772
787
  accessible name (`Filter <column>`) — a dialog named after its trigger is the normal pattern. In a
@@ -9,7 +9,9 @@
9
9
  data-label="@Label"
10
10
  data-value="@Value"
11
11
  data-placeholder="@Placeholder"
12
- data-separator="@Separator">
12
+ data-separator="@Separator"
13
+ data-fdy-text-back="@BackLabel"
14
+ data-fdy-text-submenu="@SubmenuLabel">
13
15
  <ul>
14
16
  @foreach (FdyCascadeNode node in Nodes)
15
17
  {
@@ -12,12 +12,20 @@ public partial class FdyCascade
12
12
  [Parameter] public string? Value { get; set; }
13
13
  [Parameter] public EventCallback<string?> ValueChanged { get; set; }
14
14
 
15
- [Parameter] public string? Label { get; set; }
16
- [Parameter] public string? Placeholder { get; set; }
15
+ /// <summary>Trigger label. English by default, matching Vue and React.</summary>
16
+ [Parameter] public string Label { get; set; } = "Select";
17
+ /// <summary>Empty-value placeholder. English by default, matching Vue and React.</summary>
18
+ [Parameter] public string Placeholder { get; set; } = "Select…";
17
19
 
18
20
  /// <summary>Separator between labels in the displayed path (default " / ").</summary>
19
21
  [Parameter] public string Separator { get; set; } = " / ";
20
22
 
23
+ /// <summary>aria-label for the button that goes up one level. Matches Vue and React.</summary>
24
+ [Parameter] public string BackLabel { get; set; } = "Back one level";
25
+
26
+ /// <summary>aria-label for a branch row. <c>{label}</c> is replaced with the node label.</summary>
27
+ [Parameter] public string SubmenuLabel { get; set; } = "{label}, submenu";
28
+
21
29
  protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayCascade.init", Root);
22
30
 
23
31
  protected override async ValueTask OnHydratedAsync()
@@ -99,6 +99,11 @@
99
99
 
100
100
  // Passthrough to the Freeday toast API.
101
101
  function toast(options) {
102
+ /* The enhancer's own default is Indonesian — deliberate for the raw path, wrong for a Blazor
103
+ * app, which COMPONENTS.md promises is English throughout. Defaulted here so the promise is
104
+ * kept without changing what a hand-written page gets. */
105
+ options = options || {};
106
+ if (options.closeLabel == null || options.closeLabel === '') options.closeLabel = 'Close';
102
107
  if (window.Freeday && typeof window.Freeday.toast === 'function') window.Freeday.toast(options);
103
108
  }
104
109
 
@@ -17,6 +17,22 @@
17
17
  (function () {
18
18
  'use strict';
19
19
 
20
+
21
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
22
+ * path — and every one overridable per element, so a host that speaks another language (the
23
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
24
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
25
+ var TEXT = {
26
+ position: '{n} dari {total}',
27
+ slide: 'Slide {n}'
28
+ };
29
+ function textOf(root, key, vars) {
30
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
31
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
32
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
33
+ return s;
34
+ }
35
+
20
36
  function initCarousel(root) {
21
37
  if (root.dataset.fdyCarouselReady === '1') return;
22
38
  root.dataset.fdyCarouselReady = '1';
@@ -30,7 +46,7 @@
30
46
  var index = 0;
31
47
 
32
48
  slides.forEach(function (s, i) {
33
- if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', (i + 1) + ' dari ' + slides.length);
49
+ if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', textOf(root, 'position', { n: i + 1, total: slides.length }));
34
50
  });
35
51
 
36
52
  var dots = [];
@@ -39,7 +55,7 @@
39
55
  var b = document.createElement('button');
40
56
  b.type = 'button';
41
57
  b.className = 'fdy-carousel__dot';
42
- b.setAttribute('aria-label', 'Slide ' + (i + 1));
58
+ b.setAttribute('aria-label', textOf(root, 'slide', { n: i + 1 }));
43
59
  b.addEventListener('click', function () { goTo(i); });
44
60
  dotsWrap.appendChild(b);
45
61
  dots.push(b);
@@ -53,6 +53,22 @@
53
53
  return null;
54
54
  }
55
55
 
56
+
57
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
58
+ * path — and every one overridable per element, so a host that speaks another language (the
59
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
60
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
61
+ var TEXT = {
62
+ back: 'Kembali satu tingkat',
63
+ submenu: '{label}, submenu'
64
+ };
65
+ function textOf(root, key, vars) {
66
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
67
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
68
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
69
+ return s;
70
+ }
71
+
56
72
  function initCascade(wrap) {
57
73
  if (wrap.dataset.fdyCascadeReady === '1') return;
58
74
  wrap.dataset.fdyCascadeReady = '1';
@@ -84,7 +100,7 @@
84
100
  var back = document.createElement('button');
85
101
  back.type = 'button';
86
102
  back.className = 'fdy-cascade__back';
87
- back.setAttribute('aria-label', 'Kembali satu tingkat');
103
+ back.setAttribute('aria-label', textOf(wrap, 'back'));
88
104
  back.innerHTML = BACK;
89
105
  back.hidden = true;
90
106
  var crumb = document.createElement('span');
@@ -124,7 +140,7 @@
124
140
  li.setAttribute('data-index', String(i));
125
141
  var isBranch = !!node.children;
126
142
  li.setAttribute('aria-selected', (!isBranch && node.value === selectedValue) ? 'true' : 'false');
127
- if (isBranch) li.setAttribute('aria-label', node.label + ', submenu');
143
+ if (isBranch) li.setAttribute('aria-label', textOf(wrap, 'submenu', { label: node.label }));
128
144
  var lbl = document.createElement('span');
129
145
  lbl.className = 'fdy-cascade__opt-label';
130
146
  lbl.textContent = node.label;
@@ -33,6 +33,21 @@
33
33
  .toLowerCase();
34
34
  }
35
35
 
36
+
37
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
38
+ * path — and every one overridable per element, so a host that speaks another language (the
39
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
40
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
41
+ var TEXT = {
42
+ selected: '{n} dipilih'
43
+ };
44
+ function textOf(root, key, vars) {
45
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
46
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
47
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
48
+ return s;
49
+ }
50
+
36
51
  function initDialog(dialog) {
37
52
  if (dialog.dataset.fdyCflReady === '1') return;
38
53
  dialog.dataset.fdyCflReady = '1';
@@ -70,7 +85,7 @@
70
85
  function updateCount() {
71
86
  if (!countEl) return;
72
87
  var n = rows.filter(function (r) { return r.getAttribute('aria-selected') === 'true'; }).length;
73
- countEl.textContent = n + ' dipilih';
88
+ countEl.textContent = textOf(dialog, 'selected', { n: n });
74
89
  }
75
90
 
76
91
  function fillGroup(detail) {
@@ -38,7 +38,12 @@
38
38
  badInput: 'type',
39
39
  customError: 'mismatch'
40
40
  };
41
- var DEFAULTS = {
41
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
42
+ * path — and overridable at three levels, narrowest first: `data-fdy-msg-<alias>` on the field,
43
+ * `data-fdy-msg` on the field, then `data-fdy-text-<alias>` on the FORM. The form level is what
44
+ * a host in another language needs: it sets nine messages once instead of on every input.
45
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
46
+ var TEXT = {
42
47
  required: 'Wajib diisi.',
43
48
  type: 'Format tidak valid.',
44
49
  pattern: 'Format tidak sesuai.',
@@ -47,8 +52,13 @@
47
52
  min: 'Nilai terlalu kecil.',
48
53
  max: 'Nilai terlalu besar.',
49
54
  step: 'Nilai tidak sesuai kelipatan.',
50
- mismatch: 'Nilai tidak cocok.'
55
+ mismatch: 'Nilai tidak cocok.',
56
+ invalid: 'Tidak valid.'
51
57
  };
58
+ function textOf(root, key) {
59
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
60
+ return custom != null && custom !== '' ? custom : TEXT[key];
61
+ }
52
62
  var VALIDITY_KEYS = ['valueMissing', 'typeMismatch', 'patternMismatch', 'tooShort', 'tooLong', 'rangeUnderflow', 'rangeOverflow', 'stepMismatch', 'badInput'];
53
63
 
54
64
  function isCandidate(el) {
@@ -82,7 +92,7 @@
82
92
  if (matchSel) {
83
93
  var other = document.querySelector(matchSel);
84
94
  if (other && el.value !== other.value) {
85
- el.setCustomValidity(el.getAttribute('data-fdy-msg-mismatch') || el.getAttribute('data-fdy-msg') || DEFAULTS.mismatch);
95
+ el.setCustomValidity(el.getAttribute('data-fdy-msg-mismatch') || el.getAttribute('data-fdy-msg') || textOf(el.form || el.closest('form'), 'mismatch'));
86
96
  }
87
97
  }
88
98
  if (el.validity.valid) return '';
@@ -93,7 +103,8 @@
93
103
  }
94
104
  }
95
105
  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.';
106
+ var form = el.form || el.closest('form');
107
+ return custom || el.getAttribute('data-fdy-msg') || (alias && textOf(form, alias)) || el.validationMessage || textOf(form, 'invalid');
97
108
  }
98
109
 
99
110
  function paint(el, message) {
@@ -50,6 +50,22 @@
50
50
  return out;
51
51
  }
52
52
 
53
+
54
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
55
+ * path — and every one overridable per element, so a host that speaks another language (the
56
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
57
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
58
+ var TEXT = {
59
+ show: 'Tampilkan kata sandi',
60
+ hide: 'Sembunyikan kata sandi'
61
+ };
62
+ function textOf(root, key, vars) {
63
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
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
+ }
68
+
53
69
  function initMask(el) {
54
70
  if (el.dataset.fdyMaskReady === '1') return;
55
71
  el.dataset.fdyMaskReady = '1';
@@ -85,13 +101,13 @@
85
101
  btn.type = 'button';
86
102
  btn.className = 'fdy-input-group__btn';
87
103
  btn.setAttribute('aria-pressed', 'false');
88
- btn.setAttribute('aria-label', 'Tampilkan kata sandi');
104
+ btn.setAttribute('aria-label', textOf(input, 'show'));
89
105
  btn.innerHTML = EYE;
90
106
  btn.addEventListener('click', function () {
91
107
  var reveal = input.type === 'password';
92
108
  input.type = reveal ? 'text' : 'password';
93
109
  btn.setAttribute('aria-pressed', reveal ? 'true' : 'false');
94
- btn.setAttribute('aria-label', reveal ? 'Sembunyikan kata sandi' : 'Tampilkan kata sandi');
110
+ btn.setAttribute('aria-label', reveal ? textOf(input, 'hide') : textOf(input, 'show'));
95
111
  btn.innerHTML = reveal ? EYE_OFF : EYE;
96
112
  input.focus();
97
113
  });
@@ -18,6 +18,22 @@
18
18
 
19
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
20
 
21
+
22
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
23
+ * path — and every one overridable per element, so a host that speaks another language (the
24
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
25
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
26
+ var TEXT = {
27
+ done: 'Selesai',
28
+ next: 'Lanjut'
29
+ };
30
+ function textOf(root, key, vars) {
31
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
32
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
33
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
34
+ return s;
35
+ }
36
+
21
37
  function initStepper(root) {
22
38
  if (root.dataset.fdyStepperReady === '1') return;
23
39
  root.dataset.fdyStepperReady = '1';
@@ -52,7 +68,7 @@
52
68
  });
53
69
  panels.forEach(function (p, i) { p.hidden = i !== active; });
54
70
  if (prevBtn) prevBtn.disabled = active === 0;
55
- if (nextBtn) nextBtn.textContent = active === steps.length - 1 ? 'Selesai' : (nextLabel || 'Lanjut');
71
+ if (nextBtn) nextBtn.textContent = active === steps.length - 1 ? textOf(root, 'done') : (nextLabel || textOf(root, 'next'));
56
72
  root.dispatchEvent(new CustomEvent('fdy-step-change', { bubbles: true, detail: { index: active } }));
57
73
  }
58
74
 
@@ -33,6 +33,32 @@
33
33
  : cell.textContent).trim();
34
34
  }
35
35
 
36
+
37
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
38
+ * path — and every one overridable per element, so a host that speaks another language (the
39
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
40
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
41
+ var TEXT = {
42
+ prev: 'Sebelumnya',
43
+ next: 'Berikutnya',
44
+ selected: '{n} dipilih',
45
+ filter: 'Filter kolom',
46
+ filterText: 'Berisi teks',
47
+ filterTextPlaceholder: 'Berisi…',
48
+ filterEnum: 'Tampilkan nilai',
49
+ filterRange: 'Rentang nilai',
50
+ reset: 'Reset',
51
+ close: 'Tutup',
52
+ rows: '{n} baris',
53
+ info: 'Menampilkan {from}–{to} dari {total}'
54
+ };
55
+ function textOf(root, key, vars) {
56
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
57
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
58
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
59
+ return s;
60
+ }
61
+
36
62
  function initTable(root) {
37
63
  if (root.dataset.fdyTableReady === '1') return;
38
64
  var table = root.querySelector('table.fdy-table');
@@ -157,7 +183,7 @@
157
183
  ul.appendChild(li);
158
184
  }
159
185
 
160
- addBtn('‹', page - 1, { disabled: page === 1, label: 'Sebelumnya' });
186
+ addBtn('‹', page - 1, { disabled: page === 1, label: textOf(root, 'prev') });
161
187
  var pages = [];
162
188
  for (var p = 1; p <= totalPages; p++) {
163
189
  if (p === 1 || p === totalPages || (p >= page - 1 && p <= page + 1)) pages.push(p);
@@ -167,7 +193,7 @@
167
193
  if (p === '…') addEllipsis();
168
194
  else addBtn(String(p), p, { current: p === page });
169
195
  });
170
- addBtn('›', page + 1, { disabled: page === totalPages, label: 'Berikutnya' });
196
+ addBtn('›', page + 1, { disabled: page === totalPages, label: textOf(root, 'next') });
171
197
  pagerEl.appendChild(ul);
172
198
  }
173
199
 
@@ -182,7 +208,7 @@
182
208
  if (!bulkBar) return;
183
209
  var n = selectedRows().length;
184
210
  bulkBar.hidden = n === 0;
185
- if (bulkCount) bulkCount.textContent = n + ' dipilih';
211
+ if (bulkCount) bulkCount.textContent = textOf(root, 'selected', { n: n });
186
212
  }
187
213
  function clearSelection() {
188
214
  allRows.forEach(function (r) {
@@ -238,15 +264,15 @@
238
264
  var pop = document.createElement('div');
239
265
  pop.className = 'fdy-filter';
240
266
  pop.setAttribute('role', 'dialog');
241
- pop.setAttribute('aria-label', 'Filter kolom');
267
+ pop.setAttribute('aria-label', textOf(root, 'filter'));
242
268
  var f = colFilters[idx] || { type: type };
243
269
 
244
270
  if (type === 'text') {
245
- pop.appendChild(filterTitle('Berisi teks'));
271
+ pop.appendChild(filterTitle(textOf(root, 'filterText')));
246
272
  var inp = document.createElement('input');
247
273
  inp.className = 'fdy-input';
248
274
  inp.type = 'search';
249
- inp.placeholder = 'Berisi…';
275
+ inp.placeholder = textOf(root, 'filterTextPlaceholder');
250
276
  inp.value = f.text || '';
251
277
  inp.addEventListener('input', function () {
252
278
  f.text = inp.value.trim();
@@ -256,7 +282,7 @@
256
282
  });
257
283
  pop.appendChild(inp);
258
284
  } else if (type === 'enum') {
259
- pop.appendChild(filterTitle('Tampilkan nilai'));
285
+ pop.appendChild(filterTitle(textOf(root, 'filterEnum')));
260
286
  var list = document.createElement('div');
261
287
  list.className = 'fdy-filter__list';
262
288
  var set = f.set || new Set();
@@ -279,7 +305,7 @@
279
305
  });
280
306
  pop.appendChild(list);
281
307
  } else if (type === 'number') {
282
- pop.appendChild(filterTitle('Rentang nilai'));
308
+ pop.appendChild(filterTitle(textOf(root, 'filterRange')));
283
309
  var range = document.createElement('div');
284
310
  range.className = 'fdy-filter__range';
285
311
  var minI = numberInput('Min', f.min);
@@ -304,7 +330,7 @@
304
330
  var reset = document.createElement('button');
305
331
  reset.type = 'button';
306
332
  reset.className = 'fdy-btn fdy-btn--ghost fdy-btn--sm';
307
- reset.textContent = 'Reset';
333
+ reset.textContent = textOf(root, 'reset');
308
334
  reset.addEventListener('click', function () {
309
335
  delete colFilters[idx];
310
336
  markActive(btn, false);
@@ -314,7 +340,7 @@
314
340
  var done = document.createElement('button');
315
341
  done.type = 'button';
316
342
  done.className = 'fdy-btn fdy-btn--sm';
317
- done.textContent = 'Tutup';
343
+ done.textContent = textOf(root, 'close');
318
344
  done.addEventListener('click', function () { closePopover(true); });
319
345
  foot.appendChild(reset); foot.appendChild(done);
320
346
  pop.appendChild(foot);
@@ -339,7 +365,7 @@
339
365
  btn.className = 'fdy-table__filterbtn';
340
366
  btn.setAttribute('aria-haspopup', 'dialog');
341
367
  btn.setAttribute('aria-pressed', 'false');
342
- btn.setAttribute('aria-label', 'Filter kolom');
368
+ btn.setAttribute('aria-label', textOf(root, 'filter'));
343
369
  btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18l-7 8v5l-4 2v-7z"></path></svg>';
344
370
  btn.addEventListener('click', function (e) {
345
371
  e.stopPropagation();
@@ -370,8 +396,8 @@
370
396
 
371
397
  var shownFrom = total === 0 ? 0 : start + 1;
372
398
  var shownTo = start + slice.length;
373
- if (countEl) countEl.textContent = total + ' baris';
374
- if (infoEl) infoEl.textContent = 'Menampilkan ' + shownFrom + '–' + shownTo + ' dari ' + total;
399
+ if (countEl) countEl.textContent = textOf(root, 'rows', { n: total });
400
+ if (infoEl) infoEl.textContent = textOf(root, 'info', { from: shownFrom, to: shownTo, total: total });
375
401
  buildPager(totalPages);
376
402
  syncSelectAll(slice);
377
403
  updateBulk();
@@ -48,6 +48,21 @@
48
48
  setTimeout(finish, 400); // fallback if transitionend never fires
49
49
  }
50
50
 
51
+
52
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
53
+ * path — and every one overridable per element, so a host that speaks another language (the
54
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
55
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
56
+ var TEXT = {
57
+ close: 'Tutup'
58
+ };
59
+ function textOf(root, key, vars) {
60
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
61
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
62
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
63
+ return s;
64
+ }
65
+
51
66
  function el(tag, className, text) {
52
67
  var node = document.createElement(tag);
53
68
  if (className) node.className = className;
@@ -69,7 +84,7 @@
69
84
 
70
85
  var close = el('button', 'fdy-toast__close');
71
86
  close.type = 'button';
72
- close.setAttribute('aria-label', 'Tutup');
87
+ close.setAttribute('aria-label', opts.closeLabel || TEXT.close);
73
88
  close.innerHTML = '&times;';
74
89
  close.addEventListener('click', function () { dismiss(node); });
75
90
 
@@ -50,6 +50,27 @@
50
50
  });
51
51
  }
52
52
 
53
+
54
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
55
+ * path — and every one overridable per element, so a host that speaks another language (the
56
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
57
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
58
+ var TEXT = {
59
+ remove: 'Hapus {name}',
60
+ progress: 'Progres unggah {name}',
61
+ uploading: 'Mengunggah…',
62
+ waiting: 'Menunggu server…',
63
+ done: 'Terunggah',
64
+ badType: 'Tipe berkas tidak didukung.',
65
+ tooBig: 'Ukuran melebihi batas ({max}).'
66
+ };
67
+ function textOf(root, key, vars) {
68
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
69
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
70
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
71
+ return s;
72
+ }
73
+
53
74
  function makeRow(file, zone) {
54
75
  var el = document.createElement('div');
55
76
  el.className = 'fdy-file';
@@ -69,7 +90,7 @@
69
90
  var remove = document.createElement('button');
70
91
  remove.type = 'button';
71
92
  remove.className = 'fdy-file__remove';
72
- remove.setAttribute('aria-label', 'Hapus ' + file.name);
93
+ remove.setAttribute('aria-label', textOf(zone, 'remove', { name: file.name }));
73
94
  remove.innerHTML = '&times;';
74
95
  remove.addEventListener('click', function () {
75
96
  /* Dispatched on the ZONE, not on the row — the same target as fdy-upload-add, so one listener
@@ -96,7 +117,7 @@
96
117
  progressEl.setAttribute('role', 'progressbar');
97
118
  progressEl.setAttribute('aria-valuemin', '0');
98
119
  progressEl.setAttribute('aria-valuemax', '100');
99
- progressEl.setAttribute('aria-label', 'Progres unggah ' + file.name);
120
+ progressEl.setAttribute('aria-label', textOf(zone, 'progress', { name: file.name }));
100
121
  bar = document.createElement('div');
101
122
  bar.className = 'fdy-progress__bar';
102
123
  bar.style.width = '0%';
@@ -129,7 +150,7 @@
129
150
  uploading: function () {
130
151
  el.classList.remove('fdy-file--error', 'fdy-file--success');
131
152
  icon.innerHTML = FILE_ICON;
132
- sub.textContent = fmtSize(file.size) + ' · Mengunggah…';
153
+ sub.textContent = fmtSize(file.size) + ' · ' + textOf(zone, 'uploading');
133
154
  ensureProgress();
134
155
  determinate();
135
156
  },
@@ -149,7 +170,7 @@
149
170
  waiting: function (label) {
150
171
  el.classList.remove('fdy-file--error', 'fdy-file--success');
151
172
  icon.innerHTML = FILE_ICON;
152
- sub.textContent = fmtSize(file.size) + ' · ' + (label || 'Menunggu server…');
173
+ sub.textContent = fmtSize(file.size) + ' · ' + (label || textOf(zone, 'waiting'));
153
174
  ensureProgress();
154
175
  progressEl.classList.add('fdy-progress--indeterminate');
155
176
  /* The modifier styles .fdy-progress__bar, so it must sit on the CONTAINER, and the inline
@@ -163,7 +184,7 @@
163
184
  el.classList.add('fdy-file--success');
164
185
  el.classList.remove('fdy-file--error');
165
186
  icon.innerHTML = OK_ICON;
166
- sub.textContent = fmtSize(file.size) + ' · Terunggah';
187
+ sub.textContent = fmtSize(file.size) + ' · ' + textOf(zone, 'done');
167
188
  dropProgress();
168
189
  },
169
190
  fail: function (msg) {
@@ -231,8 +252,8 @@
231
252
  if (!fileList) return;
232
253
  Array.prototype.slice.call(fileList).forEach(function (file) {
233
254
  var reason = null;
234
- if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
235
- else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
255
+ if (!accepts(file, acceptAttr)) reason = textOf(zone, 'badType');
256
+ else if (maxSize && file.size > maxSize) reason = textOf(zone, 'tooBig', { max: fmtSize(maxSize) });
236
257
  var row = makeRow(file, zone);
237
258
  if (list) list.appendChild(row.el);
238
259
  if (reason) {
@@ -1112,6 +1112,12 @@ a { color: var(--color-primary); }
1112
1112
  /* Freeday — Date picker (input-styled trigger + calendar popover).
1113
1113
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
1114
1114
  .fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
1115
+ /* Inside a labelled field, a picker fills the field like every other control.
1116
+ The 14rem/11rem caps above suit a picker standing on its own; in a form grid
1117
+ they leave the date box visibly short of the combo beside it — 224px against
1118
+ 319px in a two-column layout — and the row reads as ragged. The field's own
1119
+ max-width is what should govern there. */
1120
+ .fdy-field>.fdy-datepicker,.fdy-field>.fdy-timepicker,.fdy-field>.fdy-daterange{max-width:none;}
1115
1121
  .fdy-datepicker__trigger{display:inline-flex;align-items:center;gap:var(--space-2);width:100%;height:var(--control-h);padding:0 var(--space-3);text-align:left;cursor:pointer;font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
1116
1122
  .fdy-datepicker__trigger:hover{border-color:var(--color-text-muted);}
1117
1123
  .fdy-datepicker__trigger:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
package/dist/freeday.css CHANGED
@@ -729,6 +729,12 @@ a { color: var(--color-primary); }
729
729
  /* Freeday — Date picker (input-styled trigger + calendar popover).
730
730
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
731
731
  .fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
732
+ /* Inside a labelled field, a picker fills the field like every other control.
733
+ The 14rem/11rem caps above suit a picker standing on its own; in a form grid
734
+ they leave the date box visibly short of the combo beside it — 224px against
735
+ 319px in a two-column layout — and the row reads as ragged. The field's own
736
+ max-width is what should govern there. */
737
+ .fdy-field>.fdy-datepicker,.fdy-field>.fdy-timepicker,.fdy-field>.fdy-daterange{max-width:none;}
732
738
  .fdy-datepicker__trigger{display:inline-flex;align-items:center;gap:var(--space-2);width:100%;height:var(--control-h);padding:0 var(--space-3);text-align:left;cursor:pointer;font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
733
739
  .fdy-datepicker__trigger:hover{border-color:var(--color-text-muted);}
734
740
  .fdy-datepicker__trigger:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
package/dist/freeday.js CHANGED
@@ -210,6 +210,22 @@
210
210
  (function () {
211
211
  'use strict';
212
212
 
213
+
214
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
215
+ * path — and every one overridable per element, so a host that speaks another language (the
216
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
217
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
218
+ var TEXT = {
219
+ position: '{n} dari {total}',
220
+ slide: 'Slide {n}'
221
+ };
222
+ function textOf(root, key, vars) {
223
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
224
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
225
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
226
+ return s;
227
+ }
228
+
213
229
  function initCarousel(root) {
214
230
  if (root.dataset.fdyCarouselReady === '1') return;
215
231
  root.dataset.fdyCarouselReady = '1';
@@ -223,7 +239,7 @@
223
239
  var index = 0;
224
240
 
225
241
  slides.forEach(function (s, i) {
226
- if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', (i + 1) + ' dari ' + slides.length);
242
+ if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', textOf(root, 'position', { n: i + 1, total: slides.length }));
227
243
  });
228
244
 
229
245
  var dots = [];
@@ -232,7 +248,7 @@
232
248
  var b = document.createElement('button');
233
249
  b.type = 'button';
234
250
  b.className = 'fdy-carousel__dot';
235
- b.setAttribute('aria-label', 'Slide ' + (i + 1));
251
+ b.setAttribute('aria-label', textOf(root, 'slide', { n: i + 1 }));
236
252
  b.addEventListener('click', function () { goTo(i); });
237
253
  dotsWrap.appendChild(b);
238
254
  dots.push(b);
@@ -361,6 +377,22 @@
361
377
  return null;
362
378
  }
363
379
 
380
+
381
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
382
+ * path — and every one overridable per element, so a host that speaks another language (the
383
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
384
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
385
+ var TEXT = {
386
+ back: 'Kembali satu tingkat',
387
+ submenu: '{label}, submenu'
388
+ };
389
+ function textOf(root, key, vars) {
390
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
391
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
392
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
393
+ return s;
394
+ }
395
+
364
396
  function initCascade(wrap) {
365
397
  if (wrap.dataset.fdyCascadeReady === '1') return;
366
398
  wrap.dataset.fdyCascadeReady = '1';
@@ -392,7 +424,7 @@
392
424
  var back = document.createElement('button');
393
425
  back.type = 'button';
394
426
  back.className = 'fdy-cascade__back';
395
- back.setAttribute('aria-label', 'Kembali satu tingkat');
427
+ back.setAttribute('aria-label', textOf(wrap, 'back'));
396
428
  back.innerHTML = BACK;
397
429
  back.hidden = true;
398
430
  var crumb = document.createElement('span');
@@ -432,7 +464,7 @@
432
464
  li.setAttribute('data-index', String(i));
433
465
  var isBranch = !!node.children;
434
466
  li.setAttribute('aria-selected', (!isBranch && node.value === selectedValue) ? 'true' : 'false');
435
- if (isBranch) li.setAttribute('aria-label', node.label + ', submenu');
467
+ if (isBranch) li.setAttribute('aria-label', textOf(wrap, 'submenu', { label: node.label }));
436
468
  var lbl = document.createElement('span');
437
469
  lbl.className = 'fdy-cascade__opt-label';
438
470
  lbl.textContent = node.label;
@@ -601,6 +633,21 @@
601
633
  .toLowerCase();
602
634
  }
603
635
 
636
+
637
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
638
+ * path — and every one overridable per element, so a host that speaks another language (the
639
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
640
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
641
+ var TEXT = {
642
+ selected: '{n} dipilih'
643
+ };
644
+ function textOf(root, key, vars) {
645
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
646
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
647
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
648
+ return s;
649
+ }
650
+
604
651
  function initDialog(dialog) {
605
652
  if (dialog.dataset.fdyCflReady === '1') return;
606
653
  dialog.dataset.fdyCflReady = '1';
@@ -638,7 +685,7 @@
638
685
  function updateCount() {
639
686
  if (!countEl) return;
640
687
  var n = rows.filter(function (r) { return r.getAttribute('aria-selected') === 'true'; }).length;
641
- countEl.textContent = n + ' dipilih';
688
+ countEl.textContent = textOf(dialog, 'selected', { n: n });
642
689
  }
643
690
 
644
691
  function fillGroup(detail) {
@@ -2030,7 +2077,12 @@
2030
2077
  badInput: 'type',
2031
2078
  customError: 'mismatch'
2032
2079
  };
2033
- var DEFAULTS = {
2080
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
2081
+ * path — and overridable at three levels, narrowest first: `data-fdy-msg-<alias>` on the field,
2082
+ * `data-fdy-msg` on the field, then `data-fdy-text-<alias>` on the FORM. The form level is what
2083
+ * a host in another language needs: it sets nine messages once instead of on every input.
2084
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
2085
+ var TEXT = {
2034
2086
  required: 'Wajib diisi.',
2035
2087
  type: 'Format tidak valid.',
2036
2088
  pattern: 'Format tidak sesuai.',
@@ -2039,8 +2091,13 @@
2039
2091
  min: 'Nilai terlalu kecil.',
2040
2092
  max: 'Nilai terlalu besar.',
2041
2093
  step: 'Nilai tidak sesuai kelipatan.',
2042
- mismatch: 'Nilai tidak cocok.'
2094
+ mismatch: 'Nilai tidak cocok.',
2095
+ invalid: 'Tidak valid.'
2043
2096
  };
2097
+ function textOf(root, key) {
2098
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
2099
+ return custom != null && custom !== '' ? custom : TEXT[key];
2100
+ }
2044
2101
  var VALIDITY_KEYS = ['valueMissing', 'typeMismatch', 'patternMismatch', 'tooShort', 'tooLong', 'rangeUnderflow', 'rangeOverflow', 'stepMismatch', 'badInput'];
2045
2102
 
2046
2103
  function isCandidate(el) {
@@ -2074,7 +2131,7 @@
2074
2131
  if (matchSel) {
2075
2132
  var other = document.querySelector(matchSel);
2076
2133
  if (other && el.value !== other.value) {
2077
- el.setCustomValidity(el.getAttribute('data-fdy-msg-mismatch') || el.getAttribute('data-fdy-msg') || DEFAULTS.mismatch);
2134
+ el.setCustomValidity(el.getAttribute('data-fdy-msg-mismatch') || el.getAttribute('data-fdy-msg') || textOf(el.form || el.closest('form'), 'mismatch'));
2078
2135
  }
2079
2136
  }
2080
2137
  if (el.validity.valid) return '';
@@ -2085,7 +2142,8 @@
2085
2142
  }
2086
2143
  }
2087
2144
  var custom = alias ? el.getAttribute('data-fdy-msg-' + alias) : null;
2088
- return custom || el.getAttribute('data-fdy-msg') || (alias && DEFAULTS[alias]) || el.validationMessage || 'Tidak valid.';
2145
+ var form = el.form || el.closest('form');
2146
+ return custom || el.getAttribute('data-fdy-msg') || (alias && textOf(form, alias)) || el.validationMessage || textOf(form, 'invalid');
2089
2147
  }
2090
2148
 
2091
2149
  function paint(el, message) {
@@ -2227,6 +2285,22 @@
2227
2285
  return out;
2228
2286
  }
2229
2287
 
2288
+
2289
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
2290
+ * path — and every one overridable per element, so a host that speaks another language (the
2291
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
2292
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
2293
+ var TEXT = {
2294
+ show: 'Tampilkan kata sandi',
2295
+ hide: 'Sembunyikan kata sandi'
2296
+ };
2297
+ function textOf(root, key, vars) {
2298
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
2299
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
2300
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
2301
+ return s;
2302
+ }
2303
+
2230
2304
  function initMask(el) {
2231
2305
  if (el.dataset.fdyMaskReady === '1') return;
2232
2306
  el.dataset.fdyMaskReady = '1';
@@ -2262,13 +2336,13 @@
2262
2336
  btn.type = 'button';
2263
2337
  btn.className = 'fdy-input-group__btn';
2264
2338
  btn.setAttribute('aria-pressed', 'false');
2265
- btn.setAttribute('aria-label', 'Tampilkan kata sandi');
2339
+ btn.setAttribute('aria-label', textOf(input, 'show'));
2266
2340
  btn.innerHTML = EYE;
2267
2341
  btn.addEventListener('click', function () {
2268
2342
  var reveal = input.type === 'password';
2269
2343
  input.type = reveal ? 'text' : 'password';
2270
2344
  btn.setAttribute('aria-pressed', reveal ? 'true' : 'false');
2271
- btn.setAttribute('aria-label', reveal ? 'Sembunyikan kata sandi' : 'Tampilkan kata sandi');
2345
+ btn.setAttribute('aria-label', reveal ? textOf(input, 'hide') : textOf(input, 'show'));
2272
2346
  btn.innerHTML = reveal ? EYE_OFF : EYE;
2273
2347
  input.focus();
2274
2348
  });
@@ -2892,6 +2966,22 @@
2892
2966
 
2893
2967
  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>';
2894
2968
 
2969
+
2970
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
2971
+ * path — and every one overridable per element, so a host that speaks another language (the
2972
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
2973
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
2974
+ var TEXT = {
2975
+ done: 'Selesai',
2976
+ next: 'Lanjut'
2977
+ };
2978
+ function textOf(root, key, vars) {
2979
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
2980
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
2981
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
2982
+ return s;
2983
+ }
2984
+
2895
2985
  function initStepper(root) {
2896
2986
  if (root.dataset.fdyStepperReady === '1') return;
2897
2987
  root.dataset.fdyStepperReady = '1';
@@ -2926,7 +3016,7 @@
2926
3016
  });
2927
3017
  panels.forEach(function (p, i) { p.hidden = i !== active; });
2928
3018
  if (prevBtn) prevBtn.disabled = active === 0;
2929
- if (nextBtn) nextBtn.textContent = active === steps.length - 1 ? 'Selesai' : (nextLabel || 'Lanjut');
3019
+ if (nextBtn) nextBtn.textContent = active === steps.length - 1 ? textOf(root, 'done') : (nextLabel || textOf(root, 'next'));
2930
3020
  root.dispatchEvent(new CustomEvent('fdy-step-change', { bubbles: true, detail: { index: active } }));
2931
3021
  }
2932
3022
 
@@ -3001,6 +3091,32 @@
3001
3091
  : cell.textContent).trim();
3002
3092
  }
3003
3093
 
3094
+
3095
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
3096
+ * path — and every one overridable per element, so a host that speaks another language (the
3097
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
3098
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
3099
+ var TEXT = {
3100
+ prev: 'Sebelumnya',
3101
+ next: 'Berikutnya',
3102
+ selected: '{n} dipilih',
3103
+ filter: 'Filter kolom',
3104
+ filterText: 'Berisi teks',
3105
+ filterTextPlaceholder: 'Berisi…',
3106
+ filterEnum: 'Tampilkan nilai',
3107
+ filterRange: 'Rentang nilai',
3108
+ reset: 'Reset',
3109
+ close: 'Tutup',
3110
+ rows: '{n} baris',
3111
+ info: 'Menampilkan {from}–{to} dari {total}'
3112
+ };
3113
+ function textOf(root, key, vars) {
3114
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
3115
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
3116
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
3117
+ return s;
3118
+ }
3119
+
3004
3120
  function initTable(root) {
3005
3121
  if (root.dataset.fdyTableReady === '1') return;
3006
3122
  var table = root.querySelector('table.fdy-table');
@@ -3125,7 +3241,7 @@
3125
3241
  ul.appendChild(li);
3126
3242
  }
3127
3243
 
3128
- addBtn('‹', page - 1, { disabled: page === 1, label: 'Sebelumnya' });
3244
+ addBtn('‹', page - 1, { disabled: page === 1, label: textOf(root, 'prev') });
3129
3245
  var pages = [];
3130
3246
  for (var p = 1; p <= totalPages; p++) {
3131
3247
  if (p === 1 || p === totalPages || (p >= page - 1 && p <= page + 1)) pages.push(p);
@@ -3135,7 +3251,7 @@
3135
3251
  if (p === '…') addEllipsis();
3136
3252
  else addBtn(String(p), p, { current: p === page });
3137
3253
  });
3138
- addBtn('›', page + 1, { disabled: page === totalPages, label: 'Berikutnya' });
3254
+ addBtn('›', page + 1, { disabled: page === totalPages, label: textOf(root, 'next') });
3139
3255
  pagerEl.appendChild(ul);
3140
3256
  }
3141
3257
 
@@ -3150,7 +3266,7 @@
3150
3266
  if (!bulkBar) return;
3151
3267
  var n = selectedRows().length;
3152
3268
  bulkBar.hidden = n === 0;
3153
- if (bulkCount) bulkCount.textContent = n + ' dipilih';
3269
+ if (bulkCount) bulkCount.textContent = textOf(root, 'selected', { n: n });
3154
3270
  }
3155
3271
  function clearSelection() {
3156
3272
  allRows.forEach(function (r) {
@@ -3206,15 +3322,15 @@
3206
3322
  var pop = document.createElement('div');
3207
3323
  pop.className = 'fdy-filter';
3208
3324
  pop.setAttribute('role', 'dialog');
3209
- pop.setAttribute('aria-label', 'Filter kolom');
3325
+ pop.setAttribute('aria-label', textOf(root, 'filter'));
3210
3326
  var f = colFilters[idx] || { type: type };
3211
3327
 
3212
3328
  if (type === 'text') {
3213
- pop.appendChild(filterTitle('Berisi teks'));
3329
+ pop.appendChild(filterTitle(textOf(root, 'filterText')));
3214
3330
  var inp = document.createElement('input');
3215
3331
  inp.className = 'fdy-input';
3216
3332
  inp.type = 'search';
3217
- inp.placeholder = 'Berisi…';
3333
+ inp.placeholder = textOf(root, 'filterTextPlaceholder');
3218
3334
  inp.value = f.text || '';
3219
3335
  inp.addEventListener('input', function () {
3220
3336
  f.text = inp.value.trim();
@@ -3224,7 +3340,7 @@
3224
3340
  });
3225
3341
  pop.appendChild(inp);
3226
3342
  } else if (type === 'enum') {
3227
- pop.appendChild(filterTitle('Tampilkan nilai'));
3343
+ pop.appendChild(filterTitle(textOf(root, 'filterEnum')));
3228
3344
  var list = document.createElement('div');
3229
3345
  list.className = 'fdy-filter__list';
3230
3346
  var set = f.set || new Set();
@@ -3247,7 +3363,7 @@
3247
3363
  });
3248
3364
  pop.appendChild(list);
3249
3365
  } else if (type === 'number') {
3250
- pop.appendChild(filterTitle('Rentang nilai'));
3366
+ pop.appendChild(filterTitle(textOf(root, 'filterRange')));
3251
3367
  var range = document.createElement('div');
3252
3368
  range.className = 'fdy-filter__range';
3253
3369
  var minI = numberInput('Min', f.min);
@@ -3272,7 +3388,7 @@
3272
3388
  var reset = document.createElement('button');
3273
3389
  reset.type = 'button';
3274
3390
  reset.className = 'fdy-btn fdy-btn--ghost fdy-btn--sm';
3275
- reset.textContent = 'Reset';
3391
+ reset.textContent = textOf(root, 'reset');
3276
3392
  reset.addEventListener('click', function () {
3277
3393
  delete colFilters[idx];
3278
3394
  markActive(btn, false);
@@ -3282,7 +3398,7 @@
3282
3398
  var done = document.createElement('button');
3283
3399
  done.type = 'button';
3284
3400
  done.className = 'fdy-btn fdy-btn--sm';
3285
- done.textContent = 'Tutup';
3401
+ done.textContent = textOf(root, 'close');
3286
3402
  done.addEventListener('click', function () { closePopover(true); });
3287
3403
  foot.appendChild(reset); foot.appendChild(done);
3288
3404
  pop.appendChild(foot);
@@ -3307,7 +3423,7 @@
3307
3423
  btn.className = 'fdy-table__filterbtn';
3308
3424
  btn.setAttribute('aria-haspopup', 'dialog');
3309
3425
  btn.setAttribute('aria-pressed', 'false');
3310
- btn.setAttribute('aria-label', 'Filter kolom');
3426
+ btn.setAttribute('aria-label', textOf(root, 'filter'));
3311
3427
  btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18l-7 8v5l-4 2v-7z"></path></svg>';
3312
3428
  btn.addEventListener('click', function (e) {
3313
3429
  e.stopPropagation();
@@ -3338,8 +3454,8 @@
3338
3454
 
3339
3455
  var shownFrom = total === 0 ? 0 : start + 1;
3340
3456
  var shownTo = start + slice.length;
3341
- if (countEl) countEl.textContent = total + ' baris';
3342
- if (infoEl) infoEl.textContent = 'Menampilkan ' + shownFrom + '–' + shownTo + ' dari ' + total;
3457
+ if (countEl) countEl.textContent = textOf(root, 'rows', { n: total });
3458
+ if (infoEl) infoEl.textContent = textOf(root, 'info', { from: shownFrom, to: shownTo, total: total });
3343
3459
  buildPager(totalPages);
3344
3460
  syncSelectAll(slice);
3345
3461
  updateBulk();
@@ -3768,6 +3884,21 @@
3768
3884
  setTimeout(finish, 400); // fallback if transitionend never fires
3769
3885
  }
3770
3886
 
3887
+
3888
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
3889
+ * path — and every one overridable per element, so a host that speaks another language (the
3890
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
3891
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
3892
+ var TEXT = {
3893
+ close: 'Tutup'
3894
+ };
3895
+ function textOf(root, key, vars) {
3896
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
3897
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
3898
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
3899
+ return s;
3900
+ }
3901
+
3771
3902
  function el(tag, className, text) {
3772
3903
  var node = document.createElement(tag);
3773
3904
  if (className) node.className = className;
@@ -3789,7 +3920,7 @@
3789
3920
 
3790
3921
  var close = el('button', 'fdy-toast__close');
3791
3922
  close.type = 'button';
3792
- close.setAttribute('aria-label', 'Tutup');
3923
+ close.setAttribute('aria-label', opts.closeLabel || TEXT.close);
3793
3924
  close.innerHTML = '&times;';
3794
3925
  close.addEventListener('click', function () { dismiss(node); });
3795
3926
 
@@ -3974,6 +4105,27 @@
3974
4105
  });
3975
4106
  }
3976
4107
 
4108
+
4109
+ /* User-facing strings. Indonesian by default — documented and deliberate for the raw enhancer
4110
+ * path — and every one overridable per element, so a host that speaks another language (the
4111
+ * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
4112
+ * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
4113
+ var TEXT = {
4114
+ remove: 'Hapus {name}',
4115
+ progress: 'Progres unggah {name}',
4116
+ uploading: 'Mengunggah…',
4117
+ waiting: 'Menunggu server…',
4118
+ done: 'Terunggah',
4119
+ badType: 'Tipe berkas tidak didukung.',
4120
+ tooBig: 'Ukuran melebihi batas ({max}).'
4121
+ };
4122
+ function textOf(root, key, vars) {
4123
+ var custom = root && root.getAttribute ? root.getAttribute('data-fdy-text-' + key) : null;
4124
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
4125
+ if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
4126
+ return s;
4127
+ }
4128
+
3977
4129
  function makeRow(file, zone) {
3978
4130
  var el = document.createElement('div');
3979
4131
  el.className = 'fdy-file';
@@ -3993,7 +4145,7 @@
3993
4145
  var remove = document.createElement('button');
3994
4146
  remove.type = 'button';
3995
4147
  remove.className = 'fdy-file__remove';
3996
- remove.setAttribute('aria-label', 'Hapus ' + file.name);
4148
+ remove.setAttribute('aria-label', textOf(zone, 'remove', { name: file.name }));
3997
4149
  remove.innerHTML = '&times;';
3998
4150
  remove.addEventListener('click', function () {
3999
4151
  /* Dispatched on the ZONE, not on the row — the same target as fdy-upload-add, so one listener
@@ -4020,7 +4172,7 @@
4020
4172
  progressEl.setAttribute('role', 'progressbar');
4021
4173
  progressEl.setAttribute('aria-valuemin', '0');
4022
4174
  progressEl.setAttribute('aria-valuemax', '100');
4023
- progressEl.setAttribute('aria-label', 'Progres unggah ' + file.name);
4175
+ progressEl.setAttribute('aria-label', textOf(zone, 'progress', { name: file.name }));
4024
4176
  bar = document.createElement('div');
4025
4177
  bar.className = 'fdy-progress__bar';
4026
4178
  bar.style.width = '0%';
@@ -4053,7 +4205,7 @@
4053
4205
  uploading: function () {
4054
4206
  el.classList.remove('fdy-file--error', 'fdy-file--success');
4055
4207
  icon.innerHTML = FILE_ICON;
4056
- sub.textContent = fmtSize(file.size) + ' · Mengunggah…';
4208
+ sub.textContent = fmtSize(file.size) + ' · ' + textOf(zone, 'uploading');
4057
4209
  ensureProgress();
4058
4210
  determinate();
4059
4211
  },
@@ -4073,7 +4225,7 @@
4073
4225
  waiting: function (label) {
4074
4226
  el.classList.remove('fdy-file--error', 'fdy-file--success');
4075
4227
  icon.innerHTML = FILE_ICON;
4076
- sub.textContent = fmtSize(file.size) + ' · ' + (label || 'Menunggu server…');
4228
+ sub.textContent = fmtSize(file.size) + ' · ' + (label || textOf(zone, 'waiting'));
4077
4229
  ensureProgress();
4078
4230
  progressEl.classList.add('fdy-progress--indeterminate');
4079
4231
  /* The modifier styles .fdy-progress__bar, so it must sit on the CONTAINER, and the inline
@@ -4087,7 +4239,7 @@
4087
4239
  el.classList.add('fdy-file--success');
4088
4240
  el.classList.remove('fdy-file--error');
4089
4241
  icon.innerHTML = OK_ICON;
4090
- sub.textContent = fmtSize(file.size) + ' · Terunggah';
4242
+ sub.textContent = fmtSize(file.size) + ' · ' + textOf(zone, 'done');
4091
4243
  dropProgress();
4092
4244
  },
4093
4245
  fail: function (msg) {
@@ -4155,8 +4307,8 @@
4155
4307
  if (!fileList) return;
4156
4308
  Array.prototype.slice.call(fileList).forEach(function (file) {
4157
4309
  var reason = null;
4158
- if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
4159
- else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
4310
+ if (!accepts(file, acceptAttr)) reason = textOf(zone, 'badType');
4311
+ else if (maxSize && file.size > maxSize) reason = textOf(zone, 'tooBig', { max: fmtSize(maxSize) });
4160
4312
  var row = makeRow(file, zone);
4161
4313
  if (list) list.appendChild(row.el);
4162
4314
  if (reason) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.38.0",
3
+ "version": "1.40.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 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",
95
+ "test:browser": "node --test 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",
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"
@@ -1,6 +1,12 @@
1
1
  /* Freeday — Date picker (input-styled trigger + calendar popover).
2
2
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
3
3
  .fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
4
+ /* Inside a labelled field, a picker fills the field like every other control.
5
+ The 14rem/11rem caps above suit a picker standing on its own; in a form grid
6
+ they leave the date box visibly short of the combo beside it — 224px against
7
+ 319px in a two-column layout — and the row reads as ragged. The field's own
8
+ max-width is what should govern there. */
9
+ .fdy-field>.fdy-datepicker,.fdy-field>.fdy-timepicker,.fdy-field>.fdy-daterange{max-width:none;}
4
10
  .fdy-datepicker__trigger{display:inline-flex;align-items:center;gap:var(--space-2);width:100%;height:var(--control-h);padding:0 var(--space-3);text-align:left;cursor:pointer;font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
5
11
  .fdy-datepicker__trigger:hover{border-color:var(--color-text-muted);}
6
12
  .fdy-datepicker__trigger:focus-visible{outline:none;border-color:var(--color-primary);box-shadow:inset 0 1px 2px rgba(16,14,30,.04),0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}