@cahyo-dimas/freeday 3.12.0 → 3.13.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
@@ -9,6 +9,94 @@ benar. Perubahan seperti itu ditulis di bawah `### Changed: BREAKING (types)`
9
9
  lama → tipe baru, dan cara menyempitkannya — bukan di bawah `### Added`, betapapun aditifnya dari
10
10
  sisi kit.
11
11
 
12
+ ## [3.13.0] - 2026-10-29
13
+
14
+ **Kit berhenti memutuskan hal yang bukan urusannya, dan markup yang tak pernah dihidrasi kini
15
+ bersuara.** Lima butir dari `CATATAN-FREEDAY.md` app ronda (§9 sampai §13), dikerjakan dalam satu
16
+ rilis. Tiga di antaranya gagal tanpa galat apa pun: chip yang hilang sesudah Batal, tombol simpan
17
+ yang mati, dan kelas yang tak cocok dengan aturan mana pun.
18
+
19
+ ### Changed
20
+ - **`fdy-chip-remove` dan `fdy-upload-remove` kini dapat dibatalkan** (`#098`). Begitu
21
+ `.fdy-chip__remove` ditekan, enhancer chip langsung menghapus chip-nya, padahal kit tak tahu
22
+ apakah penghapusan itu perlu ditanyakan dulu. Matriks pola rotasi di app ronda memakai chip
23
+ sebagai tampilan dan punya dialog konfirmasi sendiri. Chip-nya hilang ketika dialog masih
24
+ terbuka, dan tetap hilang sesudah **Batal** ditekan. Datanya utuh, layarnya yang salah. Sekarang
25
+ event-nya `cancelable`, dan elemennya baru dihapus kalau tak ada listener yang memanggil
26
+ `preventDefault()`. Perubahan ini aditif: app yang tidak membatalkan mendapat perilaku yang persis
27
+ sama. Baris upload ikut diubah karena bentuknya sama, dan berkas yang sudah dipegang server
28
+ memang layak ditanyakan dulu sebelum dibuang.
29
+
30
+ ### Added
31
+ - **Peringatan konsol untuk hook yang tak pernah dihidrasi** (`#099`): `src/freeday-unhydrated.js`,
32
+ sudah termasuk di import akar. Klik, fokus atau submit pertama di dalam elemen ber-hook yang tak
33
+ punya penanda siap mencetak satu `console.warn` per elemen. Pesannya menyebut hook-nya, dan
34
+ elemennya ikut sebagai argumen kedua supaya devtools menautkannya. Pemicunya §10: `useFreeday()`
35
+ yang dipasang di pembungkus yang salah membuat `data-fdy-validate` di luar pembungkus itu diam
36
+ total, tanpa kalimat validasi dan tanpa `fdy-form-valid`. Yang memberi tahu cuma dua uji e2e yang
37
+ merah. Catatannya meminta peringatan ini untuk form, tetapi kegagalannya berlaku untuk setiap
38
+ hook, jadi tabelnya memuat 29 hook dari 24 enhancer. Peringatan dipicu interaksi, bukan saat
39
+ halaman dimuat, karena framework me-mount dan menghidrasi di antara keduanya.
40
+ - **`fdy-help--danger`** (`#100`), alias `fdy-help--error` di dalam aturan yang sama. Kit memakai
41
+ `--danger` sebagai kata nada di mana-mana (tombol, alert, lencana, toast), sehingga app menebak
42
+ `fdy-help--danger`. Kelas itu tak cocok dengan aturan apa pun, dan kalimat peringatannya tampil
43
+ abu-abu. Kontrol form sengaja tidak diberi alias. Keadaannya digerakkan `aria-invalid`, jadi
44
+ `--error` di sana jarang ditulis tangan. Aturan penamaannya kini tertulis di COMPONENTS.md.
45
+
46
+ ### Fixed
47
+ - **`FreedayCfl` dan `FreedayBreakpoint` tak punya `initAll`** (`#099`), padahal hanya nama itu
48
+ yang dicari ketiga helper stack (`useFreeday` di Vue dan React, `FreedayBlazor.initAll`). Markup
49
+ CFL mentah di dalam subtree framework dilewati ketiganya dan dialognya tak pernah terbuka. Label
50
+ breakpoint yang di-mount belakangan tetap kosong sampai breakpoint berganti. Tak ada yang
51
+ melaporkan ini; keduanya ketemu saat mengaudit kelas cacat §10.
52
+
53
+ ### Docs
54
+ - **`<ul>` polos adalah daftar berbutir yang didukung** (`#101`). `base.css` memang tak pernah
55
+ mereset daftar. Sekarang COMPONENTS.md menjanjikannya, dan sebuah uji menjaga janji itu.
56
+ - §13 (`required` di `FdyCombo`) sudah terkirim di 3.12.0 (`#096`). Tak ada kode baru; app tinggal
57
+ memakainya.
58
+
59
+ ### Gerbang
60
+ - `docs.test.mjs`: setiap penanda siap yang dicap enhancer wajib ada di tabel peringatan, dicap di
61
+ berkas milik namespace yang disebut tabel, dan berkas itu wajib benar-benar meng-query selektor
62
+ pasangannya. Setiap enhancer yang menghidrasi markup wajib mengekspos `initAll`. Penjaga string
63
+ kini melewati teks `console.warn` (untuk devtools, tak pernah masuk DOM) dan identifier
64
+ PascalCase.
65
+ - `css.test.mjs`: `fdy-help--danger` harus satu aturan dengan `--error`, dan tak boleh ada aturan
66
+ tanpa cakupan yang mereset `ul`/`ol`/`li`.
67
+ - `browser/app-owned.mjs`, dengan mouse asli: chip dan baris upload yang dibatalkan tetap di
68
+ tempatnya, form yatim memperingatkan sekali sementara form yang terhidrasi diam, dan CFL serta
69
+ label breakpoint yang di-mount belakangan terhidrasi lewat `FreedayBlazor.initAll`.
70
+ - Tiga belas mutasi diverifikasi merah sebelum dipulihkan: enam pada perilaku, lima pada tabel dan
71
+ ekspor, dua pada CSS.
72
+
73
+ ## [3.12.1] - 2026-10-27
74
+
75
+ **Modal dan drawer kembali beranimasi di bawah Safari 17.4, termasuk di iPhone 7 pemiliknya.**
76
+
77
+ ### Fixed
78
+ - **Empat shorthand `transition` dipecah jadi longhand** (`#097`), di `modal.css` dan `drawer.css`.
79
+ Shorthand yang membawa satu keyword yang tak dikenal mesin **tidak sah saat parse**, jadi seluruh
80
+ deklarasinya dibuang, dan `allow-discrete` tak dikenal di bawah Safari 17.4, yang ada di bawah
81
+ lantai dukungan kit. Akibatnya modal dan drawer buka-tutup tanpa animasi apa pun. Dipecah begini,
82
+ yang hilang di bawah lantai hanya `transition-behavior`, dan paruh opacity/transform-nya bertahan.
83
+ `overlay` dan `display` tetap terdaftar di `transition-property`, tempat nama yang tak cocok tetap
84
+ sah secara sintaks dan sekadar tak pernah cocok, jadi tak ada lagi yang ikut jatuh. Keluarnya
85
+ tetap seketika di sana sebab `display` memang tak dapat ditransisikan di bawah lantai, sama
86
+ seperti sebelumnya, jadi pemecahan ini tidak mengambil apa pun.
87
+
88
+ Ekuivalensinya di atas lantai **diukur, bukan dinalar**: `transition-property`,
89
+ `transition-timing-function` dan `transition-behavior` identik byte-per-byte sebelum dan sesudah,
90
+ dan satu-satunya beda adalah satu nilai `transition-duration` yang terserialisasi sebagai satu
91
+ nilai alih-alih tiga. Bahwa satu nilai itu tetap berlaku untuk SETIAP properti juga diukur: 280ms
92
+ masing-masing, dan easing tetap per-indeks.
93
+
94
+ Harganya empat deklarasi yang harus tetap sejalan, dan itu kini dijaga uji: `baseline.test.mjs`
95
+ menolak `allow-discrete` di dalam shorthand `transition` mana pun, dan menolak
96
+ `transition-behavior` yang berdiri tanpa ketiga longhand lainnya. Regresi yang paling mungkin
97
+ adalah seseorang merapikan empat longhand kembali menjadi satu shorthand, yang terbaca lebih
98
+ bersih dan diam-diam mengambil animasinya dari setiap peramban di bawah Safari 17.4.
99
+
12
100
  ## [3.12.0] - 2026-10-27
