@cahyo-dimas/freeday 3.12.1 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +133 -9
  2. package/COMPONENTS.md +76 -1
  3. package/README.id.md +1 -1
  4. package/README.md +1 -1
  5. package/adapters/react/components/FdyDrawer.tsx +4 -0
  6. package/adapters/react/index.d.ts +2 -0
  7. package/adapters/vue/components/FdyDrawer.vue +4 -0
  8. package/adapters/vue/index.d.ts +2 -0
  9. package/dist/freeday-app-shell.js +1 -0
  10. package/dist/freeday-autocomplete.js +1 -0
  11. package/dist/freeday-breakpoint.js +12 -2
  12. package/dist/freeday-carousel.js +1 -0
  13. package/dist/freeday-cascade.js +1 -0
  14. package/dist/freeday-cfl.js +5 -1
  15. package/dist/freeday-chart.js +1 -0
  16. package/dist/freeday-chip.js +12 -4
  17. package/dist/freeday-datepicker.js +2 -0
  18. package/dist/freeday-datetime.js +1 -0
  19. package/dist/freeday-drawer.js +2 -0
  20. package/dist/freeday-form.js +1 -0
  21. package/dist/freeday-mask.js +2 -0
  22. package/dist/freeday-menu.js +1 -0
  23. package/dist/freeday-number.js +1 -0
  24. package/dist/freeday-rating.js +1 -0
  25. package/dist/freeday-search.js +1 -0
  26. package/dist/freeday-select.js +1 -0
  27. package/dist/freeday-slider.js +1 -0
  28. package/dist/freeday-sortable.js +222 -0
  29. package/dist/freeday-stepper.js +1 -0
  30. package/dist/freeday-table.js +1 -0
  31. package/dist/freeday-tabs.js +1 -0
  32. package/dist/freeday-timepicker.js +1 -0
  33. package/dist/freeday-tree.js +1 -0
  34. package/dist/freeday-unhydrated.js +83 -0
  35. package/dist/freeday-upload.js +6 -3
  36. package/dist/freeday.bundle.css +20 -1
  37. package/dist/freeday.css +20 -1
  38. package/dist/freeday.js +366 -10
  39. package/docs/agent-onboarding.md +4 -1
  40. package/docs/getting-started.md +1 -1
  41. package/docs/integrations.md +3 -2
  42. package/package.json +2 -2
  43. package/src/components/input.css +2 -1
  44. package/src/components/sortable.css +17 -0
