@cahyo-dimas/freeday 3.5.0 → 3.7.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 (59) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/COMPONENTS.md +251 -8
  3. package/README.id.md +19 -6
  4. package/README.md +17 -5
  5. package/USAGE.md +42 -0
  6. package/adapters/blazor/FdyDateRange.razor.cs +10 -0
  7. package/adapters/blazor/FdyTable.razor +6 -2
  8. package/adapters/blazor/FdyTable.razor.cs +21 -0
  9. package/adapters/blazor/TableTypes.cs +4 -0
  10. package/adapters/react/components/FdyDateRange.tsx +8 -2
  11. package/adapters/react/components/FdyTable.tsx +24 -3
  12. package/adapters/react/index.d.ts +1 -1
  13. package/adapters/vue/components/FdyDateRange.vue +11 -5
  14. package/adapters/vue/components/FdyTable.vue +18 -2
  15. package/dist/freeday-confirm.js +101 -0
  16. package/dist/freeday-copy.js +58 -0
  17. package/dist/freeday-datepicker.js +17 -2
  18. package/dist/freeday-form.js +30 -1
  19. package/dist/freeday-search.js +77 -0
  20. package/dist/freeday.bundle.css +544 -122
  21. package/dist/freeday.css +241 -118
  22. package/dist/freeday.d.ts +17 -2
  23. package/dist/freeday.js +286 -3
  24. package/dist/freeday.tokens.css +303 -4
  25. package/docs/agent-onboarding.md +8 -0
  26. package/docs/getting-started.md +1 -1
  27. package/package.json +2 -2
  28. package/src/base.css +1 -1
  29. package/src/components/accordion.css +1 -1
  30. package/src/components/alert.css +5 -5
  31. package/src/components/app-shell.css +2 -2
  32. package/src/components/appbar.css +3 -3
  33. package/src/components/avatar.css +17 -9
  34. package/src/components/badge.css +18 -14
  35. package/src/components/breadcrumb.css +1 -1
  36. package/src/components/button.css +21 -13
  37. package/src/components/card.css +7 -0
  38. package/src/components/carousel.css +3 -3
  39. package/src/components/cascade.css +4 -4
  40. package/src/components/chip.css +33 -15
  41. package/src/components/combo.css +5 -5
  42. package/src/components/composition.css +36 -0
  43. package/src/components/datepicker.css +8 -8
  44. package/src/components/description-list.css +11 -1
  45. package/src/components/file-upload.css +9 -4
  46. package/src/components/input-group.css +2 -2
  47. package/src/components/input.css +2 -2
  48. package/src/components/menu.css +3 -3
  49. package/src/components/pagination.css +1 -1
  50. package/src/components/selection.css +8 -2
  51. package/src/components/skeleton.css +1 -1
  52. package/src/components/states.css +7 -0
  53. package/src/components/stepper.css +1 -1
  54. package/src/components/table.css +25 -11
  55. package/src/components/tabs.css +1 -1
  56. package/src/components/timepicker.css +3 -3
  57. package/src/components/toast.css +1 -1
  58. package/src/components/tree.css +1 -1
  59. package/tokens/tokens.json +57 -4