13
101
 
14
102
  **Sebuah form kini dapat menuntut kontrol kit terisi.** `data-fdy-validate` mewirekan Constraint
@@ -67,7 +155,7 @@ perbaikan ini terdengar jauh lebih mahal daripada yang ternyata.
67
155
 
68
156
  **Audit kontrol native di seluruh layar sebuah aplikasi konsumen, bukan di satu layar yang
69
157
  mengeluh.** Pemiliknya melaporkan tampilan HP aslinya tak cocok dengan peramban di lebar yang sama,
70
- dan dugaan pertama -- termasuk dugaan saya -- adalah CSS. Pengukuran menyangkalnya: dengan
158
+ dan dugaan pertama, termasuk dugaan saya, adalah CSS. Pengukuran menyangkalnya: dengan
71
159
  stylesheet aplikasi yang sebenarnya, pada 375x667, Chromium dan WebKit menata `date`, `time`,
72
160
  `number`, `search` dan `text` **identik**, 293x44 keenamnya, selisih satu piksel pada `y` yang
73
161
  cuma pembulatan. Jadi yang berbeda bukan tata letaknya, melainkan **widget native-nya**: `date` dan
@@ -79,7 +167,7 @@ layar yang selebihnya sama.
79
167
  Kit sudah mengatakan ini di tabelnya sendiri ("Use `.fdy-datepicker` / `.fdy-timepicker` /
80
168
  `.fdy-datetimepicker` instead") dan sudah mengirim CSS plus enhancer-nya. Yang belum ada adalah
81
169
  wrapper bertipe untuk timepicker, jadi aplikasi Vue/React/Blazor harus merakit sendiri
82
- plumbing custom-event-nya -- dan merakit sendiri adalah yang membuat empat `<input type="time">`
170
+ plumbing custom-event-nya, dan merakit sendiri adalah yang membuat empat `<input type="time">`
83
171
  bertahan di satu formulir shift.
84
172
 
85
173
  Catatan kejujuran: perbedaan iOS itu **tidak** direproduksi di sini. WebKit desktop bukan Mobile
@@ -89,8 +177,8 @@ lokal, dan kini diuji, adalah perilaku penggantinya.
89
177
 
90
178
  ### Added
91
179
  - **`FdyTimepicker`** (`#094`), wrapper bertipe di **ketiga** stack, menjadikannya tiga belas.
92
- Vue dan React mengimplementasikan listbox-nya secara native sebab ia kontrol form -- alasan yang
93
- sama dengan `FdyCombo`, dan alasan yang `FdyChart` nyatakan dari sisi sebaliknya: enhancer yang
180
+ Vue dan React mengimplementasikan listbox-nya secara native sebab ia kontrol form. Alasannya sama
181
+ dengan `FdyCombo`, dan `FdyChart` menyatakannya dari sisi sebaliknya: enhancer yang
94
182
  menguasai DOM berkelahi dengan render framework dan tak menyisakan `v-model`. Blazor
95
183
  menjembatani enhancer lewat seed, seperti `FdyDatepicker`. Sorotannya **dijepit, bukan
96
184
  berputar**: pada daftar 48 opsi, ArrowUp di `00:00` yang melompat ke `23:30` terbaca sebagai
@@ -99,7 +187,7 @@ lokal, dan kini diuji, adalah perilaku penggantinya.
99
187
  - **`data-id` dan `data-describedby` pada seed timepicker** (`#094`). Keduanya tak terbaca
100
188
  sebelumnya, jadi host yang menampilkan teks galat tak punya cara menautkannya. Pemicunya
101
189
  `<button>`, yang **bukan** elemen labelable, sehingga `<label for>` memang tak akan pernah
102
- mengaktifkannya -- itu didokumentasikan sekarang, bukan dibiarkan jadi kejutan.
190
+ mengaktifkannya, dan itu kini didokumentasikan.
103
191
  - **`FreedayTimepicker.setState(root, { disabled, readonly, invalid })`** (`#094`), bentuknya sama
104
192
  dengan `FreedayDatepicker.setState`. Tanpa ini, host yang merender seed-nya sekali lalu tidak
105
193
  pernah lagi (Blazor, `ShouldRender => false`) memiliki tiga parameter keadaan yang tak ada yang
@@ -109,7 +197,7 @@ lokal, dan kini diuji, adalah perilaku penggantinya.
109
197
  - **`FdyDateRange` membuang kedelapan prop label navigasi kalender** (`#095`), di ketiga stack.
110
198
  `FdyDatepicker` menerimanya sejak 2.1.1; rentangnya tidak meneruskannya, sehingga rentang tanggal
111
199
  adalah **satu-satunya** kontrol tanggal di kit yang navigasinya tak bisa dinamai dalam bahasa
112
- halamannya sendiri -- aplikasi berbahasa Indonesia mendapat "Previous month" tepat di tempat yang
200
+ halamannya sendiri: aplikasi berbahasa Indonesia mendapat "Previous month" tepat di tempat yang
113
201
  tak dapat ia jangkau. Diteruskan apa adanya ke kedua pemilih. Prop datar `xxxLabel`, bukan objek
114
202
  `labels`, sebab bentuk itulah yang sudah dipakai setiap komponen lain di kit. Blazor menambah
115
203
  `BackToMonthsLabel`, karena seed yang kedua pemilihnya dibangun dari situ punya tombol itu.
package/COMPONENTS.md CHANGED
@@ -32,6 +32,10 @@ live docs.
32
32
  state *and* the screen-reader state. Pair it with `aria-describedby` → the `id` of a
33
33
  `.fdy-help.fdy-help--error`. The `--error` class modifiers exist only for the case where the
34
34
  invalid state does not belong to the control itself.
35
+ **`--error` names a field's validation state, `--danger` names a tone** (button, alert, badge,
36
+ toast, menu item). `.fdy-help` is the one block that takes both, `--danger` being the same
37
+ colour, because help text is where either reading is natural: a sentence that warns without
38
+ any field being invalid.
35
39
  6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