package/CHANGELOG.md CHANGED
@@ -9,19 +9,143 @@ 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.14.0] - 2026-10-29
13
+
14
+ **Daftar terurut lewat seret dan papan ketik, dan satu atribut untuk berkata "ini bukan urusan
15
+ kit".** Dua butir dari `CATATAN-FREEDAY.md` app ronda (§14 dan §15). Mengaudit §14 menemukan satu
16
+ cacat yang tak dilaporkan: `FdyDrawer` Vue dan React bisa ditutup lewat backdrop walau
17
+ `dismissible=false`.
18
+
19
+ ### Added
20
+ - **`data-fdy-sortable`, enhancer ke-32** (`#103`), `src/freeday-sortable.js` plus
21
+ `.fdy-sortable__handle`. Baris diseret lewat pegangannya, atau dipindah satu langkah dengan ↑ / ↓
22
+ saat pegangannya terfokus. Lima layar di app ronda menyuruh orang mengetik nomor urut di kotak
23
+ angka, dan app itu akhirnya menulis sendiri SortableJS plus jalur papan ketiknya. Selama diseret,
24
+ garis menunjukkan tempat baris akan mendarat dan tak ada elemen yang berpindah sebelum tombol
25
+ dilepas; Escape membatalkan. `fdy-sort-change` (`{key, from, to, order}`) dapat dibatalkan,
26
+ dengan pola yang sama seperti chip di 3.13.0: halaman statis membiarkannya dan kit memindah
27
+ elemennya, sedangkan framework membatalkannya lalu mengurutkan state-nya sendiri, sebab DOM-nya
28
+ milik framework. Sesudah langkah papan ketik, fokus kembali ke pegangan baris yang sama, dicari
29
+ lewat `data-key` supaya tetap ketemu walau render ulang mengganti elemennya. Perpindahan
30
+ diumumkan di live region. `data-fdy-sort-fixed` menandai baris yang tak bisa dilewati, dan
31
+ `data-disabled` mematikan pengurutan selama daftar dicari atau disaring. Yang sengaja tidak
32
+ dibuat: gulir otomatis saat seretan mendekati tepi halaman, dan memindah baris antar-daftar.
33
+ - **`data-fdy-ignore`** (`#102`). Di elemen atau leluhur mana pun, setiap enhancer melewati
34
+ subtree itu, termasuk di bawah `initAll(document)` yang eksplisit, dan peringatan hook dari 3.13.0
35
+ ikut diam. Pesan peringatannya kini menyebut atribut ini sebagai jalan keluar yang sah. Pemicunya
36
+ matriks chip di layar Pola rotasi, yang perilakunya dikelola app sendiri, sehingga setiap klik di
37
+ situ mencetak peringatan untuk sesuatu yang memang disengaja. Nama yang diusulkan catatannya,
38
+ `data-fdy-raw`, tidak dipakai: di dokumen kit "raw" berarti jalur markup + enhancer, kebalikan
39
+ dari maksud atribut ini.
40
+
41
+ ### Fixed
42
+ - **`FdyDrawer` Vue dan React tertutup lewat backdrop walau `dismissible=false`** (`#102`), begitu
43
+ `useFreeday()` dipasang di seluruh halaman. Wrapper itu merender `.fdy-drawer`, yang merupakan hook
44
+ enhancer drawer, sehingga enhancer memasang handler backdrop kedua yang memanggil `close()`
45
+ langsung. Dialognya tertutup, `open` di state app tetap `true`, dan tak ada event `close`. Di luar
46
+ cakupan pemindai, wrapper yang sama memicu peringatan 3.13.0 setiap kali diklik. Keduanya kini
47
+ membawa `data-fdy-ignore`. `FdyChart` juga merender hook, tetapi sejak awal sudah mencap penanda
48
+ siapnya sendiri; ia kini dijaga uji supaya tetap begitu.
49
+
50
+ ### Catatan
51
+ - **Angka 9 menit di §14 bukan ongkos pemindaian.** Catatannya mengukur `useFreeday()` di seluruh
52
+ halaman pada 9 menit 6 detik untuk sepuluh uji e2e, dibanding 19,1 detik untuk cakupan modal saja,
53
+ dan mempersempit cakupannya karena itu. Pada bentuk matriks yang sama (38 baris × 4 shift, dua chip
54
+ per sel, 304 tombol), satu `rehydrate()` di seluruh halaman memakan 1,1 ms dan satu update reaktif
55
+ end-to-end 2,1 ms. Sekitar 55 detik per uji lebih mirip uji yang menunggu sampai timeout. Kalau
56
+ angka itu diambil sebelum 3.13.0, waktu itu kit masih menghapus chip yang diklik, dan itu cukup
57
+ menjelaskannya. Ini dugaan; pengukuran ulangnya ada di sisi app.
58
+
59
+ ### Gerbang
60
+ - `docs.test.mjs`: setiap fungsi init yang mencap penanda hook wajib memeriksa `data-fdy-ignore`
61
+ sebelum mencap, dan setiap wrapper Vue/React yang merender hook enhancer wajib menandainya
62
+ `data-fdy-ignore` atau mencap penanda siapnya sendiri. Snapshot string enhancer mendapat dua
63
+ string sortable.
64
+ - `browser/app-owned.mjs`: `useFreeday()` di seluruh halaman atas matriks itu, dengan ambang longgar
65
+ (25 ms per rehydrate, 50 ms per update) yang menangkap pemindaian yang meledak, bukan runner yang
66
+ lambat. Drawer `dismissible=false` bertahan dari klik backdrop, chart ter-render sekali, subtree
67
+ yang diabaikan tetap tak tersentuh, dan tak ada satu pun peringatan.
68
+ - `browser/sortable.mjs`, dengan mouse dan papan ketik asli: tanda pendaratan di tengah seretan,
69
+ Escape, klik yang bukan seretan, baris tetap, keadaan nonaktif, perubahan yang dibatalkan tak
70
+ menyentuh DOM, dan list Vue ber-key yang mengurutkan state-nya sendiri.
71
+ - `browser/harness.mjs` mendapat `dragFromTo` dan mengekspos `clickAt`.
72
+ - Dua belas mutasi diverifikasi merah. Satu mutasi yang tetap hijau membuang ambang jarak seret dari
73
+ enhancer: tekanan yang tak meninggalkan barisnya sendiri sudah ditolak `allowed()`.
74
+
75
+ ## [3.13.0] - 2026-10-29
76
+
77
+ **Kit berhenti memutuskan hal yang bukan urusannya, dan markup yang tak pernah dihidrasi kini
78
+ bersuara.** Lima butir dari `CATATAN-FREEDAY.md` app ronda (§9 sampai §13), dikerjakan dalam satu
79
+ rilis. Tiga di antaranya gagal tanpa galat apa pun: chip yang hilang sesudah Batal, tombol simpan
80
+ yang mati, dan kelas yang tak cocok dengan aturan mana pun.
81
+
82
+ ### Changed
83
+ - **`fdy-chip-remove` dan `fdy-upload-remove` kini dapat dibatalkan** (`#098`). Begitu
84
+ `.fdy-chip__remove` ditekan, enhancer chip langsung menghapus chip-nya, padahal kit tak tahu
85
+ apakah penghapusan itu perlu ditanyakan dulu. Matriks pola rotasi di app ronda memakai chip
86
+ sebagai tampilan dan punya dialog konfirmasi sendiri. Chip-nya hilang ketika dialog masih
87
+ terbuka, dan tetap hilang sesudah **Batal** ditekan. Datanya utuh, layarnya yang salah. Sekarang
88
+ event-nya `cancelable`, dan elemennya baru dihapus kalau tak ada listener yang memanggil
89
+ `preventDefault()`. Perubahan ini aditif: app yang tidak membatalkan mendapat perilaku yang persis
90
+ sama. Baris upload ikut diubah karena bentuknya sama, dan berkas yang sudah dipegang server
91
+ memang layak ditanyakan dulu sebelum dibuang.
92
+
93
+ ### Added
94
+ - **Peringatan konsol untuk hook yang tak pernah dihidrasi** (`#099`): `src/freeday-unhydrated.js`,
95
+ sudah termasuk di import akar. Klik, fokus atau submit pertama di dalam elemen ber-hook yang tak
96
+ punya penanda siap mencetak satu `console.warn` per elemen. Pesannya menyebut hook-nya, dan
97
+ elemennya ikut sebagai argumen kedua supaya devtools menautkannya. Pemicunya §10: `useFreeday()`
98
+ yang dipasang di pembungkus yang salah membuat `data-fdy-validate` di luar pembungkus itu diam
99
+ total, tanpa kalimat validasi dan tanpa `fdy-form-valid`. Yang memberi tahu cuma dua uji e2e yang
100
+ merah. Catatannya meminta peringatan ini untuk form, tetapi kegagalannya berlaku untuk setiap
101
+ hook, jadi tabelnya memuat 29 hook dari 24 enhancer. Peringatan dipicu interaksi, bukan saat
102
+ halaman dimuat, karena framework me-mount dan menghidrasi di antara keduanya.
103
+ - **`fdy-help--danger`** (`#100`), alias `fdy-help--error` di dalam aturan yang sama. Kit memakai
104
+ `--danger` sebagai kata nada di mana-mana (tombol, alert, lencana, toast), sehingga app menebak
105
+ `fdy-help--danger`. Kelas itu tak cocok dengan aturan apa pun, dan kalimat peringatannya tampil
106
+ abu-abu. Kontrol form sengaja tidak diberi alias. Keadaannya digerakkan `aria-invalid`, jadi
107
+ `--error` di sana jarang ditulis tangan. Aturan penamaannya kini tertulis di COMPONENTS.md.
108
+
109
+ ### Fixed
110
+ - **`FreedayCfl` dan `FreedayBreakpoint` tak punya `initAll`** (`#099`), padahal hanya nama itu
111
+ yang dicari ketiga helper stack (`useFreeday` di Vue dan React, `FreedayBlazor.initAll`). Markup
112
+ CFL mentah di dalam subtree framework dilewati ketiganya dan dialognya tak pernah terbuka. Label
113
+ breakpoint yang di-mount belakangan tetap kosong sampai breakpoint berganti. Tak ada yang
114
+ melaporkan ini; keduanya ketemu saat mengaudit kelas cacat §10.
115
+
116
+ ### Docs
117
+ - **`<ul>` polos adalah daftar berbutir yang didukung** (`#101`). `base.css` memang tak pernah
118
+ mereset daftar. Sekarang COMPONENTS.md menjanjikannya, dan sebuah uji menjaga janji itu.
119
+ - §13 (`required` di `FdyCombo`) sudah terkirim di 3.12.0 (`#096`). Tak ada kode baru; app tinggal
120
+ memakainya.
121
+
122
+ ### Gerbang
123
+ - `docs.test.mjs`: setiap penanda siap yang dicap enhancer wajib ada di tabel peringatan, dicap di
124
+ berkas milik namespace yang disebut tabel, dan berkas itu wajib benar-benar meng-query selektor
125
+ pasangannya. Setiap enhancer yang menghidrasi markup wajib mengekspos `initAll`. Penjaga string
126
+ kini melewati teks `console.warn` (untuk devtools, tak pernah masuk DOM) dan identifier
127
+ PascalCase.
128
+ - `css.test.mjs`: `fdy-help--danger` harus satu aturan dengan `--error`, dan tak boleh ada aturan
129
+ tanpa cakupan yang mereset `ul`/`ol`/`li`.
130
+ - `browser/app-owned.mjs`, dengan mouse asli: chip dan baris upload yang dibatalkan tetap di
131
+ tempatnya, form yatim memperingatkan sekali sementara form yang terhidrasi diam, dan CFL serta
132
+ label breakpoint yang di-mount belakangan terhidrasi lewat `FreedayBlazor.initAll`.
133
+ - Tiga belas mutasi diverifikasi merah sebelum dipulihkan: enam pada perilaku, lima pada tabel dan
134
+ ekspor, dua pada CSS.
135
+
12
136
  ## [3.12.1] - 2026-10-27