package/CHANGELOG.md CHANGED
@@ -9,6 +9,136 @@ 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.7.0] - 2026-10-23
13
+
14
+ **Kit ini tidak bisa dipakai di Safari 15, dan yang menemukannya adalah sebuah HP.** Pemilik sebuah
15
+ app ronda membuka aplikasinya di iPhone 7 dan menulis tiga kata: tampilannya ancur banget. Sebabnya
16
+ `color-mix()`, 94 kemunculan tanpa satu pun cadangan, dipakai untuk `box-shadow` (26),
17
+ `border-color` (17), `color` (11) dan `background` (5). Safari baru mendukungnya di 16.2; iPhone 7
18
+ mentok di iOS 15.
19
+
20
+ Perbaikan pertama yang dicoba justru membuktikan sebabnya. Pola cadangan yang biasa dipakai orang,
21
+ nilai statis dulu lalu `color-mix()` sesudahnya, tidak bekerja ketika deklarasinya memuat `var()`:
22
+ deklarasi itu sah saat parse dan baru gagal saat computed-value, dan di jalur itu properti jatuh ke
23
+ nilai initial, bukan ke baris sebelumnya. Untuk `border-color` nilai initial itu `currentColor`,
24
+ yaitu hitam. Keluhan tentang garis hitam tebal di mana-mana itulah bukti mekanismenya. Jadi
25
+ `color-mix()`-nya yang dibuang.
26
+
27
+ ### Fixed
28
+ - **Nol `color-mix()` di seluruh CSS yang dikirim** (`#072`). `rgba(var(--x-rgb), .p)` identik
29
+ dengan `color-mix(in srgb, X p%, transparent)` menurut definisi, jadi 10 token semantik baru
30
+ menutup 84 dari 94 kemunculan; `--color-primary-rgb` sendirian menutup 45. Sisanya yang mencampur
31
+ terhadap permukaan dipipihkan di `tokens/build.mjs`, yang kini menghitung nilai bertulis
32
+ `color-mix(in srgb, {a} p%, {b})` sekali per tema sambil menelusuri rantai `$dark ?? $value`,
33
+ sehingga nilai gelapnya diturunkan alih-alih diketik dua kali.
34
+ - **Modifier tone bekerja di Safari 15** (`#072`). Delapan tone kali latar, tinta dan dua garis kini
35
+ 33 token terhitung, dan properti privat `--_fdy-{avatar,badge,chip}-tone` hilang seluruhnya.
36
+ Tampilan di peramban modern terbukti tidak berubah: byte yang benar-benar dicat dibandingkan lewat
37
+ tangkapan layar satu piksel, nol selisih di 48 warna kali dua tema.
38
+ - **Lima tempat yang mencetak teks dengan `--color-primary` pindah ke `--color-primary-strong`**
39
+ (`#073`). Kontrak kontras hanya pernah menjanjikan peran itu terhadap permukaan pada ambang 3:1,
40
+ yakni ambang WCAG untuk komponen UI, bukan untuk teks. Jadi suite-nya lulus sambil kit mencetak
41
+ tinta di bawah AA pada teal 3.74, cyan 3.68, emerald 3.77 dan lime 3.09. Azure lulus di 6.56, dan
42
+ itu sebabnya tidak ada yang menyadarinya. Yang pindah: tautan, centang combo, ikon dropzone saat
43
+ seret, dan dua panah urut tabel. Sembilan belas tempat lain yang polanya ikut tertangkap sengaja
44
+ tidak disentuh, sebab semuanya batas kontrol dan objek grafis yang memang sah di 3:1.
45
+
46
+ ### Added
47
+ - **Penjaga yang membuat ketiganya tidak bisa kembali diam-diam.** Satu uji menegaskan nol
48
+ `color-mix()` di CSS yang dikirim. Satu lagi menegaskan nol properti `color` yang menyebut
49
+ `--color-primary` gundul, membaca nama properti alih-alih substring supaya kesembilan belas isian
50
+ tetap sah. Dan kontrak kontras kini mengukur `--color-primary-strong` terhadap permukaan pada
51
+ ambang teks untuk setiap palet, bukan hanya azure.
52
+
53
+ ### Catatan dukungan peramban
54
+ Floor-nya kini Chrome 105, Safari 15.4 dan Firefox 121, ditentukan `:has()`. Angka itu diaudit dari
55
+ fitur yang dipakai, bukan dari sesi di perangkat sungguhan. Belum ada yang menjalankan kit ini di
56
+ Safari 15.4 nyata.
57
+
58
+ ## [3.6.0] - 2026-10-22
59
+
60
+ **Tiga belas hal yang dibangun ulang sebuah app back-office karena kit belum punya.** App-nya tidak
61
+ memakai Freeday (Vue + PrimeVue), jadi yang diangkat adalah polanya, bukan komponennya: setiap
62
+ kandidat dibandingkan dengan kit, dan hanya yang generik dan benar-benar belum ada yang masuk
63
+ (`#059`–`#071`). Satu di antaranya cacat yang kit punya juga: submit yang ditolak memfokus field
64
+ yang tak terlihat. Satu lagi mengubah tampilan setiap tabel, jadi dikirim sebagai opt-in dan
65
+ dijadwalkan jadi default di 4.0 (NEXT-UP #16). Tidak ada tipe publik yang menyempit dan tidak ada
66
+ tampilan yang berubah tanpa diminta, jadi ini rilis minor.
67
+
68
+ ### Added
69
+ - **`Freeday.confirm()`** (`#060`). Pertanyaan ya/tidak sebelum aksi yang tak bisa diulang, tanpa
70
+ merakit dialog sendiri. Mengembalikan `Promise<boolean>`: `true` saat accept, `false` saat reject,
71
+ Escape atau klik backdrop. `details[]` tampil sebagai daftar di bawah pesan, untuk konfirmasi yang
72
+ harus menyebut temuannya. Fokus awal di tombol reject, teks dipasang sebagai teks (bukan HTML), dan
73
+ kedua tombol punya hook `data-fdy-confirm-accept` / `-reject` untuk test. Tipe ada di
74
+ `freeday.d.ts`; Blazor memanggilnya lewat `JS.InvokeAsync<bool>`.
75
+ - **`[data-fdy-search]`** (`#061`, menjawab NEXT-UP #10). `.fdy-input-group` yang mencari saat
76
+ pengetik berhenti sejenak (`data-fdy-search-delay`, default 400 ms), langsung saat Enter, dan
77
+ punya tombol clear. Ketiga jalur berakhir di satu event `fdy-search {term}`. Clear menulis lewat
78
+ setter native dan memicu `input` native, jadi `v-model`, `onChange` React dan `@bind` Blazor ikut.
79
+ - **`[data-fdy-copy]`** (`#062`). Tombol yang menyalin nilainya sendiri lalu memberi toast. Tanpa
80
+ Clipboard API (http) pembaca diberi tahu untuk menyalin manual. Listener-nya didelegasikan di
81
+ `document`, jadi tombol yang dirender belakangan tak perlu hidrasi.
82
+ - Ketiganya dijaga `browser/confirm-search-copy.mjs` (7 spec), dan tiap spec diverifikasi dengan
83
+ mutasi: delapan mutasi pada kode baru, delapan merah.
84
+ - **`settled` pada date range** (`#063`). Vue `@settled`, React `onSettled`, Blazor `Settled`,
85
+ jalur mentah `fdy-daterange-settled`. Terpancar hanya saat kedua ujung terisi atau keduanya
86
+ kosong. Sebelumnya setiap stack memancarkan per ujung, jadi list yang reload saat berubah memuat
87
+ data dengan tanggal awal saja lalu memuat lagi saat tanggal akhir dipilih. `change` dan `v-model`
88
+ tidak berubah. Dijaga `browser/daterange-settled.mjs` (4 spec) + `FdyDateRangeTests` (2 bUnit),
89
+ lima mutasi, lima merah.
90
+ - **`.fdy-dl--grid`** (`#066`). Grid fakta: label di atas nilai, kolom sebanyak yang muat di lebar
91
+ list itu SENDIRI, maksimal tiga. Diukur dari elemen, bukan jendela, jadi markup yang sama tiga
92
+ kolom di area utama dan satu kolom di rail sempit tanpa modifier. Semua `.fdy-dl dd` kini
93
+ `overflow-wrap:anywhere`, jadi email atau nomor panjang tidak lagi tercetak di atas kolom sebelah.
94
+ - **`.fdy-card__head` + `__action`** (`#070`). Header kartu: tanda, judul yang di-truncate, dan
95
+ satu aksi di kanan, dengan garis selebar kartu. Tandanya `.fdy-avatar --sm --square --tone-N`.
96
+ - **`.fdy-avatar--square`** untuk tanda yang bukan orang (logo perusahaan, chip ikon kartu).
97
+ - **Hint state switch** (`#068`). Dua baris `[data-fdy-when="on|off"]` di satu `.fdy-help`; kit
98
+ menampilkan yang cocok dengan state sekarang, tanpa JS. `aria-describedby` menunjuk ke
99
+ pembungkusnya, jadi pembaca layar hanya mendengar baris yang tampil (diverifikasi lewat deskripsi
100
+ aksesibel yang dihitung Chrome).
101
+ - **`.fdy-state--compact`** (`#069`) untuk state di dalam kartu kecil.
102
+ - **Empty state `FdyTable` tahu kenapa ia kosong** (`#069`). Slot Vue `empty` dan fungsi React
103
+ `empty` menerima `{ filtered, clearFilters }`; Blazor punya `EmptyTemplate`
104
+ (`RenderFragment<FdyTableEmptyContext>`). "Tidak ada yang cocok" bisa menawarkan Clear, "belum
105
+ ada data" menawarkan Create.
106
+ - **`.fdy-page__columns`** (`#065`, menutup "Form master-detail / 2 kolom" yang ditunda di
107
+ NEXT-UP). Rail `__lead` / `__aside` di samping `__main`; lebar dipilih dari rail mana yang ada
108
+ (8/4, 3/9, 3/6/3). Saat menumpuk, rail ringkasan didahulukan. Mulai `md` rail sticky dan
109
+ scroll sendiri, jadi total dan status tetap terlihat dan rail yang lebih tinggi dari layar tidak
110
+ menggantung. Diadaptasi dari layout yang di app asalnya menahan seluruh halaman; di kit shell-nya
111
+ tetap scroll halaman, jadi tidak ada consumer yang dipaksa mengubah layout-nya. Dijaga
112
+ `browser/page-columns.mjs` (5 spec, 6 mutasi, 6 merah).
113
+ - **`.fdy-table--hover-actionable`** (`#064`), prop `hoverActionable` / `HoverActionable` di ketiga
114
+ adapter. Hanya baris yang melakukan sesuatu (`rowActivatable`) yang ber-hover (baris terpilih tetap berwarna terpilih);
115
+ baris inert kembali ke warna istirahatnya sendiri, termasuk stripe, dan sel beku tetap opak.
116
+ Opt-in di 3.x supaya tidak ada tabel yang berubah; direncanakan jadi default di 4.0. Hover dasar
117
+ kini membaca `--_fdy-row-hover` (privat), tampilannya tidak berubah. Dijaga
118
+ `browser/table-hover.mjs` (4 spec, 5 mutasi, 5 merah) + `FdyTableHoverTests` (2 bUnit).
119
+ - **Docs: breadcrumb** (`#071 §8`). `aria-current="page"` hanya untuk crumb yang memang halaman ini;
120
+ trail yang berhenti di induknya (form record yang crumb terakhirnya list) membiarkan crumb itu
121
+ tetap link.
122
+ - **Docs: Record actions** (`#067`). Urutan tetap untuk aksi form di `.fdy-page__actions`:
123
+ Find | New, maksimal dua aksi inline, sisanya di menu More dengan urutan deklarasi yang sama dan
124
+ warna danger yang tetap terbawa, Save terakhir dan satu-satunya tombol solid.
125
+ - Dijaga `browser/display-parts.mjs` (4), `browser/table-empty.mjs` (2) dan `FdyTableEmptyTests`
126
+ (2 bUnit). Setiap perilaku baru diverifikasi dengan mutasi (13 mutasi, 13 merah).
127
+ ### Fixed
128
+ - **Date range jalur mentah: ujung yang dihapus kini diumumkan.** Tanggal awal yang melewati
129
+ tanggal akhir mengosongkan tanggal akhir tanpa event apa pun, jadi `To` di Blazor (yang hanya
130
+ mendengar pilihan) tetap memegang tanggal yang sudah tidak ada. Pengosongan itu sekarang
131
+ memancarkan `fdy-datepicker-change` dengan `value: ''`.
132
+ - **Submit yang ditolak kini memperlihatkan field yang ditolaknya** (`#059`). `freeday-form.js`
133
+ memanggil `focus()` pada kontrol invalid pertama dan berhenti di situ. Kalau kontrol itu tidak
134
+ dirender (di dalam `<details>` tertutup, di tab yang tak dipilih, di panel stepper yang sudah
135
+ dilewati), `focus()` tidak melakukan apa pun dan tidak melapor apa pun: field memerah di tempat
136
+ yang tak terlihat, dan tombol Save terbaca mati. Sekarang penghalangnya dibuka dulu lewat kontrol
137
+ milik komponennya sendiri, supaya state komponen itu tetap cocok dengan yang tampil: `<details>`
138
+ di-`open`, tab-nya diklik, tombol step-nya diklik. Step yang **belum dicapai** sengaja tidak
139
+ dilompati, karena itu sama dengan melewati guard `fdy-step-before-change`. Dijaga
140
+ `browser/form-reveal.mjs` (5 spec); empat di antaranya merah sebelum perbaikan.
141
+
12
142
  ## [3.5.0] - 2026-09-15
13
143
 
14
144
  **Sebuah angka yang hanya bisa ditemukan dengan membaca stylesheet kit.** `#058` (dari
package/COMPONENTS.md CHANGED
@@ -77,7 +77,7 @@ element is fine. Events are bubbling
77
77
  | `data-fdy-autocomplete` | `freeday-autocomplete.js` | `FreedayAutocomplete` | `fdy-autocomplete-select` |
78
78
  | `data-fdy-cascade` | `freeday-cascade.js` | `FreedayCascade` | `fdy-cascade-change` |
79
79
  | `data-fdy-cfl` | `freeday-cfl.js` | `FreedayCfl` | `fdy-cfl-select` |
80
- | `data-fdy-datepicker`, `data-fdy-daterange` | `freeday-datepicker.js` | `FreedayDatepicker` | `fdy-datepicker-change` |
80
+ | `data-fdy-datepicker`, `data-fdy-daterange` | `freeday-datepicker.js` | `FreedayDatepicker` | `fdy-datepicker-change`, `fdy-daterange-settled` |
81
81
  | `data-fdy-timepicker` | `freeday-timepicker.js` | `FreedayTimepicker` | `fdy-time-select` |
82
82
  | `data-fdy-datetimepicker` | `freeday-datetime.js` | `FreedayDatetime` | `fdy-datetime-change` |
83
83
  | `data-fdy-table` | `freeday-table.js` | `FreedayTable` | `fdy-table-change` |
@@ -93,9 +93,12 @@ element is fine. Events are bubbling
93
93
  | `data-fdy-rating` | `freeday-rating.js` | `FreedayRating` | — (native `change`) |
94
94
  | `data-fdy-slider` | `freeday-slider.js` | `FreedaySlider` | — (native `input`) |
95
95
  | `data-fdy-number` | `freeday-number.js` | `FreedayNumber` | — (native `input`, `change`) |
96
+ | `data-fdy-search` | `freeday-search.js` | `FreedaySearch` | `fdy-search` (+ native `input` on clear) |
97
+ | `data-fdy-copy="value"` (delegated, no hydration) | `freeday-copy.js` | `FreedayCopy` | `fdy-copy` |
96
98
  | `data-fdy-drawer="id"` | `freeday-drawer.js` | `FreedayDrawer` | — |
97
99
  | `data-fdy-chart="type"` | `freeday-chart.js` | `FreedayChart` | — (`.update(el)` to repaint) |
98
100
  | — (imperative) | `freeday-toast.js` | `Freeday.toast()` / `Freeday.dismiss()` | — |
101
+ | — (imperative, returns `Promise<boolean>`) | `freeday-confirm.js` | `Freeday.confirm()` | — |
99
102
  | — (stamps `<html data-breakpoint>`) | `freeday-breakpoint.js` | `FreedayBreakpoint` | `fdy-breakpoint-change` |
100
103
  | — (internal positioning) | `freeday-popover.js` | `FreedayPopover` | — |
101
104
 
@@ -289,6 +292,78 @@ carry the doctrine in markup.
289
292
  </div>
290
293
  ```
291
294
 
295
+ ### Document columns — `.fdy-page__columns`
296
+
297
+ A record or form page with side rails: `__lead` on the left for the record's identity (avatar,
298
+ number, status), `__main` for the sections, `__aside` on the right for the summary (totals, what
299
+ happens next). The column widths follow which rails are **present**, so no screen picks them:
300
+
301
+ | Children | From `md` | From `lg` |
302
+ |---|---|---|
303
+ | `__main` | one column | one column |
304
+ | `__main` + `__aside` | 8 / 4 | 8 / 4 |
305
+ | `__lead` + `__main` | 3 / 9 | 3 / 9 |
306
+ | `__lead` + `__main` + `__aside` | rails stacked on the left, main on the right | 3 / 6 / 3 |
307
+
308
+ - Below `md` everything stacks, and **the summary rail comes first**. Last, it would sit under
309
+ every section, and the button in it with it.
310
+ - From `md` each rail is **sticky and scrolls on its own**: the total and the status stay in view
311
+ while `__main` scrolls with the page, and a rail taller than the screen scrolls inside itself
312
+ instead of hanging its end off the bottom. The exception is both rails sharing the left column
313
+ (between `md` and `lg`): two sticky rails in one column would slide over each other, so there they
314
+ scroll with the page.
315
+ - Inside `.fdy-app` the rails stick below the topbar. Set `--fdy-page-sticky-top` on the columns or
316
+ an ancestor if your header is taller.
317
+ - A rail's box starts `--space-1` early on every side, room for a focus ring that a scrolling box
318
+ would otherwise clip. Its content still lines up with `__main`.
319
+ - Put `.fdy-dl--grid` in a rail as is: it measures the rail, not the window, so it stays one column.
320
+
321
+ ```html
322
+ <div class="fdy-page__columns">
323
+ <aside class="fdy-page__lead" aria-label="Record">…</aside>
324
+ <div class="fdy-page__main">…sections…</div>
325
+ <aside class="fdy-page__aside" aria-label="Summary">…</aside>
326
+ </div>
327
+ ```
328
+
329
+ ### Record actions — a form's `.fdy-page__actions`
330
+
331
+ A document or master form carries more than one button: its own actions (Print, Send, Cancel
332
+ document, Copy from…) and Save. Without a rule the row grows with every module, reflows at tablet
333
+ width and pushes Save off the end, and two screens with the same actions order them differently.
334
+ The kit's rule, in this fixed order:
335
+
336
+ 1. **Find | New** as one `.fdy-btn-group`, ghost. Hide New on the create screen itself: a button
337
+ that navigates to the screen you are on appears to do nothing.
338
+ 2. **At most two** of the form's own actions inline, ghost, in the order they are declared.
339
+ 3. **More**: an `.fdy-menu` holding every other action, in the **same declared order**. A
340
+ destructive item keeps `.fdy-menu__item--danger`: an action that loses its colour on the way
341
+ into a menu is one the reader no longer recognises. Keep each action's test hook the same
342
+ whether it is inline or in the menu.
343
+ 4. **Save**, last, the **only** solid button on the row. Never green: green says something already
344
+ succeeded.
345
+
346
+ ```html
347
+ <div class="fdy-page__actions">
348
+ <div class="fdy-btn-group" role="group" aria-label="Records">
349
+ <a class="fdy-btn fdy-btn--ghost" href="/orders">Find</a>
350
+ <a class="fdy-btn fdy-btn--ghost" href="/orders/new">New</a>
351
+ </div>
352
+ <button class="fdy-btn fdy-btn--ghost" type="button">Print</button>
353
+ <button class="fdy-btn fdy-btn--ghost" type="button">Send</button>
354
+ <div class="fdy-menu-wrap" data-fdy-menu>
355
+ <button class="fdy-btn fdy-btn--ghost" type="button" aria-haspopup="menu" aria-expanded="false">More</button>
356
+ <ul class="fdy-menu fdy-menu--end" role="menu" hidden>
357
+ <li role="none"><button class="fdy-menu__item" role="menuitem">Copy from quotation</button></li>
358
+ <li role="none"><button class="fdy-menu__item fdy-menu__item--danger" role="menuitem">Cancel document</button></li>
359
+ </ul>
360
+ </div>
361
+ <button class="fdy-btn" type="submit" form="order-form">Save</button>
362
+ </div>
363
+ ```
364
+
365
+ Ask before a destructive action with `Freeday.confirm({ tone: 'danger', … })`.
366
+
292
367
  ## Type roles & text
293
368
  One role per level of hierarchy. Never re-use a card title for a page title.
294
369
 
@@ -513,7 +588,7 @@ cannot guarantee.
513
588
 
514
589
  | Type | What the UA still draws | What to do |
515
590
  |---|---|---|
516
- | `search` | WebKit's clear (×) button, in OS colours | **Left alone on purpose** — it is the only way to empty the field. Want it themed? Drop the type back to `text` and add a `.fdy-input-group__btn` that clears. |
591
+ | `search` | WebKit's clear (×) button, in OS colours | **Left alone on purpose** — it is the only way to empty the field. Want it themed, and want clearing to search again? Use [`data-fdy-search`](#search-box--data-fdy-search), which is `type="text"` with a clear button of its own. |
517
592
  | `date` `time` `datetime-local` | the picker indicator glyph, and the whole native picker | Use `.fdy-datepicker` / `.fdy-timepicker` / `.fdy-datetimepicker` instead — the kit ships its own. |
518
593
 
519
594
  `number` used to be a third: the UA spin buttons are unthemeable, so `.fdy-input[type="number"]`
@@ -557,6 +632,31 @@ the shared border, focus ring and error promotion. Needs `freeday-number.js`.
557
632
  `aria-label`, so pointer and browse-mode users still get a named control.
558
633
  - `type="button"` is required. Inside a `<form>`, a bare `<button>` submits it.
559
634
 
635
+ ## Search box — `[data-fdy-search]`
636
+ The search box above a list: it searches when the reader pauses typing, at once on Enter, and has a
637
+ clear button that brings every row back. **Not a new block.** It is an `.fdy-input-group`, like the
638
+ number field. Needs `freeday-search.js`.
639
+
640
+ ```html
641
+ <div class="fdy-input-group" data-fdy-search>
642
+ <span class="fdy-input-group__addon fdy-input-group__addon--icon"><!-- search svg --></span>
643
+ <input class="fdy-input" type="text" aria-label="Search orders">
644
+ <button type="button" class="fdy-input-group__btn" data-fdy-search-clear aria-label="Clear search">&times;</button>
645
+ </div>
646
+ ```
647
+
648
+ - **One event for all three paths.** The group emits bubbling `fdy-search` with `detail.term`
649
+ (trimmed) after the pause, on Enter, and on clear. `""` means "show everything again". Listen to
650
+ that and nothing else.
651
+ - The pause is `data-fdy-search-delay` in ms on the group, default `400`. A new keystroke restarts
652
+ it; Enter and clear send at once and cancel what was pending.
653
+ - `type="text"`, not `search`: WebKit's own × is unthemed, and emptying a field with it never
654
+ searched again on a list that only listened for Enter.
655
+ - The clear button is yours, and so is its `aria-label`. The enhancer hides it while the box is
656
+ empty. Clearing writes through the native value setter and fires a native `input`, so `v-model`,
657
+ a React `onChange` and Blazor `@bind` see the empty value, then puts focus back in the box.
658
+ - Enter is `preventDefault`ed, so a search box inside a `<form>` does not submit it.
659
+
560
660
  ## Checkbox · radio · switch
561
661
  Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrapping `<label>`;
562
662
  `.fdy-checkbox` on a bare `<input type="checkbox">` used inside tables/trees.
@@ -566,6 +666,23 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
566
666
  <label class="fdy-switch"><input type="checkbox"> Enable notifications</label>
567
667
  ```
568
668
 
669
+ **A state hint** says what the switch's **current** state means, for a label that does not explain
670
+ "on" by itself ("Approval" on a notification rule). Two lines in one `.fdy-help`, and the kit
671
+ shows the one that matches. Point `aria-describedby` at the **wrapper**, not at the two lines: a
672
+ hidden child of a referenced element is skipped, so a screen reader hears only the line on screen,
673
+ while each line referenced directly would be read even when hidden. Zero JS; works for
674
+ `.fdy-check` too. Needs a `.fdy-field` around both.
675
+
676
+ ```html
677
+ <div class="fdy-field">
678
+ <label class="fdy-switch"><input type="checkbox" aria-describedby="appr-hint"> Approval</label>
679
+ <div class="fdy-help" id="appr-hint">
680
+ <span data-fdy-when="on">Each change waits for a supervisor.</span>
681
+ <span data-fdy-when="off">Changes apply at once; nobody reviews them.</span>
682
+ </div>
683
+ </div>
684
+ ```
685
+
569
686
  ## Select / combobox — `.fdy-combo`
570
687
  > **Typed wrapper: `<FdyCombo>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
571
688
 
@@ -857,7 +974,8 @@ page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear b
857
974
  | Prop | Type | What it does |
858
975
  |---|---|---|
859
976
  | `modelValue` · `value` | `DateRangeValue` = `{ start, end }`, each `string \| null` | Both ends at once, so the pair can never be half-applied. |
860
- | `onChange` | `(value: DateRangeValue) => void` | React. Vue emits `update:modelValue` and `change`. |
977
+ | `onChange` | `(value: DateRangeValue) => void` | React. Vue emits `update:modelValue` and `change`. Fires on **each** end, which is right for a binding and wrong for a reload. |
978
+ | `onSettled?` | `(value: DateRangeValue) => void` | React. Vue emits `settled`, Blazor `Settled` (`SettledRange(From, To)`), the raw path `fdy-daterange-settled` `{ from, to }` on the group. Fires only when **both ends are chosen or both are empty**, so a list that reloads on it never filters on half a range. |
861
979
  | `min?` · `max?` | `string` | ISO bounds for both ends. The end can never precede the start. |
862
980
  | `locale?` | `string` | As on the single picker. |
863
981
  | `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
@@ -874,7 +992,9 @@ The enhancer builds everything.
874
992
  `__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months` `--years`)
875
993
  `__dow` `__day` `__month` `__year`
876
994
  - Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
877
- get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
995
+ get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start:
996
+ a start picked past the end clears the end, and that clear fires `fdy-datepicker-change` with
997
+ `value: ''` like a pick. Reload on `fdy-daterange-settled`, not on each change.
878
998
  **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor). Use it in those stacks.
879
999
  **Getting to a distant date.** The title is a **button**, and it drills up one level each press:
880
1000
  day grid → 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) → 12-cell year grid