36
40
  alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
37
41
  7. **On Vue, React or Blazor, thirteen components have a typed wrapper. Use it.** `FdyCombo` ·
@@ -44,6 +48,10 @@ live docs.
44
48
  breaks quietly: later-rendered DOM is never hydrated and the widget's state sits in the DOM
45
49
  instead of your framework's. Everything else is the same markup everywhere; hydrate the other
46
50
  interactive components with `useFreeday` (Vue/React) or `FreedayBlazor.initAll` (Blazor).
51
+ **Markup no `initAll` reached says so.** The first click, focus or submit inside a hook the kit
52
+ never enhanced logs one `console.warn` naming it, with the element attached. The usual cause is
53
+ a `useFreeday()` ref on a wrapper that does not contain it. `freeday-unhydrated.js` does this,
54
+ and it ships in the root import.
47
55
 
48
56
  ### Containment — why the kit's containers are `position:relative`
49
57
 
@@ -562,7 +570,7 @@ native control, otherwise a `<div>` + explicitly associated label.
562
570
  `.fdy-field--full` on the fields — **not** a `max-width` override on the container. Both of this
563
571
  kit's own consoles wrote that override, once per form, before this modifier reached outside the
564
572
  grid.
565
- - `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
573
+ - `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`, or its alias `--danger`)
566
574
 
567
575
  **Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
568
576
  `.fdy-label` on the legend. The kit already resets the UA border/padding and supplies the spacing:
@@ -1214,6 +1222,9 @@ zone.addEventListener('fdy-upload-add', (e) => {
1214
1222
  zone.addEventListener('fdy-upload-remove', (e) => forget(e.detail.file)); // same element
1215
1223
  ```
1216
1224
 