13
137
 
14
138
  **Modal dan drawer kembali beranimasi di bawah Safari 17.4, termasuk di iPhone 7 pemiliknya.**
15
139
 
16
140
  ### Fixed
17
141
  - **Empat shorthand `transition` dipecah jadi longhand** (`#097`), di `modal.css` dan `drawer.css`.
18
- Shorthand yang membawa satu keyword yang tak dikenal mesin **tidak sah saat parse**, jadi SELURUH
19
- deklarasinya dibuang -- dan `allow-discrete` tak dikenal di bawah Safari 17.4, yang ada di bawah
142
+ Shorthand yang membawa satu keyword yang tak dikenal mesin **tidak sah saat parse**, jadi seluruh
143
+ deklarasinya dibuang, dan `allow-discrete` tak dikenal di bawah Safari 17.4, yang ada di bawah
20
144
  lantai dukungan kit. Akibatnya modal dan drawer buka-tutup tanpa animasi apa pun. Dipecah begini,
21
145
  yang hilang di bawah lantai hanya `transition-behavior`, dan paruh opacity/transform-nya bertahan.
22
146
  `overlay` dan `display` tetap terdaftar di `transition-property`, tempat nama yang tak cocok tetap
23
147
  sah secara sintaks dan sekadar tak pernah cocok, jadi tak ada lagi yang ikut jatuh. Keluarnya