@@ -982,6 +1102,13 @@ blur/input. Needs `freeday-form.js`.
982
1102
  - Per-control messages: `data-fdy-msg-required`, `-type`, `-minlength`, `-min`, `-mismatch`, …
983
1103
  - Cross-field: `data-fdy-match="#otherFieldId"`
984
1104
  - Emits `fdy-form-valid` / `fdy-form-invalid`.
1105
+ - **A refused field is revealed before it is focused**, because `focus()` on a control that is not
1106
+ rendered does nothing. A closed `<details>` around it (accordion, tree, nav group) is opened, an
1107
+ unselected tab is selected by clicking it, and a `.fdy-step-panel` left behind is returned to by
1108
+ clicking its step. A step **not yet reached** is never jumped to, since that would skip its
1109
+ `fdy-step-before-change` guard: put the submit where every refusable field is already reachable,
1110
+ or mark the step `is-error` from `fdy-form-invalid`. Anything else hiding the field (your own
1111
+ `hidden`, a closed drawer) stays as you left it.
985
1112
 
986
1113
  ## Password & mask
987
1114
  `data-fdy-password` on `<input type="password">` adds a reveal toggle (reusing the input-group
@@ -1080,6 +1207,15 @@ hover and selection are ordered so the later state always wins. Override the col
1080
1207
  Tier-3 token **`--fdy-table-stripe`** (set it on any ancestor; the rule reads it as a `var()`
1081
1208
  fallback, so your value wins without a specificity fight). Frozen columns stay striped.
1082
1209
 
1210
+ **`--hover-actionable`** keeps the hover tint for rows that do something
1211
+ (`.fdy-table__row--activatable`) and gives every other row none. A selected row keeps its selected
1212
+ colour either way. A tinted row
1213
+ with no click handler promises a target that is not there: people click it, nothing happens, and
1214
+ the button they needed was elsewhere in the row. Striping is what keeps the eye on a line; hover
1215
+ says "target". An inert row hovers back to its own resting colour, stripe included, and a frozen
1216
+ cell to its opaque surface. Opt-in in 3.x so no existing table changes; planned as the default
1217
+ for 4.0.
1218
+
1083
1219
  ```html
1084
1220
  <div class="fdy-table-wrap">
1085
1221
  <table class="fdy-table">
@@ -1187,12 +1323,13 @@ provide the state prop for a concern and you own that concern instead. React nam
1187
1323
  | `pageSizes?` · `onPageSizeChange?` | `readonly number[]` · `(size) => void` | Offer a rows-per-page control. Server mode reports the pick through the page event (read its `size`); client mode applies it itself and reports it too, so it works with nothing wired. |
1188
1324
  | `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
1189
1325
  | `loading?` | `boolean` | Show the in-table loading state. |
1190
- | `emptyText?` · `empty?` | `string` · `ReactNode` | The empty state as text, or as markup (React `empty`, Vue's `empty` slot). |
1326
+ | `emptyText?` · `empty?` | `string` · `ReactNode \| (ctx) => ReactNode` | The empty state as text, or as markup (React `empty`, Vue's `empty` slot). As a React function or through the Vue slot's props it receives `{ filtered, clearFilters }`: whether an active column filter is what emptied the table, and how to clear every filter (through `onFiltersChange` / `update:filters` when they are controlled). "No rows match" wants a Clear button, "nothing here yet" a Create one. |
1191
1327
  | `ariaLabel?` | `string` | Names the table for assistive tech. |
1192
1328
  | `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
1193
1329
  | `rowClass?` | `(row: Row) => string \| undefined` | Per-row class hook, e.g. marking the selected row. |
1194
1330
  | `expandedKeys?` · `renderRowDetail?` | `ReadonlyArray<string \| number>` · `(row) => ReactNode` | Controlled expansion: these keys get a full-width detail row. Vue uses the `row-detail` slot. |
1195
1331
  | `striped?` | `boolean` | Zebra-stripe the body rows (`.fdy-table--striped`). |
1332
+ | `hoverActionable?` | `boolean` | Hover only the rows that do something (`rowActivatable`), `.fdy-table--hover-actionable`. A tint on a row with no handler promises a target that is not there. Inert rows keep their resting colour, stripe included. Off by default so existing tables do not change; planned as the default for 4.0. |
1196
1333
  | `selectable?` | `boolean` | Render the checkbox column and the bulk bar. |
1197
1334
  | `selectedKeys?` · `onSelectedKeysChange?` | `ReadonlyArray<string \| number>` · `(keys) => void` | Controlled selection, as `rowKey` values — the same identity `expandedKeys` uses, because a key survives the re-fetch that replaces every row object. Omit `selectedKeys` and the column still works, keeping the selection internally. The callback fires in **both** modes, so a screen can watch the selection without owning it. Vue emits `update:selectedKeys`; Blazor binds `@bind-SelectedKeys`. |
1198
1335
  | `bulkActions?` | `ReactNode` | React. Content in the bulk bar beside the count; Vue uses the `bulk-actions` slot (scoped: `keys`, `clear`), Blazor `BulkContent`. |
@@ -1204,7 +1341,8 @@ provide the state prop for a concern and you own that concern instead. React nam
1204
1341
 
1205
1342
  Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
1206
1343
  `Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
1207
- `FiltersChanged`.
1344
+ `FiltersChanged`. Its filter-aware empty state is `EmptyTemplate`, a
1345
+ `RenderFragment<FdyTableEmptyContext>` (`Filtered`, `ClearFilters`) that wins over `EmptyContent`.
1208
1346
 
1209
1347
  **Select-all ticks the current page, not the whole result set.** A header checkbox that silently
1210
1348
  selects rows the reader cannot see is how a bulk delete goes wrong, so it acts on the rows on
@@ -1332,6 +1470,11 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
1332
1470
  (`aria-hidden="true"`) `__title` `__text` `__actions`. Put it inside the card or table area it
1333
1471
  replaces.
1334
1472
 
1473
+ `--compact` is the same state inside a small card or dashboard widget: icon and text on one line,
1474
+ `__actions` pushed to the end, no `__title`. Give a failed load its own `--compact --danger` line
1475
+ rather than rendering zero: a widget that shows 0 for a request that failed reads as "nothing to
1476
+ do", the opposite of what happened.
1477
+
1335
1478
  ## Charts — `data-fdy-chart`
1336
1479
  > **Typed wrapper: `<FdyChart>`**. data props in all three (`type` + `values` / `series`); it repaints on data change, so `FreedayChart.update(el)` is only for the raw path below.
1337
1480
 
@@ -1433,12 +1576,55 @@ responsive `.fdy-datatable` should become below `md`, rather than a stack of `.f
1433
1576
 
1434
1577
  ## Description list — `.fdy-dl`
1435
1578
  Key–value detail view: `<dl class="fdy-dl">` with `<dt>`/`<dd>` pairs. `--rows` for a row-per-pair
1436
- layout.
1579
+ layout. Every `dd` wraps a long unbreakable value (an email, an order code, a 13-digit phone) inside
1580
+ its column instead of printing over the next one.
1581
+
1582
+ **`--grid` is the facts panel:** caption above value, each pair wrapped in a `<div>` (valid inside
1583
+ `<dl>`), in as many columns as the list's **own** width holds, never more than three. It is sized
1584
+ by the element, not the window, so the same markup is three columns in a wide main column and one
1585
+ column in a narrow side rail, with no modifier. `--fdy-dl-col-min` (default `14rem`) is the
1586
+ narrowest a column may get.
1587
+
1588
+ ```html
1589
+ <dl class="fdy-dl fdy-dl--grid">
1590
+ <div><dt>Customer</dt><dd>PT Sumber Makmur</dd></div>
1591
+ <div><dt>Email</dt><dd>procurement@sumbermakmur.co.id</dd></div>
1592
+ <div><dt>Phone</dt><dd class="fdy-mono">0812345678901</dd></div>
1593
+ </dl>
1594
+ ```
1595
+
1596
+ A fact is not a field: use this for read-only values, `.fdy-form-grid` only where something can be
1597
+ typed.
1437
1598
 
1438
1599
  ---
1439
1600
 
1440
1601
  # Feedback
1441
1602
 
1603
+ ## Copy a value — `[data-fdy-copy]`
1604
+ A button that copies a value the reader takes somewhere else: an external document number, an
1605
+ event ID. Needs `freeday-copy.js`; uses `Freeday.toast()` for its feedback.
1606
+
1607
+ ```html
1608
+ <dd>
1609
+ <span class="fdy-mono">INV-2026-00042</span>
1610
+ <button type="button" class="fdy-btn fdy-btn--ghost fdy-btn--icon fdy-btn--sm"
1611
+ data-fdy-copy="INV-2026-00042" aria-label="Copy invoice number"><!-- copy svg --></button>
1612
+ </dd>
1613
+ ```
1614
+
1615
+ - **The value is the attribute.** Write it from the same variable as the visible text; a table row
1616
+ needs no ids to find its own cell. An empty value copies nothing, so render the button only when
1617
+ there is something to copy.
1618
+ - One delegated listener on the document: a button rendered later needs no hydration.
1619
+ - The toast says `Copied.` on success. Without a Clipboard API (plain `http`, an old browser) or
1620
+ when the write is refused, a sticky danger toast tells the reader to copy by hand. A copy that
1621
+ silently does nothing is the one outcome it never produces. Override both strings per button
1622
+ with `data-fdy-text-copied` / `data-fdy-text-failed`.
1623
+ - Emits bubbling `fdy-copy` with `detail.value` and `detail.ok`.
1624
+ - Name the button for its value (`Copy invoice number`); an icon alone has no name.
1625
+ - Inside a clickable table row the click still reaches the row's handler. Stop it there if copying
1626
+ should not also open the row.
1627
+
1442
1628
  ## Alert — `.fdy-alert`
1443
1629
  Inline message. Modifiers `--info` `--success` `--warning` `--danger`. Parts `__icon` `__body`
1444
1630
  `__title` `__text` `__close`. The danger variant takes `role="alert"`.
@@ -1476,6 +1662,35 @@ something is in the top layer too. Where the Popover API is unavailable (before
1476
1662
  Safari 17 / Firefox 125) the region falls back to a fixed block at `z-index: 200`, which cannot
1477
1663
  clear an open modal, the message is still announced to a screen reader.
1478
1664
 
1665
+ ## Confirm — `Freeday.confirm()`
1666
+ A yes/no question before an action that cannot be taken back. Imperative, like the toast. Needs
1667
+ `freeday-confirm.js`; renders a `.fdy-modal--sm`.
1668
+
1669
+ ```js
1670
+ const yes = await Freeday.confirm({
1671
+ title: 'Delete order?',
1672
+ message: 'Order SO-1042 will be deleted permanently.',
1673
+ details: ['2 payments are deleted with it'], // optional, shown as a list
1674
+ acceptText: 'Delete order', rejectText: 'Keep', // name the action
1675
+ tone: 'danger', // 'primary' (default) | 'danger'
1676
+ })
1677
+ ```
1678
+
1679
+ - Resolves `true` on accept, `false` on reject, **Escape, or a click on the backdrop**. One dialog
1680
+ gives one answer.
1681
+ - `details` is for a confirm that has to name its findings. Five findings folded into `message`
1682
+ read as one long sentence.
1683
+ - Focus starts on the reject button, so an Enter that was meant for the page behind cannot
1684
+ confirm. Focus returns to the opener when it closes, and the dialog leaves the DOM.
1685
+ - Text is set as text, never parsed as HTML, so a record name can go into `message` as is.
1686
+ - The buttons carry `data-fdy-confirm-accept` / `data-fdy-confirm-reject`. A test needs them: an
1687
+ accept labelled "Delete" shares its accessible name with every row's own Delete button.
1688
+ - Defaults are `Confirm` / `Cancel`; pass `acceptText` / `rejectText` instead of relying on them.
1689
+ - Vue and React call it as is. Blazor awaits it through interop:
1690
+ `await JS.InvokeAsync<bool>("Freeday.confirm", new { title = "Delete order?", tone = "danger" })`.
1691
+ - An "unsaved changes" guard is three lines on top of it, in your router's leave hook:
1692
+ `return isDirty() ? Freeday.confirm({ … }) : true`.
1693
+
1479
1694
  ## Tooltip — `.fdy-tooltip`
1480
1695
  Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
1481
1696
  gets `aria-describedby` → the `.fdy-tooltip[role="tooltip"]` `id`.
@@ -1610,6 +1825,12 @@ for primary navigation (see Nav above).
1610
1825
  `.fdy-breadcrumb__item` (last one `aria-current="page"`, no link), `.fdy-breadcrumb__link`, and
1611
1826
  `.fdy-breadcrumb__sep` marked `aria-hidden="true"`.
1612
1827
 
1828
+ `aria-current="page"` belongs to the crumb that **is** this page, not to whichever crumb is last. A
1829
+ trail that stops at the parent (a record form whose last crumb is its list) keeps that crumb a
1830
+ link with no `aria-current`; otherwise the reader is told they are on the list while looking at a
1831
+ record. A crumb for a menu group that has no page of its own is plain text in the item, not a link
1832
+ to nowhere.
1833
+
1613
1834
  ## Tabs — `.fdy-tabs`
1614
1835
  APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1615
1836
  `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
@@ -1736,7 +1957,7 @@ is `role="group" aria-roledescription="slide"`.
1736
1957
  # Display
1737
1958
 
1738
1959
  ## Card — `.fdy-card`
1739
- Parts `__body` `__title` `__desc` `__footer`. Modifiers:
1960
+ Parts `__head` `__body` `__title` `__desc` `__action` `__footer`. Modifiers:
1740
1961
 
1741
1962
  - `--elevated` raises it (use sparingly; most surfaces are flat)
1742
1963
  - `--interactive` gives cursor + hover-lift to a card that *has* a click handler. **Presentational
@@ -1753,6 +1974,26 @@ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
1753
1974
  - The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
1754
1975
  to the card. That is also what keeps hidden labels inside it from escaping. See *Containment*.
1755
1976
 
1977
+ **Header band: `__head`.** A mark, the title and the card's way out on one row, with a rule under
1978
+ it from edge to edge. The action sits beside the title so it is in the same place on every card;
1979
+ under the content it moved each time the content grew. The title truncates rather than wrapping
1980
+ under the action. The mark is an `.fdy-avatar --sm --square --tone-N`, so its tints are the ones
1981
+ the contrast gate already covers; pick the tone for what the card is **about** and keep it fixed.
1982
+
1983
+ ```html
1984
+ <article class="fdy-card">
1985
+ <header class="fdy-card__head">
1986
+ <span class="fdy-avatar fdy-avatar--sm fdy-avatar--square fdy-avatar--tone-3" aria-hidden="true"><!-- icon --></span>
1987
+ <h3 class="fdy-card__title">Due today</h3>
1988
+ <div class="fdy-card__action"><a class="fdy-btn fdy-btn--text fdy-btn--sm" href="/queue">View all</a></div>
1989
+ </header>
1990
+ <div class="fdy-card__body">…</div>
1991
+ </article>
1992
+ ```
1993
+
1994
+ A collapsible section whose band is a `<button>` cannot hold the action inside it (a button in a
1995
+ button is invalid and is read as one control): put `__action` beside the button, not in it.
1996
+
1756
1997
  ### One card, one primary action, one escape hatch
1757
1998
 
1758
1999
  A card with **two** actions cannot be `--button`: interactive content nested in a `<button>` is
@@ -1813,6 +2054,8 @@ Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks t
1813
2054
  group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
1814
2055
  same-initial avatars. Hash the index off the **full** name, not the initials.
1815
2056
 
2057
+ `--square` for a mark that is not a person: a company logo, a card's icon chip.
2058
+
1816
2059
  Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
1817
2060
  because `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
1818
2061
  ghost border cuts across the circle.
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.5.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.5.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.7.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.7.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`.
@@ -255,11 +255,24 @@ terlihat, HTML native dulu sebelum ARIA, komponen interaktif ikut pola WAI-ARIA
255
255
  `docs/index.html`.
256
256
 
257
257
  ## Dukungan browser
258
- Butuh browser evergreen, kira-kira 2023+: **Chrome 111 · Safari 16.4 · Firefox 113** (floor
259
- ditentukan `color-mix()`). Fitur lain: native `<dialog>`/`::backdrop`, `accent-color`, `100dvh`,
260
- `conic-gradient`, `scroll-snap`, semuanya ≥ Safari 15.4. Blur backdrop pakai
261
- `-webkit-backdrop-filter` + `backdrop-filter` (Safari lama tetap jalan, hanya tanpa blur). Belum ada
262
- CSS build/autoprefixer, jadi dukungan browser lama jadi tanggung jawab konsumen.
258
+ **Chrome 105 · Safari 15.4 · Firefox 121.** Floor-nya ditentukan `:has()`, dan oleh generasi Safari
259
+ 15.4 untuk native `<dialog>`/`::backdrop`, `accent-color`, dan `100dvh`. `conic-gradient` dan
260
+ `scroll-snap` lebih tua lagi.
261
+
262
+ Dulu floor itu ditentukan `color-mix()` di Safari 16.4, dan sekarang tidak lagi. Harganya memang tak
263
+ pernah pantas: deklarasi yang mencampur sebuah token membawa `var()`, jadi ia dianggap sah saat
264
+ parse dan baru gagal saat computed-value, dan di jalur itu properti jatuh ke nilai *initial*-nya,
265
+ bukan ke baris cadangan di atasnya. Di iPhone yang mentok di iOS 15, 17 deklarasi `border-color`
266
+ berubah jadi `currentColor`, yaitu garis hitam tebal di hampir segalanya. Semua alpha di kit ini
267
+ sekarang ditulis `rgba(var(--color-primary-rgb), .26)`, warnanya sama dan sudah bisa diparse sejak
268
+ Safari 9; empat aturan yang memang tak bisa dijawab secara statis membawa pasangan `@supports` yang
269
+ sungguhan. `test/color-mix.test.mjs` menjaga agar yang berikutnya tetap jujur.
270
+
271
+ Yang paling baru dipakai kit ini (`@container`, `@starting-style`, `text-wrap`) sifatnya peningkatan:
272
+ browser yang belum punya akan melewati bloknya dan tetap memakai aturan tanpa syarat di bawahnya.
273
+ Blur backdrop pakai `-webkit-backdrop-filter` + `backdrop-filter` (Safari lama tetap jalan, hanya
274
+ tanpa blur). Belum ada CSS build/autoprefixer, jadi dukungan di bawah floor itu jadi tanggung jawab
275
+ konsumen.
263
276
 
264
277
  ## Lisensi
265
278
  [MIT](LICENSE) © 2026 Cahyo D. Kurnianto. Bebas dipakai, diubah, dan didistribusikan asal
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.5.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.5.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.7.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.7.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`.
@@ -264,11 +264,23 @@ patterns, and `prefers-reduced-motion` is respected. Status is never conveyed by
264
264
  the "Accessibility" section in `docs/index.html`.
265
265
 
266
266
  ## Browser support
267
- Needs an evergreen browser, roughly 2023+: **Chrome 111 · Safari 16.4 · Firefox 113** (the floor is
268
- set by `color-mix()`). Other features used: native `<dialog>`/`::backdrop`, `accent-color`,
269
- `100dvh`, `conic-gradient`, `scroll-snap`, all of them ≥ Safari 15.4. Backdrop blur uses
267
+ **Chrome 105 · Safari 15.4 · Firefox 121.** The floor is set by `:has()`, and by the Safari 15.4
268
+ generation of native `<dialog>`/`::backdrop`, `accent-color` and `100dvh`. `conic-gradient` and
269
+ `scroll-snap` are older still.
270
+
271
+ `color-mix()` used to set that floor at Safari 16.4, and it no longer does. It was never worth it:
272
+ a declaration that mixes a token carries `var()`, so it is assumed valid at parse time and only
273
+ fails at computed-value time, where the property drops to its *initial* value instead of to a
274
+ fallback line. On an iPhone that stopped at iOS 15 that turned 17 `border-color` declarations into
275
+ `currentColor`, which is a thick black outline on roughly everything. Every alpha in the kit is now
276
+ written `rgba(var(--color-primary-rgb), .26)`, which is the same colour and parses back to Safari 9;
277
+ the four rules that cannot be answered statically carry a real `@supports` counterpart.
278
+ `test/color-mix.test.mjs` keeps the next one honest.
279
+
280
+ The newest things the kit touches (`@container`, `@starting-style`, `text-wrap`) are enhancements: a
281
+ browser without them skips the block and keeps the unconditional rule underneath. Backdrop blur uses
270
282
  `-webkit-backdrop-filter` + `backdrop-filter` (older Safari still works, just without the blur).
271
- There's no CSS build or autoprefixer step, so supporting older browsers is the consumer's
283
+ There's no CSS build or autoprefixer step, so supporting anything below the floor is the consumer's
272
284
  responsibility.
273
285
 
274
286
  ## License