1225
+ `fdy-upload-remove` is **cancelable** too: `preventDefault()` keeps the row, for a file the server
1226
+ already holds and the user should confirm losing. Remove it yourself afterwards with `row.el.remove()`.
1227
+
1217
1228
  **Bring your own row:** omit the file list entirely (no `data-filelist`, and no `.fdy-filelist`
1218
1229
  sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add`, where `detail.row`
1219
1230
  still works, its element simply isn't attached. Note the fallback when `data-filelist` is absent is
@@ -1757,6 +1768,10 @@ responsive `.fdy-datatable` should become below `md`, rather than a stack of `.f
1757
1768
  control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
1758
1769
  reads as clickable while it is refusing input.
1759
1770
  - Not `.fdy-list-reset`, which only strips UA bullets/indent from a semantic list.
1771
+ - Not for three bullet points in a dialog either. **A plain bulleted list is a plain `<ul>`/`<ol>`,
1772
+ no class**, and that is supported: `base.css` deliberately leaves lists alone, so native bullets
1773
+ and indent survive (a test keeps it that way). The exception is a utility framework run with its
1774
+ preflight on, which strips them.
1760
1775
 
1761
1776
  ```html
1762
1777
  <ul class="fdy-list">
@@ -2335,6 +2350,21 @@ Three roles inside a `.fdy-chips` row:
2335
2350
 
2336
2351
  Interactive chips are managed by `freeday-chip.js`: wrap the row in `data-fdy-chips` (+
2337
2352
  `data-single` for pick-one, `data-label` for the group name), give each `data-value`.
2353
+
2354
+ **Removal is cancelable.** Pressing `.fdy-chip__remove` fires `fdy-chip-remove` (`detail.value`) on
2355
+ the chip, then removes the chip only if no listener called `preventDefault()`. Cancel it whenever
2356
+ the removal is the app's call: a confirm step, or chips rendered from framework state, which
2357
+ should leave the DOM when the state changes and not before. Without that, Cancel in your own
2358
+ confirm dialog leaves the data intact and the chip gone.
2359
+
2360
+ ```js
2361
+ row.addEventListener('fdy-chip-remove', async (e) => {
2362
+ e.preventDefault(); // the kit leaves the chip alone
2363
+ if (await Freeday.confirm({ title: `Remove ${e.detail.value}?`, tone: 'danger' })) {
2364
+ removeFromState(e.detail.value); // your render takes it out
2365
+ }
2366
+ });
2367
+ ```
2338
2368
  `--tone-1`…`--tone-8` for non-semantic category colours.
2339
2369
 
2340
2370
  ## Timeline — `.fdy-timeline`
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev. UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.12.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.12.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.13.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.13.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs. The UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.12.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.12.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.13.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.13.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit. One source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -26,10 +26,19 @@
26
26
  if (bp === last) return;
27
27
  last = bp;
28
28
  root.setAttribute('data-breakpoint', bp);
29
- Array.prototype.forEach.call(document.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
29
+ fillLabels(document);
30
+ root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
31
+ }
32
+
33
+ /* Also run over late-mounted markup by the stack helpers, which find it through initAll: a label
34
+ rendered after load otherwise stayed empty until the next breakpoint change. */
35
+ function fillLabels(context) {
36
+ var ctx = context || document;
37
+ var bp = last || current();
38
+ if (ctx.matches && ctx.matches('[data-fdy-breakpoint-label]')) ctx.textContent = bp;
39
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
30
40
  el.textContent = bp;
31
41
  });
32
- root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
33
42
  }
34
43
 
35
44
  window.addEventListener('resize', update, { passive: true });
@@ -40,6 +49,7 @@
40
49
  }
41
50
 
42
51
  window.FreedayBreakpoint = {
52
+ initAll: fillLabels,
43
53
  get: function () { return last || current(); },
44
54
  current: current,
45
55
  // true if the current breakpoint is `name` or wider (e.g. is('md') → md, lg, or xl)
@@ -240,5 +240,7 @@
240
240
  initTriggers();
241
241
  }
242
242
 
243
- window.FreedayCfl = { init: initTriggers };
243
+ /* initAll is what every stack helper looks for (useFreeday in Vue and React, FreedayBlazor.initAll):
244
+ without it they skipped this enhancer, and raw CFL markup rendered by a framework never opened. */
245
+ window.FreedayCfl = { init: initTriggers, initAll: initTriggers };
244
246
  })();
@@ -1,8 +1,10 @@
1
1
  /* Freeday, chip enhancer (optional, zero-dependency).
2
2
  * Two behaviours, both progressive enhancement:
3
3
  *
4
- * 1. Removable chips, any .fdy-chip__remove button removes its chip and emits a
5
- * bubbling "fdy-chip-remove" (detail {value}).
4
+ * 1. Removable chips, any .fdy-chip__remove button emits a bubbling, CANCELABLE
5
+ * "fdy-chip-remove" (detail {value}) and then removes its chip, unless a listener called
6
+ * preventDefault(). Cancel it whenever the app owns the removal: a confirm step, or a chip
7
+ * rendered from framework state, which should leave the DOM when the state does.
6
8
  * 2. Choice / filter groups, a [data-fdy-chips] container of interactive chips
7
9
  * (.fdy-chip--choice / .fdy-chip--filter, each a <button>) toggles aria-pressed
8
10
  * on click. Add data-single for one-at-a-time (radio-like) selection; filter
@@ -28,8 +30,12 @@
28
30
  if (rm) rm.remove();
29
31
  value = (clone.textContent || '').trim();
30
32
  }
31
- chip.dispatchEvent(new CustomEvent('fdy-chip-remove', { bubbles: true, detail: { value: value } }));
32
- chip.remove();
33
+ /* Cancelable, because the kit cannot know whether this removal needs asking first. Removing
34
+ unconditionally made a chip vanish while the app's own confirm dialog was still open, and
35
+ stay gone after Cancel, with the data intact and the screen wrong (CATATAN-FREEDAY §9). */
36
+ var ev = new CustomEvent('fdy-chip-remove', { bubbles: true, cancelable: true, detail: { value: value } });
37
+ chip.dispatchEvent(ev);
38
+ if (!ev.defaultPrevented) chip.remove();
33
39
  });
34
40
  }
35
41
 
@@ -0,0 +1,76 @@
1
+ /* Freeday, un-hydrated hook warning (optional, zero-dependency, no markup of its own).
2
+ * Every enhancer marks the element it took over (`data-fdy-*-ready`). Markup that no initAll()
3
+ * ever reached carries the hook and not the mark, and it fails in the worst way available: it
4
+ * renders, looks finished, and does nothing, with no error anywhere. A `data-fdy-validate` form
5
+ * outside the subtree handed to useFreeday() neither paints a message nor fires fdy-form-valid,
6
+ * so its save button reads as dead (CATATAN-FREEDAY §10). The enhancer knows the attribute is
7
+ * there; the person who wrote it does not.
8
+ *
9
+ * So on the first click, focus or submit inside such an element this logs ONE console.warn per
10
+ * element, naming the hook, with the element itself as the second argument so devtools links it.
11
+ * Interaction, not load, is the trigger on purpose: a framework mounts and hydrates in between,
12
+ * and only once a person is touching the widget is "not enhanced yet" the same as "never".
13
+ *
14
+ * HOOKS pairs each hook selector with the ready flag its enhancer stamps on THAT element and the
15
+ * namespace that owns it. A hook whose enhancer is not loaded is skipped: that is a different
16
+ * mistake, and this message would misname it. test/docs.test.mjs fails if an enhancer gains a
17
+ * ready flag this table does not list.
18
+ */
19
+ (function () {
20
+ 'use strict';
21
+
22
+ var HOOKS = [
23
+ ['[data-fdy-app]', 'fdyAppShellReady', 'FreedayAppShell'],
24
+ ['[data-fdy-autocomplete]', 'fdyAcReady', 'FreedayAutocomplete'],
25
+ ['[data-fdy-carousel]', 'fdyCarouselReady', 'FreedayCarousel'],
26
+ ['[data-fdy-cascade]', 'fdyCascadeReady', 'FreedayCascade'],
27
+ ['[data-fdy-cfl]', 'fdyCflBound', 'FreedayCfl'],
28
+ ['[data-fdy-cfl-open]', 'fdyCflBound', 'FreedayCfl'],
29
+ ['[data-fdy-chart]', 'fdyChartReady', 'FreedayChart'],
30
+ ['[data-fdy-chips]', 'fdyChipsReady', 'FreedayChip'],
31
+ ['.fdy-chip__remove', 'fdyChipRmReady', 'FreedayChip'],
32
+ ['[data-fdy-datepicker]', 'fdyDpReady', 'FreedayDatepicker'],
33
+ ['[data-fdy-daterange]', 'fdyDrReady', 'FreedayDatepicker'],
34
+ ['[data-fdy-datetimepicker]', 'fdyDtReady', 'FreedayDatetime'],
35
+ ['[data-fdy-drawer]', 'fdyDrawerBound', 'FreedayDrawer'],
36
+ ['.fdy-drawer', 'fdyDrawerReady', 'FreedayDrawer'],
37
+ ['[data-fdy-validate]', 'fdyFormReady', 'FreedayForm'],
38
+ ['[data-fdy-mask]', 'fdyMaskReady', 'FreedayMask'],
39
+ ['[data-fdy-password]', 'fdyPwReady', 'FreedayMask'],
40
+ ['[data-fdy-menu]', 'fdyMenuReady', 'FreedayMenu'],
41
+ ['[data-fdy-number]', 'fdyNumberReady', 'FreedayNumber'],
42
+ ['[data-fdy-rating]', 'fdyRatingReady', 'FreedayRating'],
43
+ ['[data-fdy-search]', 'fdySearchReady', 'FreedaySearch'],
44
+ ['[data-fdy-combo]', 'fdyComboReady', 'FreedayCombo'],
45
+ ['[data-fdy-slider]', 'fdySliderReady', 'FreedaySlider'],
46
+ ['[data-fdy-stepper]', 'fdyStepperReady', 'FreedayStepper'],
47
+ ['[data-fdy-table]', 'fdyTableReady', 'FreedayTable'],
48
+ ['[data-fdy-tabs]', 'fdyTabsReady', 'FreedayTabs'],
49
+ ['[data-fdy-timepicker]', 'fdyTpReady', 'FreedayTimepicker'],
50
+ ['[data-fdy-tree]', 'fdyTreeReady', 'FreedayTree'],
51
+ ['[data-fdy-dropzone]', 'fdyUploadReady', 'FreedayUpload']
52
+ ];
53
+
54
+ var warned = typeof WeakSet === 'function' ? new WeakSet() : null;
55
+
56
+ function check(e) {
57
+ if (!warned || !e.target || !e.target.closest) return;
58
+ for (var i = 0; i < HOOKS.length; i++) {
59
+ var hook = HOOKS[i];
60
+ if (!window[hook[2]]) continue;
61
+ var el = e.target.closest(hook[0]);
62
+ if (!el || el.dataset[hook[1]] === '1' || warned.has(el)) continue;
63
+ warned.add(el);
64
+ console.warn('Freeday: this ' + hook[0] + ' was never enhanced, so it does nothing. Either it is '
65
+ + 'outside every initAll() scope (Vue/React: inside the element given to useFreeday(); Blazor: '
66
+ + 'FreedayBlazor.initAll after render; anything else: ' + hook[2] + '.initAll(el) once it is in '
67
+ + 'the DOM), or its markup lacks a part the enhancer needs (see COMPONENTS.md).', el);
68
+ }
69
+ }
70
+
71
+ ['click', 'focusin', 'submit'].forEach(function (type) {
72
+ document.addEventListener(type, check, true);
73
+ });
74
+
75
+ window.FreedayUnhydrated = { hooks: HOOKS };
76
+ })();
@@ -18,7 +18,8 @@
18
18
  * Emits bubbling CustomEvents. BOTH on the dropzone element, which is the one target a consumer
19
19
  * needs to listen on:
20
20
  * "fdy-upload-add" {file, rejected, reason, row}
21
- * "fdy-upload-remove" {file}
21
+ * "fdy-upload-remove" {file}, cancelable: preventDefault() keeps the row (e.g. to confirm
22
+ * removing a file the server already holds, then call row.el.remove())
22
23
  * `row` is the state machine over the rendered .fdy-file:
23
24
  * .ready() (rest, where a dropped file starts) / .uploading() / .setProgress(pct) /
24
25
  * .waiting(label) (sent, awaiting the server) / .done() / .fail(msg) / .el
@@ -111,8 +112,9 @@
111
112
  Firing on both would look safer, but it makes the pair asymmetric, one `add` and, for
112
113
  anyone delegating on a common ancestor, two `remove`s, which is its own silent bug. It
113
114
  also fixes the listless case: a row that was never attached bubbles to nothing at all. */
114
- zone.dispatchEvent(new CustomEvent('fdy-upload-remove', { bubbles: true, detail: { file: file } }));
115
- el.remove();
115
+ var ev = new CustomEvent('fdy-upload-remove', { bubbles: true, cancelable: true, detail: { file: file } });
116
+ zone.dispatchEvent(ev);
117
+ if (!ev.defaultPrevented) el.remove();
116
118
  });
117
119
  el.appendChild(icon);
118
120
  el.appendChild(meta);
@@ -2472,10 +2472,10 @@ a { color: var(--color-primary-strong); }
2472
2472
  /* display is gated behind [open] so the native <dialog> stays hidden until opened.
2473
2473
  * An unconditional display here would beat the UA `dialog:not([open]){display:none}`
2474
2474
  * rule (author origin wins over user-agent) and leave the drawer visible on load. */
2475
- .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition:transform var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
2475
+ .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
2476
2476
  .fdy-drawer[open]{display:flex;transform:none;}
2477
2477
  .fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
2478
- .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
2478
+ .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
2479
2479
  .fdy-drawer__header{flex:none;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
2480
2480
  .fdy-drawer__title{font-family:var(--font-display);font-weight:var(--weight-semibold);font-size:var(--text-lg);color:var(--color-text);margin:0 auto 0 0;}
2481
2481
  .fdy-drawer__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
@@ -2696,7 +2696,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2696
2696
  remember to mark aria-hidden, and it cannot be forgotten. */
2697
2697
  .fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
2698
2698
  .fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
2699
- .fdy-help--error{color:var(--color-danger);}
2699
+ /* --danger is the tone word everywhere else in the kit; help text takes either (CATATAN-FREEDAY §11). */
2700
+ .fdy-help--error,.fdy-help--danger{color:var(--color-danger);}
2700
2701
 
2701
2702
  /* Freeday. Keyboard key */
2702
2703
  .fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
@@ -2778,12 +2779,12 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2778
2779
  .fdy-btn-split__toggle svg{display:block;width:1rem;height:1rem;}
2779
2780
 
2780
2781
  /* Freeday. Modal (native <dialog>) */
2781
- .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);transition:opacity var(--dur-base) var(--ease-spring),transform var(--dur-base) var(--ease-spring),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
2782
+ .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
2782
2783
  .fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
2783
2784
  .fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
2784
2785
  .fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
2785
2786
  .fdy-modal--wide{width:min(60rem,calc(100vw - var(--space-8)));}
2786
- .fdy-modal::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-base) var(--ease-standard),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
2787
+ .fdy-modal::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
2787
2788
  .fdy-modal__header{flex:none;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
2788
2789
  .fdy-modal__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;}
2789
2790
  .fdy-modal__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
package/dist/freeday.css CHANGED
@@ -1295,10 +1295,10 @@ a { color: var(--color-primary-strong); }
1295
1295
  /* display is gated behind [open] so the native <dialog> stays hidden until opened.
1296
1296
  * An unconditional display here would beat the UA `dialog:not([open]){display:none}`
1297
1297
  * rule (author origin wins over user-agent) and leave the drawer visible on load. */
1298
- .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition:transform var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
1298
+ .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
1299
1299
  .fdy-drawer[open]{display:flex;transform:none;}
1300
1300
  .fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
1301
- .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
1301
+ .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
1302
1302
  .fdy-drawer__header{flex:none;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
1303
1303
  .fdy-drawer__title{font-family:var(--font-display);font-weight:var(--weight-semibold);font-size:var(--text-lg);color:var(--color-text);margin:0 auto 0 0;}
1304
1304
  .fdy-drawer__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
@@ -1519,7 +1519,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1519
1519
  remember to mark aria-hidden, and it cannot be forgotten. */
1520
1520
  .fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
1521
1521
  .fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
1522
- .fdy-help--error{color:var(--color-danger);}
1522
+ /* --danger is the tone word everywhere else in the kit; help text takes either (CATATAN-FREEDAY §11). */
1523
+ .fdy-help--error,.fdy-help--danger{color:var(--color-danger);}
1523
1524
 
1524
1525
  /* Freeday. Keyboard key */
1525
1526
  .fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
@@ -1601,12 +1602,12 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1601
1602
  .fdy-btn-split__toggle svg{display:block;width:1rem;height:1rem;}
1602
1603
 
1603
1604
  /* Freeday. Modal (native <dialog>) */
1604
- .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);transition:opacity var(--dur-base) var(--ease-spring),transform var(--dur-base) var(--ease-spring),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
1605
+ .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
1605
1606
  .fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
1606
1607
  .fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
1607
1608
  .fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
1608
1609
  .fdy-modal--wide{width:min(60rem,calc(100vw - var(--space-8)));}
1609
- .fdy-modal::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-base) var(--ease-standard),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
1610
+ .fdy-modal::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
1610
1611
  .fdy-modal__header{flex:none;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
1611
1612
  .fdy-modal__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;}
1612
1613
  .fdy-modal__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
package/dist/freeday.js CHANGED
@@ -409,10 +409,19 @@
409
409
  if (bp === last) return;
410
410
  last = bp;
411
411
  root.setAttribute('data-breakpoint', bp);
412
- Array.prototype.forEach.call(document.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
412
+ fillLabels(document);
413
+ root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
414
+ }
415
+
416
+ /* Also run over late-mounted markup by the stack helpers, which find it through initAll: a label
417
+ rendered after load otherwise stayed empty until the next breakpoint change. */
418
+ function fillLabels(context) {
419
+ var ctx = context || document;
420
+ var bp = last || current();
421
+ if (ctx.matches && ctx.matches('[data-fdy-breakpoint-label]')) ctx.textContent = bp;
422
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
413
423
  el.textContent = bp;
414
424
  });
415
- root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
416
425
  }
417
426
 
418
427
  window.addEventListener('resize', update, { passive: true });
@@ -423,6 +432,7 @@
423
432
  }
424
433
 
425
434
  window.FreedayBreakpoint = {
435
+ initAll: fillLabels,
426
436
  get: function () { return last || current(); },
427
437
  current: current,
428
438
  // true if the current breakpoint is `name` or wider (e.g. is('md') → md, lg, or xl)
@@ -1328,7 +1338,9 @@
1328
1338
  initTriggers();
1329
1339
  }
1330
1340
 
1331
- window.FreedayCfl = { init: initTriggers };
1341
+ /* initAll is what every stack helper looks for (useFreeday in Vue and React, FreedayBlazor.initAll):
1342
+ without it they skipped this enhancer, and raw CFL markup rendered by a framework never opened. */
1343
+ window.FreedayCfl = { init: initTriggers, initAll: initTriggers };
1332
1344
  })();
1333
1345
 
1334
1346
  /* Freeday, chart renderer (optional, zero-dependency, no external chart lib).
@@ -1845,8 +1857,10 @@
1845
1857
  /* Freeday, chip enhancer (optional, zero-dependency).
1846
1858
  * Two behaviours, both progressive enhancement:
1847
1859
  *
1848
- * 1. Removable chips, any .fdy-chip__remove button removes its chip and emits a
1849
- * bubbling "fdy-chip-remove" (detail {value}).
1860
+ * 1. Removable chips, any .fdy-chip__remove button emits a bubbling, CANCELABLE
1861
+ * "fdy-chip-remove" (detail {value}) and then removes its chip, unless a listener called
1862
+ * preventDefault(). Cancel it whenever the app owns the removal: a confirm step, or a chip
1863
+ * rendered from framework state, which should leave the DOM when the state does.
1850
1864
  * 2. Choice / filter groups, a [data-fdy-chips] container of interactive chips
1851
1865
  * (.fdy-chip--choice / .fdy-chip--filter, each a <button>) toggles aria-pressed
1852
1866
  * on click. Add data-single for one-at-a-time (radio-like) selection; filter
@@ -1872,8 +1886,12 @@
1872
1886
  if (rm) rm.remove();
1873
1887
  value = (clone.textContent || '').trim();
1874
1888
  }
1875
- chip.dispatchEvent(new CustomEvent('fdy-chip-remove', { bubbles: true, detail: { value: value } }));
1876
- chip.remove();
1889
+ /* Cancelable, because the kit cannot know whether this removal needs asking first. Removing
1890
+ unconditionally made a chip vanish while the app's own confirm dialog was still open, and
1891
+ stay gone after Cancel, with the data intact and the screen wrong (CATATAN-FREEDAY §9). */
1892
+ var ev = new CustomEvent('fdy-chip-remove', { bubbles: true, cancelable: true, detail: { value: value } });
1893
+ chip.dispatchEvent(ev);
1894
+ if (!ev.defaultPrevented) chip.remove();
1877
1895
  });
1878
1896
  }
1879
1897
 
@@ -5465,6 +5483,83 @@
5465
5483
  window.FreedayTree = { init: initTree, initAll: initAll };
5466
5484
  })();
5467
5485
 
5486
+ /* Freeday, un-hydrated hook warning (optional, zero-dependency, no markup of its own).
5487
+ * Every enhancer marks the element it took over (`data-fdy-*-ready`). Markup that no initAll()
5488
+ * ever reached carries the hook and not the mark, and it fails in the worst way available: it
5489
+ * renders, looks finished, and does nothing, with no error anywhere. A `data-fdy-validate` form
5490
+ * outside the subtree handed to useFreeday() neither paints a message nor fires fdy-form-valid,
5491
+ * so its save button reads as dead (CATATAN-FREEDAY §10). The enhancer knows the attribute is
5492
+ * there; the person who wrote it does not.
5493
+ *
5494
+ * So on the first click, focus or submit inside such an element this logs ONE console.warn per
5495
+ * element, naming the hook, with the element itself as the second argument so devtools links it.
5496
+ * Interaction, not load, is the trigger on purpose: a framework mounts and hydrates in between,
5497
+ * and only once a person is touching the widget is "not enhanced yet" the same as "never".
5498
+ *
5499
+ * HOOKS pairs each hook selector with the ready flag its enhancer stamps on THAT element and the
5500
+ * namespace that owns it. A hook whose enhancer is not loaded is skipped: that is a different
5501
+ * mistake, and this message would misname it. test/docs.test.mjs fails if an enhancer gains a
5502
+ * ready flag this table does not list.
5503
+ */
5504
+ (function () {
5505
+ 'use strict';
5506
+
5507
+ var HOOKS = [
5508
+ ['[data-fdy-app]', 'fdyAppShellReady', 'FreedayAppShell'],
5509
+ ['[data-fdy-autocomplete]', 'fdyAcReady', 'FreedayAutocomplete'],
5510
+ ['[data-fdy-carousel]', 'fdyCarouselReady', 'FreedayCarousel'],
5511
+ ['[data-fdy-cascade]', 'fdyCascadeReady', 'FreedayCascade'],
5512
+ ['[data-fdy-cfl]', 'fdyCflBound', 'FreedayCfl'],
5513
+ ['[data-fdy-cfl-open]', 'fdyCflBound', 'FreedayCfl'],
5514
+ ['[data-fdy-chart]', 'fdyChartReady', 'FreedayChart'],
5515
+ ['[data-fdy-chips]', 'fdyChipsReady', 'FreedayChip'],
5516
+ ['.fdy-chip__remove', 'fdyChipRmReady', 'FreedayChip'],
5517
+ ['[data-fdy-datepicker]', 'fdyDpReady', 'FreedayDatepicker'],
5518
+ ['[data-fdy-daterange]', 'fdyDrReady', 'FreedayDatepicker'],
5519
+ ['[data-fdy-datetimepicker]', 'fdyDtReady', 'FreedayDatetime'],
5520
+ ['[data-fdy-drawer]', 'fdyDrawerBound', 'FreedayDrawer'],
5521
+ ['.fdy-drawer', 'fdyDrawerReady', 'FreedayDrawer'],
5522
+ ['[data-fdy-validate]', 'fdyFormReady', 'FreedayForm'],
5523
+ ['[data-fdy-mask]', 'fdyMaskReady', 'FreedayMask'],
5524
+ ['[data-fdy-password]', 'fdyPwReady', 'FreedayMask'],
5525
+ ['[data-fdy-menu]', 'fdyMenuReady', 'FreedayMenu'],
5526
+ ['[data-fdy-number]', 'fdyNumberReady', 'FreedayNumber'],
5527
+ ['[data-fdy-rating]', 'fdyRatingReady', 'FreedayRating'],
5528
+ ['[data-fdy-search]', 'fdySearchReady', 'FreedaySearch'],
5529
+ ['[data-fdy-combo]', 'fdyComboReady', 'FreedayCombo'],
5530
+ ['[data-fdy-slider]', 'fdySliderReady', 'FreedaySlider'],
5531
+ ['[data-fdy-stepper]', 'fdyStepperReady', 'FreedayStepper'],
5532
+ ['[data-fdy-table]', 'fdyTableReady', 'FreedayTable'],
5533
+ ['[data-fdy-tabs]', 'fdyTabsReady', 'FreedayTabs'],
5534
+ ['[data-fdy-timepicker]', 'fdyTpReady', 'FreedayTimepicker'],
5535
+ ['[data-fdy-tree]', 'fdyTreeReady', 'FreedayTree'],
5536
+ ['[data-fdy-dropzone]', 'fdyUploadReady', 'FreedayUpload']
5537
+ ];
5538
+
5539
+ var warned = typeof WeakSet === 'function' ? new WeakSet() : null;
5540
+
5541
+ function check(e) {
5542
+ if (!warned || !e.target || !e.target.closest) return;
5543
+ for (var i = 0; i < HOOKS.length; i++) {
5544
+ var hook = HOOKS[i];
5545
+ if (!window[hook[2]]) continue;
5546
+ var el = e.target.closest(hook[0]);
5547
+ if (!el || el.dataset[hook[1]] === '1' || warned.has(el)) continue;
5548
+ warned.add(el);
5549
+ console.warn('Freeday: this ' + hook[0] + ' was never enhanced, so it does nothing. Either it is '
5550
+ + 'outside every initAll() scope (Vue/React: inside the element given to useFreeday(); Blazor: '
5551
+ + 'FreedayBlazor.initAll after render; anything else: ' + hook[2] + '.initAll(el) once it is in '
5552
+ + 'the DOM), or its markup lacks a part the enhancer needs (see COMPONENTS.md).', el);
5553
+ }
5554
+ }
5555
+
5556
+ ['click', 'focusin', 'submit'].forEach(function (type) {
5557
+ document.addEventListener(type, check, true);
5558
+ });
5559
+
5560
+ window.FreedayUnhydrated = { hooks: HOOKS };
5561
+ })();
5562
+
5468
5563
  /* Freeday, file-upload enhancer (optional, zero-dependency).
5469
5564
  * A drop target that also opens the file dialog on click / Enter / Space, validates each file
5470
5565
  * (accept + max size), and renders a per-file row with explicit state. Actual upload is the
@@ -5485,7 +5580,8 @@
5485
5580
  * Emits bubbling CustomEvents. BOTH on the dropzone element, which is the one target a consumer
5486
5581
  * needs to listen on:
5487
5582
  * "fdy-upload-add" {file, rejected, reason, row}
5488
- * "fdy-upload-remove" {file}
5583
+ * "fdy-upload-remove" {file}, cancelable: preventDefault() keeps the row (e.g. to confirm
5584
+ * removing a file the server already holds, then call row.el.remove())
5489
5585
  * `row` is the state machine over the rendered .fdy-file:
5490
5586
  * .ready() (rest, where a dropped file starts) / .uploading() / .setProgress(pct) /
5491
5587
  * .waiting(label) (sent, awaiting the server) / .done() / .fail(msg) / .el
@@ -5578,8 +5674,9 @@
5578
5674
  Firing on both would look safer, but it makes the pair asymmetric, one `add` and, for
5579
5675
  anyone delegating on a common ancestor, two `remove`s, which is its own silent bug. It
5580
5676
  also fixes the listless case: a row that was never attached bubbles to nothing at all. */
5581
- zone.dispatchEvent(new CustomEvent('fdy-upload-remove', { bubbles: true, detail: { file: file } }));
5582
- el.remove();
5677
+ var ev = new CustomEvent('fdy-upload-remove', { bubbles: true, cancelable: true, detail: { file: file } });
5678
+ zone.dispatchEvent(ev);
5679
+ if (!ev.defaultPrevented) el.remove();
5583
5680
  });
5584
5681
  el.appendChild(icon);
5585
5682
  el.appendChild(meta);
@@ -229,4 +229,5 @@ Order of work that avoids a half-migrated mess:
229
229
  - Keyboard: Tab reaches every control, focus is always visible, Esc closes overlays.
230
230
  - Form errors carry `aria-invalid` + a linked message; icon-only buttons have `aria-label`.
231
231
  - The interactive components on the screen have their enhancer loaded, and SPA-rendered DOM is
232
- re-hydrated.
232
+ re-hydrated. Click each one once with devtools open: a `Freeday: this [data-fdy-…] was never
233
+ enhanced` warning means it sits outside every `initAll()` scope.
@@ -198,7 +198,7 @@ live docs also have a copy button per component.
198
198
  ```bash
199
199
  npm i @cahyo-dimas/freeday
200
200
  ```
201
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.12.0"` (public npm package). `dist/` is
201
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.13.0"` (public npm package). `dist/` is
202
202
  committed and published → no build step; `npm ci` runs without auth.
203
203
 
204
204
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
@@ -40,8 +40,8 @@ Every integration comes down to one of these:
40
40
  | `freeday-mask` | `fdy-mask` `{value,raw}` | `FreedayMask` |
41
41
  | `freeday-form` | `fdy-form-invalid` `{invalid}` / `fdy-form-valid` | `FreedayForm` |
42
42
  | `freeday-table` | `fdy-table-change` · `fdy-row-select` | `FreedayTable` |
43
- | `freeday-chip` | `fdy-chip-change` `{value,pressed,selected}` · `fdy-chip-remove` `{value}` | `FreedayChip` |
44
- | `freeday-upload` | `fdy-upload-add` / `fdy-upload-remove` | `FreedayUpload` |
43
+ | `freeday-chip` | `fdy-chip-change` `{value,pressed,selected}` · `fdy-chip-remove` `{value}`, cancelable | `FreedayChip` |
44
+ | `freeday-upload` | `fdy-upload-add` / `fdy-upload-remove` (cancelable) | `FreedayUpload` |
45
45
  | `freeday-stepper` | `fdy-step-change` | `FreedayStepper` |
46
46
  | `freeday-carousel` | `fdy-carousel-change` | `FreedayCarousel` |
47
47
  | `freeday-breakpoint` | `fdy-breakpoint-change` | `FreedayBreakpoint` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -96,7 +96,7 @@
96
96
  "scripts": {
97
97
  "build": "node tokens/build.mjs",
98
98
  "test": "node --test",
99
- "test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs 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/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.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/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/timepicker.mjs browser/form-kit-required.mjs browser/harness-contract.mjs",
99
+ "test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs 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/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.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/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/timepicker.mjs browser/form-kit-required.mjs browser/picker-in-modal.mjs browser/harness-contract.mjs browser/app-owned.mjs",
100
100
  "prepack": "node tokens/build.mjs",
101
101
  "version": "node tokens/build.mjs && git add dist",
102
102
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",
@@ -4,10 +4,10 @@
4
4
  /* display is gated behind [open] so the native <dialog> stays hidden until opened.
5
5
  * An unconditional display here would beat the UA `dialog:not([open]){display:none}`
6
6
  * rule (author origin wins over user-agent) and leave the drawer visible on load. */
7
- .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition:transform var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
7
+ .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
8
8
  .fdy-drawer[open]{display:flex;transform:none;}
9
9
  .fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
10
- .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
10
+ .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
11
11
  .fdy-drawer__header{flex:none;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
12
12
  .fdy-drawer__title{font-family:var(--font-display);font-weight:var(--weight-semibold);font-size:var(--text-lg);color:var(--color-text);margin:0 auto 0 0;}
13
13
  .fdy-drawer__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
@@ -56,4 +56,5 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
56
56
  remember to mark aria-hidden, and it cannot be forgotten. */
57
57
  .fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
58
58
  .fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
59
- .fdy-help--error{color:var(--color-danger);}
59
+ /* --danger is the tone word everywhere else in the kit; help text takes either (CATATAN-FREEDAY §11). */
60
+ .fdy-help--error,.fdy-help--danger{color:var(--color-danger);}
@@ -1,10 +1,10 @@
1
1
  /* Freeday. Modal (native <dialog>) */
2
- .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);transition:opacity var(--dur-base) var(--ease-spring),transform var(--dur-base) var(--ease-spring),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
2
+ .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
3
3
  .fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
4
4
  .fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
5
5
  .fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
6
6
  .fdy-modal--wide{width:min(60rem,calc(100vw - var(--space-8)));}
7
- .fdy-modal::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-base) var(--ease-standard),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
7
+ .fdy-modal::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
8
8
  .fdy-modal__header{flex:none;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
9
9
  .fdy-modal__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;}
10
10
  .fdy-modal__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}