24
- tetap seketika di sana sebab `display` memang tak dapat ditransisikan di bawah lantai -- sama
148
+ tetap seketika di sana sebab `display` memang tak dapat ditransisikan di bawah lantai, sama
25
149
  seperti sebelumnya, jadi pemecahan ini tidak mengambil apa pun.
26
150
 
27
151
  Ekuivalensinya di atas lantai **diukur, bukan dinalar**: `transition-property`,
@@ -94,7 +218,7 @@ perbaikan ini terdengar jauh lebih mahal daripada yang ternyata.
94
218
 
95
219
  **Audit kontrol native di seluruh layar sebuah aplikasi konsumen, bukan di satu layar yang
96
220
  mengeluh.** Pemiliknya melaporkan tampilan HP aslinya tak cocok dengan peramban di lebar yang sama,
97
- dan dugaan pertama -- termasuk dugaan saya -- adalah CSS. Pengukuran menyangkalnya: dengan
221
+ dan dugaan pertama, termasuk dugaan saya, adalah CSS. Pengukuran menyangkalnya: dengan
98
222
  stylesheet aplikasi yang sebenarnya, pada 375x667, Chromium dan WebKit menata `date`, `time`,
99
223
  `number`, `search` dan `text` **identik**, 293x44 keenamnya, selisih satu piksel pada `y` yang
100
224
  cuma pembulatan. Jadi yang berbeda bukan tata letaknya, melainkan **widget native-nya**: `date` dan
@@ -106,7 +230,7 @@ layar yang selebihnya sama.
106
230
  Kit sudah mengatakan ini di tabelnya sendiri ("Use `.fdy-datepicker` / `.fdy-timepicker` /
107
231
  `.fdy-datetimepicker` instead") dan sudah mengirim CSS plus enhancer-nya. Yang belum ada adalah
108
232
  wrapper bertipe untuk timepicker, jadi aplikasi Vue/React/Blazor harus merakit sendiri
109
- plumbing custom-event-nya -- dan merakit sendiri adalah yang membuat empat `<input type="time">`
233
+ plumbing custom-event-nya, dan merakit sendiri adalah yang membuat empat `<input type="time">`
110
234
  bertahan di satu formulir shift.
111
235
 
112
236
  Catatan kejujuran: perbedaan iOS itu **tidak** direproduksi di sini. WebKit desktop bukan Mobile
@@ -116,8 +240,8 @@ lokal, dan kini diuji, adalah perilaku penggantinya.
116
240
 
117
241
  ### Added
118
242
  - **`FdyTimepicker`** (`#094`), wrapper bertipe di **ketiga** stack, menjadikannya tiga belas.
119
- Vue dan React mengimplementasikan listbox-nya secara native sebab ia kontrol form -- alasan yang
120
- sama dengan `FdyCombo`, dan alasan yang `FdyChart` nyatakan dari sisi sebaliknya: enhancer yang
243
+ Vue dan React mengimplementasikan listbox-nya secara native sebab ia kontrol form. Alasannya sama
244
+ dengan `FdyCombo`, dan `FdyChart` menyatakannya dari sisi sebaliknya: enhancer yang
121
245
  menguasai DOM berkelahi dengan render framework dan tak menyisakan `v-model`. Blazor
122
246
  menjembatani enhancer lewat seed, seperti `FdyDatepicker`. Sorotannya **dijepit, bukan
123
247
  berputar**: pada daftar 48 opsi, ArrowUp di `00:00` yang melompat ke `23:30` terbaca sebagai
@@ -126,7 +250,7 @@ lokal, dan kini diuji, adalah perilaku penggantinya.
126
250
  - **`data-id` dan `data-describedby` pada seed timepicker** (`#094`). Keduanya tak terbaca
127
251
  sebelumnya, jadi host yang menampilkan teks galat tak punya cara menautkannya. Pemicunya
128
252
  `<button>`, yang **bukan** elemen labelable, sehingga `<label for>` memang tak akan pernah
129
- mengaktifkannya -- itu didokumentasikan sekarang, bukan dibiarkan jadi kejutan.
253
+ mengaktifkannya, dan itu kini didokumentasikan.
130
254
  - **`FreedayTimepicker.setState(root, { disabled, readonly, invalid })`** (`#094`), bentuknya sama
131
255
  dengan `FreedayDatepicker.setState`. Tanpa ini, host yang merender seed-nya sekali lalu tidak
132
256
  pernah lagi (Blazor, `ShouldRender => false`) memiliki tiga parameter keadaan yang tak ada yang
@@ -136,7 +260,7 @@ lokal, dan kini diuji, adalah perilaku penggantinya.
136
260
  - **`FdyDateRange` membuang kedelapan prop label navigasi kalender** (`#095`), di ketiga stack.
137
261
  `FdyDatepicker` menerimanya sejak 2.1.1; rentangnya tidak meneruskannya, sehingga rentang tanggal
138
262
  adalah **satu-satunya** kontrol tanggal di kit yang navigasinya tak bisa dinamai dalam bahasa
139
- halamannya sendiri -- aplikasi berbahasa Indonesia mendapat "Previous month" tepat di tempat yang
263
+ halamannya sendiri: aplikasi berbahasa Indonesia mendapat "Previous month" tepat di tempat yang
140
264
  tak dapat ia jangkau. Diteruskan apa adanya ke kedua pemilih. Prop datar `xxxLabel`, bukan objek
141
265
  `labels`, sebab bentuk itulah yang sudah dipakai setiap komponen lain di kit. Blazor menambah
142
266
  `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,15 @@ 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.
55
+ **`data-fdy-ignore` says "not yours".** On an element or any ancestor, every enhancer skips that
56
+ subtree, even under an explicit `initAll(document)`, and the warning above stays quiet. Use it
57
+ where the app drives kit-styled markup itself, such as a chip matrix with its own removal flow.
58
+ The Vue and React `FdyDrawer` carry it already, so a page-wide `useFreeday()` cannot hand the
59
+ drawer enhancer a second backdrop handler.
47
60
 
48
61
  ### Containment — why the kit's containers are `position:relative`
49
62
 
@@ -93,6 +106,7 @@ element is fine. Events are bubbling
93
106
  | `data-fdy-tree` | `freeday-tree.js` | `FreedayTree` | — |
94
107
  | `data-fdy-rating` | `freeday-rating.js` | `FreedayRating` | — (native `change`) |
95
108
  | `data-fdy-slider` | `freeday-slider.js` | `FreedaySlider` | — (native `input`) |
109
+ | `data-fdy-sortable` | `freeday-sortable.js` | `FreedaySortable` | `fdy-sort-change` |
96
110
  | `data-fdy-number` | `freeday-number.js` | `FreedayNumber` | — (native `input`, `change`) |
97
111
  | `data-fdy-search` | `freeday-search.js` | `FreedaySearch` | `fdy-search` (+ native `input` on clear) |
98
112
  | `data-fdy-copy="value"` (delegated, no hydration) | `freeday-copy.js` | `FreedayCopy` | `fdy-copy` |
@@ -562,7 +576,7 @@ native control, otherwise a `<div>` + explicitly associated label.
562
576
  `.fdy-field--full` on the fields — **not** a `max-width` override on the container. Both of this
563
577
  kit's own consoles wrote that override, once per form, before this modifier reached outside the
564
578
  grid.
565
- - `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
579
+ - `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`, or its alias `--danger`)
566
580
 
567
581
  **Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
568
582
  `.fdy-label` on the legend. The kit already resets the UA border/padding and supplies the spacing:
@@ -1214,6 +1228,9 @@ zone.addEventListener('fdy-upload-add', (e) => {
1214
1228
  zone.addEventListener('fdy-upload-remove', (e) => forget(e.detail.file)); // same element
1215
1229
  ```
1216
1230
 
1231
+ `fdy-upload-remove` is **cancelable** too: `preventDefault()` keeps the row, for a file the server
1232
+ already holds and the user should confirm losing. Remove it yourself afterwards with `row.el.remove()`.
1233
+
1217
1234
  **Bring your own row:** omit the file list entirely (no `data-filelist`, and no `.fdy-filelist`
1218
1235
  sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add`, where `detail.row`
1219
1236
  still works, its element simply isn't attached. Note the fallback when `data-filelist` is absent is
@@ -1757,6 +1774,10 @@ responsive `.fdy-datatable` should become below `md`, rather than a stack of `.f
1757
1774
  control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
1758
1775
  reads as clickable while it is refusing input.
1759
1776
  - Not `.fdy-list-reset`, which only strips UA bullets/indent from a semantic list.
1777
+ - Not for three bullet points in a dialog either. **A plain bulleted list is a plain `<ul>`/`<ol>`,
1778
+ no class**, and that is supported: `base.css` deliberately leaves lists alone, so native bullets
1779
+ and indent survive (a test keeps it that way). The exception is a utility framework run with its
1780
+ preflight on, which strips them.
1760
1781
 
1761
1782
  ```html
1762
1783
  <ul class="fdy-list">
@@ -2335,8 +2356,62 @@ Three roles inside a `.fdy-chips` row:
2335
2356
 
2336
2357
  Interactive chips are managed by `freeday-chip.js`: wrap the row in `data-fdy-chips` (+
2337
2358
  `data-single` for pick-one, `data-label` for the group name), give each `data-value`.
2359
+
2360
+ **Removal is cancelable.** Pressing `.fdy-chip__remove` fires `fdy-chip-remove` (`detail.value`) on
2361
+ the chip, then removes the chip only if no listener called `preventDefault()`. Cancel it whenever
2362
+ the removal is the app's call: a confirm step, or chips rendered from framework state, which
2363
+ should leave the DOM when the state changes and not before. Without that, Cancel in your own
2364
+ confirm dialog leaves the data intact and the chip gone.
2365
+
2366
+ ```js
2367
+ row.addEventListener('fdy-chip-remove', async (e) => {
2368
+ e.preventDefault(); // the kit leaves the chip alone
2369
+ if (await Freeday.confirm({ title: `Remove ${e.detail.value}?`, tone: 'danger' })) {
2370
+ removeFromState(e.detail.value); // your render takes it out
2371
+ }
2372
+ });
2373
+ ```
2338
2374
  `--tone-1`…`--tone-8` for non-semantic category colours.
2339
2375
 
2376
+ ## Sortable list — `data-fdy-sortable`
2377
+ Reorder a list by dragging a handle, or by focusing the handle and pressing ↑ / ↓. Needs
2378
+ `freeday-sortable.js`. The kit draws the handle and the drag states; the list keeps whatever look it
2379
+ already had (`.fdy-list`, a table body, plain rows).
2380
+
2381
+ - Container: `.fdy-sortable` + `data-fdy-sortable` on a `<ul>`/`<ol>`, a `<tbody>` or a `<div>`.
2382
+ Its element children are the items.
2383
+ - Item: `data-key` (the id the event reports, **required** when a framework renders the list),
2384
+ `data-label` (the name read out, the item's text otherwise), and `data-fdy-sort-fixed` for an item
2385
+ that never moves and that nothing can pass, such as a system row pinned to one end.
2386
+ - Handle: `<button class="fdy-sortable__handle" type="button">` inside the item, icon inside. With no
2387
+ `aria-label` of your own it gets *Move {label}. Drag, or press the up and down arrow keys.*
2388
+ - `data-disabled` on the container switches reordering off, and the handles follow through
2389
+ `aria-disabled`. Set it while the list is searched or filtered, where a row's visible position is
2390
+ not its stored one.
2391
+ - States, set by the enhancer: `is-sorting` on the container, `is-dragging` on the item, and
2392
+ `is-drop-before` / `is-drop-after` on the item it will land against. Nothing moves until the
2393
+ pointer is released, and Escape cancels.
2394
+ - Event: `fdy-sort-change` on the container, `detail {key, from, to, order}`, **cancelable**. Left
2395
+ alone, the kit moves the element. Under Vue, React or Blazor, cancel it and reorder your state,
2396
+ because the DOM is the framework's. Either way the kit announces *{label} moved to position {n} of
2397
+ {total}.* in a polite live region, and after a keyboard step it puts focus back on the moved
2398
+ item's handle, found by `data-key` so a re-render that replaced the element still gets it.
2399
+ - Strings: `data-fdy-text-handle`, `data-fdy-text-moved` on the container.
2400
+ - Not in the kit: scrolling the page while a drag nears its edge, and moving items between lists.
2401
+ Keep a sortable list short enough to see whole, or paginate it.
2402
+
2403
+ ```vue
2404
+ <ul ref="list" class="fdy-list fdy-sortable" data-fdy-sortable
2405
+ :data-disabled="filtered ? '' : undefined"
2406
+ @fdy-sort-change.prevent="(e) => saveOrder(e.detail.order)">
2407
+ <li v-for="r in rows" :key="r.id" class="fdy-list__row" :data-key="r.id" :data-label="r.name">
2408
+ <button class="fdy-sortable__handle" type="button"><IconGrip class="fdy-icon" aria-hidden="true" /></button>
2409
+ {{ r.name }}
2410
+ </li>
2411
+ </ul>
2412
+ <!-- useFreeday(list) in <script setup> -->
2413
+ ```
2414
+
2340
2415
  ## Timeline — `.fdy-timeline`
2341
2416
  Vertical event feed, pure CSS. `<ol class="fdy-timeline">` of `.fdy-timeline__item` (`--muted`
2342
2417
  `--success` `--warning` `--danger`) with `__marker` `__time` `__title` `__text`. Status is carried
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.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.12.1)
8
+ [![Release](https://img.shields.io/badge/release-v3.14.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.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.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.12.1)
8
+ [![Release](https://img.shields.io/badge/release-v3.14.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.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`.
@@ -7,6 +7,9 @@ import { useEffect, useId, useRef } from 'react';
7
7
  // through app state, backdrop-click via `event.target === dialogEl`) applied to a drawer that anchors
8
8
  // left (default) or right. Native <dialog> supplies the focus trap, focus restore, top-layer stacking
9
9
  // and inert background; `dismissible` (default true) gates Esc + backdrop dismissal.
10
+ // `data-fdy-ignore` on the root: this component owns the behaviour, so a page-wide useFreeday() must
11
+ // not hand `.fdy-drawer` to the enhancer as well. Its backdrop handler calls close() past
12
+ // `dismissible` and leaves `open` true on a dialog that is already closed.
10
13
 
11
14
  export interface FdyDrawerProps {
12
15
  open: boolean;
@@ -37,6 +40,7 @@ export function FdyDrawer(props: FdyDrawerProps): JSX.Element {
37
40
  <dialog
38
41
  ref={dialogRef}
39
42
  className={drawerClass}
43
+ data-fdy-ignore=""
40
44
  aria-labelledby={titleId}
41
45
  onCancel={(e): void => {
42
46
  e.preventDefault();
@@ -23,6 +23,8 @@ export interface FdyMaskDetail { value: string; raw: string }
23
23
  export interface FdyFormInvalidDetail { invalid: HTMLElement[] }
24
24
  export interface FdyChipChangeDetail { value: string; pressed: boolean; selected: string[] }
25
25
  export interface FdyChipRemoveDetail { value: string }
26
+ /** `fdy-sort-change`, cancelable: `order` is every item's `data-key` in the new order. */
27
+ export interface FdySortChangeDetail { key: string; from: number; to: number; order: string[] }
26
28
  export interface FdyCflSelectDetail { row?: unknown; rows?: unknown[] }
27
29
  export interface FdyTableChangeDetail { page: number; sort: string | null; query: string }
28
30
 
@@ -7,6 +7,9 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
7
7
  // dialogEl`, applied to a drawer that anchors left (default) or right. Native <dialog> supplies the
8
8
  // focus trap, focus restore, top-layer stacking and inert background; `dismissible` (default true)
9
9
  // gates Esc + backdrop dismissal.
10
+ // `data-fdy-ignore` on the root: this component owns the behaviour, so a page-wide useFreeday() must
11
+ // not hand `.fdy-drawer` to the enhancer as well. Its backdrop handler calls close() past
12
+ // `dismissible` and leaves `open` true on a dialog that is already closed.
10
13
 
11
14
  // dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
12
15
  // `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
@@ -53,6 +56,7 @@ function onClick(e: MouseEvent): void {
53
56
  <dialog
54
57
  ref="dialogEl"
55
58
  :class="drawerClass"
59
+ data-fdy-ignore
56
60
  :aria-labelledby="titleId"
57
61
  @cancel.prevent="onCancel"
58
62
  @click="onClick"
@@ -93,5 +93,7 @@ export interface FdyMaskDetail { value: string; raw: string }
93
93
  export interface FdyFormInvalidDetail { invalid: HTMLElement[] }
94
94
  export interface FdyChipChangeDetail { value: string; pressed: boolean; selected: string[] }
95
95
  export interface FdyChipRemoveDetail { value: string }
96
+ /** `fdy-sort-change`, cancelable: `order` is every item's `data-key` in the new order. */
97
+ export interface FdySortChangeDetail { key: string; from: number; to: number; order: string[] }
96
98
  export interface FdyCflSelectDetail { row?: unknown; rows?: unknown[] }
97
99
  export interface FdyTableChangeDetail { page: number; sort: string | null; query: string }
@@ -46,6 +46,7 @@
46
46
 
47
47
  function initShell(app) {
48
48
  if (app.dataset.fdyAppShellReady === '1') return;
49
+ if (app.closest('[data-fdy-ignore]')) return;
49
50
  app.dataset.fdyAppShellReady = '1';
50
51
 
51
52
  var toggle = app.querySelector('.fdy-app__navtoggle');
@@ -18,6 +18,7 @@
18
18
 
19
19
  function initAutocomplete(root) {
20
20
  if (root.dataset.fdyAcReady === '1') return;
21
+ if (root.closest('[data-fdy-ignore]')) return;
21
22
  root.dataset.fdyAcReady = '1';
22
23
 
23
24
  var input = root.querySelector('input[role="combobox"]') || root.querySelector('input');
@@ -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)
@@ -46,6 +46,7 @@
46
46
 
47
47
  function initCarousel(root) {
48
48
  if (root.dataset.fdyCarouselReady === '1') return;
49
+ if (root.closest('[data-fdy-ignore]')) return;
49
50
  root.dataset.fdyCarouselReady = '1';
50
51
 
51
52
  var viewport = root.querySelector('.fdy-carousel__viewport');
@@ -84,6 +84,7 @@
84
84
 
85
85
  function initCascade(wrap) {
86
86
  if (wrap.dataset.fdyCascadeReady === '1') return;
87
+ if (wrap.closest('[data-fdy-ignore]')) return;
87
88
  wrap.dataset.fdyCascadeReady = '1';
88
89
  wrap.classList.add('fdy-cascade');
89
90
 
@@ -207,6 +207,7 @@
207
207
 
208
208
  function bindGroup(group) {
209
209
  if (group.dataset.fdyCflBound === '1') return;
210
+ if (group.closest('[data-fdy-ignore]')) return;
210
211
  var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
211
212
  if (!dialog) return;
212
213
  initDialog(dialog);
@@ -218,6 +219,7 @@
218
219
 
219
220
  function bindOpener(opener) {
220
221
  if (opener.dataset.fdyCflBound === '1') return;
222
+ if (opener.closest('[data-fdy-ignore]')) return;
221
223
  var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
222
224
  if (!dialog) return;
223
225
  initDialog(dialog);
@@ -240,5 +242,7 @@
240
242
  initTriggers();
241
243
  }
242
244
 
243
- window.FreedayCfl = { init: initTriggers };
245
+ /* initAll is what every stack helper looks for (useFreeday in Vue and React, FreedayBlazor.initAll):
246
+ without it they skipped this enhancer, and raw CFL markup rendered by a framework never opened. */
247
+ window.FreedayCfl = { init: initTriggers, initAll: initTriggers };
244
248
  })();
@@ -489,6 +489,7 @@
489
489
  // Auto-init renders once; explicit update() re-renders after data changes.
490
490
  function initChart(el) {
491
491
  if (el.dataset.fdyChartReady === '1') return;
492
+ if (el.closest('[data-fdy-ignore]')) return;
492
493
  el.dataset.fdyChartReady = '1';
493
494
  renderChart(el);
494
495
  }
@@ -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
@@ -16,6 +18,7 @@
16
18
 
17
19
  function initRemove(btn) {
18
20
  if (btn.dataset.fdyChipRmReady === '1') return;
21
+ if (btn.closest('[data-fdy-ignore]')) return;
19
22
  btn.dataset.fdyChipRmReady = '1';
20
23
  btn.addEventListener('click', function () {
21
24
  var chip = btn.closest('.fdy-chip');
@@ -28,13 +31,18 @@
28
31
  if (rm) rm.remove();
29
32
  value = (clone.textContent || '').trim();
30
33
  }
31
- chip.dispatchEvent(new CustomEvent('fdy-chip-remove', { bubbles: true, detail: { value: value } }));
32
- chip.remove();
34
+ /* Cancelable, because the kit cannot know whether this removal needs asking first. Removing
35
+ unconditionally made a chip vanish while the app's own confirm dialog was still open, and
36
+ stay gone after Cancel, with the data intact and the screen wrong (CATATAN-FREEDAY §9). */
37
+ var ev = new CustomEvent('fdy-chip-remove', { bubbles: true, cancelable: true, detail: { value: value } });
38
+ chip.dispatchEvent(ev);
39
+ if (!ev.defaultPrevented) chip.remove();
33
40
  });
34
41
  }
35
42
 
36
43
  function initGroup(group) {
37
44
  if (group.dataset.fdyChipsReady === '1') return;
45
+ if (group.closest('[data-fdy-ignore]')) return;
38
46
  group.dataset.fdyChipsReady = '1';
39
47
  var single = group.hasAttribute('data-single');
40
48
  if (!group.getAttribute('role')) group.setAttribute('role', 'group');
@@ -118,6 +118,7 @@
118
118
 
119
119
  function initPicker(wrap) {
120
120
  if (wrap.dataset.fdyDpReady === '1') return wrap._fdyDp || null;
121
+ if (wrap.closest('[data-fdy-ignore]')) return null;
121
122
  wrap.dataset.fdyDpReady = '1';
122
123
  wrap.classList.add('fdy-datepicker');
123
124
 
@@ -647,6 +648,7 @@
647
648
 
648
649
  function initRange(root) {
649
650
  if (root.dataset.fdyDrReady === '1') return;
651
+ if (root.closest('[data-fdy-ignore]')) return;
650
652
  root.dataset.fdyDrReady = '1';
651
653
  var fromEl = root.querySelector('[data-fdy-datepicker][data-role="from"]');
652
654
  var toEl = root.querySelector('[data-fdy-datepicker][data-role="to"]');
@@ -17,6 +17,7 @@
17
17
 
18
18
  function initDatetime(wrap) {
19
19
  if (wrap.dataset.fdyDtReady === '1') return;
20
+ if (wrap.closest('[data-fdy-ignore]')) return;
20
21
  wrap.dataset.fdyDtReady = '1';
21
22
  wrap.classList.add('fdy-datetimepicker');
22
23
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  function initDrawer(dialog) {
10
10
  if (dialog.dataset.fdyDrawerReady === '1') return;
11
+ if (dialog.closest('[data-fdy-ignore]')) return;
11
12
  dialog.dataset.fdyDrawerReady = '1';
12
13
  // Click on the backdrop (target is the dialog box itself) closes it.
13
14
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
@@ -18,6 +19,7 @@
18
19
 
19
20
  function bindTrigger(btn) {
20
21
  if (btn.dataset.fdyDrawerBound === '1') return;
22
+ if (btn.closest('[data-fdy-ignore]')) return;
21
23
  var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
22
24
  if (!dialog) return;
23
25
  initDrawer(dialog);
@@ -186,6 +186,7 @@
186
186
 
187
187
  function initForm(form) {
188
188
  if (form.dataset.fdyFormReady === '1') return;
189
+ if (form.closest('[data-fdy-ignore]')) return;
189
190
  form.dataset.fdyFormReady = '1';
190
191
  form.setAttribute('novalidate', '');
191
192
  var eager = form.hasAttribute('data-fdy-eager');
@@ -79,6 +79,7 @@
79
79
 
80
80
  function initMask(el) {
81
81
  if (el.dataset.fdyMaskReady === '1') return;
82
+ if (el.closest('[data-fdy-ignore]')) return;
82
83
  el.dataset.fdyMaskReady = '1';
83
84
  var pattern = el.getAttribute('data-fdy-mask') || '';
84
85
  if (!pattern) return;
@@ -101,6 +102,7 @@
101
102
 
102
103
  function initReveal(input) {
103
104
  if (input.dataset.fdyPwReady === '1') return;
105
+ if (input.closest('[data-fdy-ignore]')) return;
104
106
  input.dataset.fdyPwReady = '1';
105
107
 
106
108
  var group = document.createElement('span');
@@ -19,6 +19,7 @@
19
19
 
20
20
  function initMenu(wrap) {
21
21
  if (wrap.dataset.fdyMenuReady === '1') return;
22
+ if (wrap.closest('[data-fdy-ignore]')) return;
22
23
  wrap.dataset.fdyMenuReady = '1';
23
24
  var trigger = wrap.querySelector('[aria-haspopup="menu"]');
24
25
  var menu = wrap.querySelector('.fdy-menu, [role="menu"]');
@@ -27,6 +27,7 @@
27
27
 
28
28
  function initNumber(root) {
29
29
  if (root.dataset.fdyNumberReady === '1') return;
30
+ if (root.closest('[data-fdy-ignore]')) return;
30
31
 
31
32
  var input = root.querySelector('input[type="number"]');
32
33
  var buttons = Array.prototype.slice.call(root.querySelectorAll('[data-fdy-number-step]'));
@@ -8,6 +8,7 @@
8
8
 
9
9
  function initRating(root) {
10
10
  if (root.dataset.fdyRatingReady === '1') return;
11
+ if (root.closest('[data-fdy-ignore]')) return;
11
12
  root.dataset.fdyRatingReady = '1';
12
13
 
13
14
  var stars = Array.prototype.slice.call(root.querySelectorAll('.fdy-rating__star'));