@cahyo-dimas/freeday 3.4.0 → 3.6.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 (37) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/COMPONENTS.md +278 -12
  3. package/README.id.md +9 -5
  4. package/README.md +9 -5
  5. package/adapters/blazor/FdyDateRange.razor.cs +10 -0
  6. package/adapters/blazor/FdyTable.razor +6 -2
  7. package/adapters/blazor/FdyTable.razor.cs +21 -0
  8. package/adapters/blazor/TableTypes.cs +4 -0
  9. package/adapters/react/components/FdyDateRange.tsx +8 -2
  10. package/adapters/react/components/FdyTable.tsx +24 -3
  11. package/adapters/react/index.d.ts +1 -1
  12. package/adapters/vue/components/FdyDateRange.vue +11 -5
  13. package/adapters/vue/components/FdyTable.vue +18 -2
  14. package/dist/freeday-confirm.js +101 -0
  15. package/dist/freeday-copy.js +58 -0
  16. package/dist/freeday-datepicker.js +17 -2
  17. package/dist/freeday-form.js +30 -1
  18. package/dist/freeday-search.js +77 -0
  19. package/dist/freeday.bundle.css +98 -8
  20. package/dist/freeday.css +98 -8
  21. package/dist/freeday.d.ts +17 -2
  22. package/dist/freeday.js +286 -3
  23. package/dist/freeday.media.css +27 -0
  24. package/docs/agent-onboarding.md +10 -0
  25. package/docs/getting-started.md +1 -1
  26. package/package.json +6 -2
  27. package/src/components/avatar.css +2 -0
  28. package/src/components/breakpoints.css +7 -1
  29. package/src/components/card.css +7 -0
  30. package/src/components/composition.css +36 -0
  31. package/src/components/description-list.css +11 -1
  32. package/src/components/filterbar.css +8 -1
  33. package/src/components/selection.css +6 -0
  34. package/src/components/states.css +7 -0
  35. package/src/components/table.css +14 -5
  36. package/tokens/breakpoints.d.ts +4 -0
  37. package/tokens/breakpoints.mjs +22 -8
package/CHANGELOG.md CHANGED
@@ -9,6 +9,133 @@ 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.6.0] - 2026-10-22
13
+
14
+ **Tiga belas hal yang dibangun ulang sebuah app back-office karena kit belum punya.** App-nya tidak
15
+ memakai Freeday (Vue + PrimeVue), jadi yang diangkat adalah polanya, bukan komponennya: setiap
16
+ kandidat dibandingkan dengan kit, dan hanya yang generik dan benar-benar belum ada yang masuk
17
+ (`#059`–`#071`). Satu di antaranya cacat yang kit punya juga: submit yang ditolak memfokus field
18
+ yang tak terlihat. Satu lagi mengubah tampilan setiap tabel, jadi dikirim sebagai opt-in dan
19
+ dijadwalkan jadi default di 4.0 (NEXT-UP #16). Tidak ada tipe publik yang menyempit dan tidak ada
20
+ tampilan yang berubah tanpa diminta, jadi ini rilis minor.
21
+
22
+ ### Added
23
+ - **`Freeday.confirm()`** (`#060`). Pertanyaan ya/tidak sebelum aksi yang tak bisa diulang, tanpa
24
+ merakit dialog sendiri. Mengembalikan `Promise<boolean>`: `true` saat accept, `false` saat reject,
25
+ Escape atau klik backdrop. `details[]` tampil sebagai daftar di bawah pesan, untuk konfirmasi yang
26
+ harus menyebut temuannya. Fokus awal di tombol reject, teks dipasang sebagai teks (bukan HTML), dan
27
+ kedua tombol punya hook `data-fdy-confirm-accept` / `-reject` untuk test. Tipe ada di
28
+ `freeday.d.ts`; Blazor memanggilnya lewat `JS.InvokeAsync<bool>`.
29
+ - **`[data-fdy-search]`** (`#061`, menjawab NEXT-UP #10). `.fdy-input-group` yang mencari saat
30
+ pengetik berhenti sejenak (`data-fdy-search-delay`, default 400 ms), langsung saat Enter, dan
31
+ punya tombol clear. Ketiga jalur berakhir di satu event `fdy-search {term}`. Clear menulis lewat
32
+ setter native dan memicu `input` native, jadi `v-model`, `onChange` React dan `@bind` Blazor ikut.
33
+ - **`[data-fdy-copy]`** (`#062`). Tombol yang menyalin nilainya sendiri lalu memberi toast. Tanpa
34
+ Clipboard API (http) pembaca diberi tahu untuk menyalin manual. Listener-nya didelegasikan di
35
+ `document`, jadi tombol yang dirender belakangan tak perlu hidrasi.
36
+ - Ketiganya dijaga `browser/confirm-search-copy.mjs` (7 spec), dan tiap spec diverifikasi dengan
37
+ mutasi: delapan mutasi pada kode baru, delapan merah.
38
+ - **`settled` pada date range** (`#063`). Vue `@settled`, React `onSettled`, Blazor `Settled`,
39
+ jalur mentah `fdy-daterange-settled`. Terpancar hanya saat kedua ujung terisi atau keduanya
40
+ kosong. Sebelumnya setiap stack memancarkan per ujung, jadi list yang reload saat berubah memuat
41
+ data dengan tanggal awal saja lalu memuat lagi saat tanggal akhir dipilih. `change` dan `v-model`
42
+ tidak berubah. Dijaga `browser/daterange-settled.mjs` (4 spec) + `FdyDateRangeTests` (2 bUnit),
43
+ lima mutasi, lima merah.
44
+ - **`.fdy-dl--grid`** (`#066`). Grid fakta: label di atas nilai, kolom sebanyak yang muat di lebar
45
+ list itu SENDIRI, maksimal tiga. Diukur dari elemen, bukan jendela, jadi markup yang sama tiga
46
+ kolom di area utama dan satu kolom di rail sempit tanpa modifier. Semua `.fdy-dl dd` kini
47
+ `overflow-wrap:anywhere`, jadi email atau nomor panjang tidak lagi tercetak di atas kolom sebelah.
48
+ - **`.fdy-card__head` + `__action`** (`#070`). Header kartu: tanda, judul yang di-truncate, dan
49
+ satu aksi di kanan, dengan garis selebar kartu. Tandanya `.fdy-avatar --sm --square --tone-N`.
50
+ - **`.fdy-avatar--square`** untuk tanda yang bukan orang (logo perusahaan, chip ikon kartu).
51
+ - **Hint state switch** (`#068`). Dua baris `[data-fdy-when="on|off"]` di satu `.fdy-help`; kit
52
+ menampilkan yang cocok dengan state sekarang, tanpa JS. `aria-describedby` menunjuk ke
53
+ pembungkusnya, jadi pembaca layar hanya mendengar baris yang tampil (diverifikasi lewat deskripsi
54
+ aksesibel yang dihitung Chrome).
55
+ - **`.fdy-state--compact`** (`#069`) untuk state di dalam kartu kecil.
56
+ - **Empty state `FdyTable` tahu kenapa ia kosong** (`#069`). Slot Vue `empty` dan fungsi React
57
+ `empty` menerima `{ filtered, clearFilters }`; Blazor punya `EmptyTemplate`
58
+ (`RenderFragment<FdyTableEmptyContext>`). "Tidak ada yang cocok" bisa menawarkan Clear, "belum
59
+ ada data" menawarkan Create.
60
+ - **`.fdy-page__columns`** (`#065`, menutup "Form master-detail / 2 kolom" yang ditunda di
61
+ NEXT-UP). Rail `__lead` / `__aside` di samping `__main`; lebar dipilih dari rail mana yang ada
62
+ (8/4, 3/9, 3/6/3). Saat menumpuk, rail ringkasan didahulukan. Mulai `md` rail sticky dan
63
+ scroll sendiri, jadi total dan status tetap terlihat dan rail yang lebih tinggi dari layar tidak
64
+ menggantung. Diadaptasi dari layout yang di app asalnya menahan seluruh halaman; di kit shell-nya
65
+ tetap scroll halaman, jadi tidak ada consumer yang dipaksa mengubah layout-nya. Dijaga
66
+ `browser/page-columns.mjs` (5 spec, 6 mutasi, 6 merah).
67
+ - **`.fdy-table--hover-actionable`** (`#064`), prop `hoverActionable` / `HoverActionable` di ketiga
68
+ adapter. Hanya baris yang melakukan sesuatu (`rowActivatable`) yang ber-hover (baris terpilih tetap berwarna terpilih);
69
+ baris inert kembali ke warna istirahatnya sendiri, termasuk stripe, dan sel beku tetap opak.
70
+ Opt-in di 3.x supaya tidak ada tabel yang berubah; direncanakan jadi default di 4.0. Hover dasar
71
+ kini membaca `--_fdy-row-hover` (privat), tampilannya tidak berubah. Dijaga
72
+ `browser/table-hover.mjs` (4 spec, 5 mutasi, 5 merah) + `FdyTableHoverTests` (2 bUnit).
73
+ - **Docs: breadcrumb** (`#071 §8`). `aria-current="page"` hanya untuk crumb yang memang halaman ini;
74
+ trail yang berhenti di induknya (form record yang crumb terakhirnya list) membiarkan crumb itu
75
+ tetap link.
76
+ - **Docs: Record actions** (`#067`). Urutan tetap untuk aksi form di `.fdy-page__actions`:
77
+ Find | New, maksimal dua aksi inline, sisanya di menu More dengan urutan deklarasi yang sama dan
78
+ warna danger yang tetap terbawa, Save terakhir dan satu-satunya tombol solid.
79
+ - Dijaga `browser/display-parts.mjs` (4), `browser/table-empty.mjs` (2) dan `FdyTableEmptyTests`
80
+ (2 bUnit). Setiap perilaku baru diverifikasi dengan mutasi (13 mutasi, 13 merah).
81
+ ### Fixed
82
+ - **Date range jalur mentah: ujung yang dihapus kini diumumkan.** Tanggal awal yang melewati
83
+ tanggal akhir mengosongkan tanggal akhir tanpa event apa pun, jadi `To` di Blazor (yang hanya
84
+ mendengar pilihan) tetap memegang tanggal yang sudah tidak ada. Pengosongan itu sekarang
85
+ memancarkan `fdy-datepicker-change` dengan `value: ''`.
86
+ - **Submit yang ditolak kini memperlihatkan field yang ditolaknya** (`#059`). `freeday-form.js`
87
+ memanggil `focus()` pada kontrol invalid pertama dan berhenti di situ. Kalau kontrol itu tidak
88
+ dirender (di dalam `<details>` tertutup, di tab yang tak dipilih, di panel stepper yang sudah
89
+ dilewati), `focus()` tidak melakukan apa pun dan tidak melapor apa pun: field memerah di tempat
90
+ yang tak terlihat, dan tombol Save terbaca mati. Sekarang penghalangnya dibuka dulu lewat kontrol
91
+ milik komponennya sendiri, supaya state komponen itu tetap cocok dengan yang tampil: `<details>`
92
+ di-`open`, tab-nya diklik, tombol step-nya diklik. Step yang **belum dicapai** sengaja tidak
93
+ dilompati, karena itu sama dengan melewati guard `fdy-step-before-change`. Dijaga
94
+ `browser/form-reveal.mjs` (5 spec); empat di antaranya merah sebelum perbaikan.
95
+
96
+ ## [3.5.0] - 2026-09-15
97
+
98
+ **Sebuah angka yang hanya bisa ditemukan dengan membaca stylesheet kit.** `#058` (dari
99
+ `IDU_AI_DOC_SAPB1_CLIENT` dan `IDU_AI_DOC_SAAS`, satu sapuan atas dua konsol) datang dari audit yang
100
+ hasilnya nyaris bersih: tipografi, spasi dan warna tak menyisakan satu pun nilai mentah. Yang tersisa
101
+ cuma media query — dan di situlah dua hal yang sama-sama berakar pada satu kenyataan CSS: `@media`
102
+ tak bisa membaca custom property.
103
+
104
+ Akibatnya angka breakpoint kit hanya hidup sebagai literal, app mengetik ulang literal itu, dan
105
+ literal yang diketik ulang hanyut. Dua konsol itu menuliskan satu lebar yang sama dengan tiga cara
106
+ (`640px`, `40rem`, `45rem`) untuk satu maksud yang sama.
107
+
108
+ ### Added
109
+ - **`@cahyo-dimas/freeday/media`** — berkas `@custom-media` (`dist/freeday.media.css`), jawaban
110
+ sisi-CSS untuk apa yang selama ini hanya dijawab `tokens/breakpoints.mjs` di sisi JS. App menulis
111
+ `@media (--fdy-below-sm)` atau `@media (--fdy-filterbar-stacked)`, bukan literal yang diketik
112
+ ulang, dan karenanya tak bisa hanyut.
113
+ Isinya `--fdy-below-*` / `--fdy-from-*` untuk ramp (`sm`·`md`·`lg`·`xl`), plus `--fdy-nav-drawer`,
114
+ `--fdy-nav-static` dan `--fdy-filterbar-stacked` untuk dua lebar yang bukan anak tangga ramp.
115
+ **Opt-in, dan tak mengubah apa pun bagi yang tidak memakainya:** `@custom-media` adalah transform
116
+ PostCSS (`postcss-custom-media`, sudah termasuk di `postcss-preset-env`), jadi app ber-Vite
117
+ mendapatkannya cuma-cuma dan app tanpa PostCSS tetap menulis literal seperti kemarin. Berkas ini
118
+ **tidak** ikut `freeday.css` maupun `freeday.bundle.css` — at-rule waktu-build tak punya urusan di
119
+ stylesheet yang dimuat tiap app — dan tak membawa satu rule pun, jadi meng-import-nya tak berbiaya
120
+ saat runtime.
121
+ Di-**generate** dari objek yang sama yang diekspor sisi JS, jadi kedua paruh skala itu tak mungkin
122
+ berbeda pendapat; yang menjaganya agar tetap sama dengan CSS yang dideskripsikannya adalah dua tes
123
+ baru di `test/docs.test.mjs`, diverifikasi dengan mutasi.
124
+ - **`breakpoints.filterbar` (640)** — lebar tempat `.fdy-filterbar` menumpuk fieldnya jadi selebar
125
+ penuh, kini bernama, bukan cuma tertulis di `filterbar.css:45`.
126
+
127
+ ### Changed
128
+ - **640 tetap 640, tapi berhenti jadi angka siluman.** Ia bukan anak tangga ramp (`sm` = 600) dan
129
+ sengaja tidak dipindahkan: 640 adalah lebar telepon-lanskap yang nyata, dan menggesernya akan
130
+ me-reflow setiap filter bar di setiap app. Yang salah bukan angkanya, melainkan bahwa satu-satunya
131
+ cara menemukannya adalah membaca stylesheet kit lalu mempercayai sumber di atas dokumentasi.
132
+ Menyelaraskan rule app ke `sm` — hal yang wajar dilakukan dari dokumentasi saja — merusak
133
+ 600–640px: fieldnya sudah ditumpuk kit sementara app masih membatasi lebar field `--w-grow`-nya,
134
+ sehingga field itu terlihat lebih sempit dari tetangganya di tumpukan.
135
+ Header `breakpoints.css`, komentar `filterbar.css`, `COMPONENTS.md` dan kedua README kini menyebut
136
+ `nav` **dan** `filterbar` sebagai dua lebar yang bukan ramp, lengkap dengan pita px yang rusak
137
+ kalau dibulatkan ke anak tangga terdekat.
138
+
12
139
  ## [3.4.0] - 2026-09-07
13
140
 
14
141
  **Tiga hal yang hanya terlihat setelah sebuah app bertemu telepon sungguhan.** `#057` (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
 
@@ -313,10 +388,33 @@ Responsive display: `.fdy-hide-below-sm|md|lg` · `.fdy-hide-above-sm|md|lg`.
313
388
  Breakpoints (min-width): `sm` 600 · `md` 960 · `lg` 1280 · `xl` 1920, importable as
314
389
  `import { breakpoints } from '@cahyo-dimas/freeday/breakpoints'`.
315
390
 
316
- **`breakpoints.nav` (721) is separate and is the one the shell uses.** `.fdy-app` switches the
317
- sidebar from off-canvas drawer to static column at 721px, *not* at `md`. Any `matchMedia` guard or
318
- utility variant that has to agree with the shell must use `nav`; using `md` leaves 721–959px broken
319
- (sidebar already static while your script still treats it as an overlay).
391
+ **Two widths in that object are not ramp steps**, and each is the one to use when your rule has to
392
+ fire with a kit rule. Aligning either to the nearest ramp step leaves a band of broken widths, in
393
+ which the kit has already switched and your app has not:
394
+
395
+ | | px | Governs | Aligning to the ramp instead breaks |
396
+ |---|---|---|---|
397
+ | `breakpoints.nav` | 721 | `.fdy-app` switches the sidebar from off-canvas drawer to static column — and `.fdy-app__main`'s padding step and `.fdy-bottomnav` ride the same width | 721–959px, if you use `md`: sidebar already static while your script still treats it as an overlay |
398
+ | `breakpoints.filterbar` | 640 | `.fdy-filterbar` stacks every field full-width | 600–640px, if you use `sm`: fields already stacked while your rule still caps the `--w-grow` one, so it sits visibly narrower than the fields under it |
399
+
400
+ **In CSS, import the breakpoints instead of retyping them.** `@media` cannot read a custom property,
401
+ so there is no `--breakpoint-*` token and never will be; `@cahyo-dimas/freeday/media` answers it the
402
+ only way CSS can, as a `@custom-media` sheet:
403
+
404
+ ```css
405
+ @import '@cahyo-dimas/freeday/media'; /* once, above the rules that use it */
406
+
407
+ @media (--fdy-filterbar-stacked) { .page-filters > .fdy-field--w-grow { max-width: none } }
408
+ @media (--fdy-nav-drawer) { .page-banner { padding-inline: var(--space-5) } }
409
+ ```
410
+
411
+ Declared: `--fdy-below-sm|md|lg|xl` and `--fdy-from-sm|md|lg|xl` (the ramp, matching the utilities
412
+ above), plus `--fdy-nav-drawer` / `--fdy-nav-static` and `--fdy-filterbar-stacked` for the two
413
+ widths in the table. It is **opt-in**: `@custom-media` is a PostCSS transform
414
+ (`postcss-custom-media`, included in `postcss-preset-env`), so an app that runs PostCSS — anything on
415
+ Vite already does — gets it, and an app that does not keeps writing literals exactly as before. The
416
+ sheet carries no rules, so importing it costs nothing at runtime, and it is deliberately not part of
417
+ `freeday.css` or `freeday.bundle.css`.
320
418
 
321
419
  **Visual style: `data-style="soft | glass"`.** `soft` is the default and the look the kit has always
322
420
  had; `glass` frosts the **raised** surfaces — card, modal, drawer, menu, appbar, the app shell's
@@ -490,7 +588,7 @@ cannot guarantee.
490
588
 
491
589
  | Type | What the UA still draws | What to do |
492
590
  |---|---|---|
493
- | `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. |
494
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. |
495
593
 
496
594
  `number` used to be a third: the UA spin buttons are unthemeable, so `.fdy-input[type="number"]`
@@ -534,6 +632,31 @@ the shared border, focus ring and error promotion. Needs `freeday-number.js`.
534
632
  `aria-label`, so pointer and browse-mode users still get a named control.
535
633
  - `type="button"` is required. Inside a `<form>`, a bare `<button>` submits it.
536
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
+
537
660
  ## Checkbox · radio · switch
538
661
  Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrapping `<label>`;
539
662
  `.fdy-checkbox` on a bare `<input type="checkbox">` used inside tables/trees.
@@ -543,6 +666,23 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
543
666
  <label class="fdy-switch"><input type="checkbox"> Enable notifications</label>
544
667
  ```
545
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
+
546
686
  ## Select / combobox — `.fdy-combo`
547
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).
548
688
 
@@ -834,7 +974,8 @@ page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear b
834
974
  | Prop | Type | What it does |
835
975
  |---|---|---|
836
976
  | `modelValue` · `value` | `DateRangeValue` = `{ start, end }`, each `string \| null` | Both ends at once, so the pair can never be half-applied. |
837
- | `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. |
838
979
  | `min?` · `max?` | `string` | ISO bounds for both ends. The end can never precede the start. |
839
980
  | `locale?` | `string` | As on the single picker. |
840
981
  | `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
@@ -851,7 +992,9 @@ The enhancer builds everything.
851
992
  `__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months` `--years`)
852
993
  `__dow` `__day` `__month` `__year`
853
994
  - Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
854
- 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.
855
998
  **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor). Use it in those stacks.
856
999
  **Getting to a distant date.** The title is a **button**, and it drills up one level each press:
857
1000
  day grid → 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) → 12-cell year grid
@@ -959,6 +1102,13 @@ blur/input. Needs `freeday-form.js`.
959
1102
  - Per-control messages: `data-fdy-msg-required`, `-type`, `-minlength`, `-min`, `-mismatch`, …
960
1103
  - Cross-field: `data-fdy-match="#otherFieldId"`
961
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.
962
1112
 
963
1113
  ## Password & mask
964
1114
  `data-fdy-password` on `<input type="password">` adds a reveal toggle (reusing the input-group
@@ -1057,6 +1207,15 @@ hover and selection are ordered so the later state always wins. Override the col
1057
1207
  Tier-3 token **`--fdy-table-stripe`** (set it on any ancestor; the rule reads it as a `var()`
1058
1208
  fallback, so your value wins without a specificity fight). Frozen columns stay striped.
1059
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
+
1060
1219
  ```html
1061
1220
  <div class="fdy-table-wrap">
1062
1221
  <table class="fdy-table">
@@ -1164,12 +1323,13 @@ provide the state prop for a concern and you own that concern instead. React nam
1164
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. |
1165
1324
  | `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
1166
1325
  | `loading?` | `boolean` | Show the in-table loading state. |
1167
- | `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. |
1168
1327
  | `ariaLabel?` | `string` | Names the table for assistive tech. |
1169
1328
  | `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
1170
1329
  | `rowClass?` | `(row: Row) => string \| undefined` | Per-row class hook, e.g. marking the selected row. |
1171
1330
  | `expandedKeys?` · `renderRowDetail?` | `ReadonlyArray<string \| number>` · `(row) => ReactNode` | Controlled expansion: these keys get a full-width detail row. Vue uses the `row-detail` slot. |
1172
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. |
1173
1333
  | `selectable?` | `boolean` | Render the checkbox column and the bulk bar. |
1174
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`. |
1175
1335
  | `bulkActions?` | `ReactNode` | React. Content in the bulk bar beside the count; Vue uses the `bulk-actions` slot (scoped: `keys`, `clear`), Blazor `BulkContent`. |
@@ -1181,7 +1341,8 @@ provide the state prop for a concern and you own that concern instead. React nam
1181
1341
 
1182
1342
  Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
1183
1343
  `Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
1184
- `FiltersChanged`.
1344
+ `FiltersChanged`. Its filter-aware empty state is `EmptyTemplate`, a
1345
+ `RenderFragment<FdyTableEmptyContext>` (`Filtered`, `ClearFilters`) that wins over `EmptyContent`.
1185
1346
 
1186
1347
  **Select-all ticks the current page, not the whole result set.** A header checkbox that silently
1187
1348
  selects rows the reader cannot see is how a bulk delete goes wrong, so it acts on the rows on
@@ -1309,6 +1470,11 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
1309
1470
  (`aria-hidden="true"`) `__title` `__text` `__actions`. Put it inside the card or table area it
1310
1471
  replaces.
1311
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
+
1312
1478
  ## Charts — `data-fdy-chart`
1313
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.
1314
1480
 
@@ -1410,12 +1576,55 @@ responsive `.fdy-datatable` should become below `md`, rather than a stack of `.f
1410
1576
 
1411
1577
  ## Description list — `.fdy-dl`
1412
1578
  Key–value detail view: `<dl class="fdy-dl">` with `<dt>`/`<dd>` pairs. `--rows` for a row-per-pair
1413
- 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.
1414
1598
 
1415
1599
  ---
1416
1600
 
1417
1601
  # Feedback
1418
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
+
1419
1628
  ## Alert — `.fdy-alert`
1420
1629
  Inline message. Modifiers `--info` `--success` `--warning` `--danger`. Parts `__icon` `__body`
1421
1630
  `__title` `__text` `__close`. The danger variant takes `role="alert"`.
@@ -1453,6 +1662,35 @@ something is in the top layer too. Where the Popover API is unavailable (before
1453
1662
  Safari 17 / Firefox 125) the region falls back to a fixed block at `z-index: 200`, which cannot
1454
1663
  clear an open modal, the message is still announced to a screen reader.
1455
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
+
1456
1694
  ## Tooltip — `.fdy-tooltip`
1457
1695
  Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
1458
1696
  gets `aria-describedby` → the `.fdy-tooltip[role="tooltip"]` `id`.
@@ -1587,6 +1825,12 @@ for primary navigation (see Nav above).
1587
1825
  `.fdy-breadcrumb__item` (last one `aria-current="page"`, no link), `.fdy-breadcrumb__link`, and
1588
1826
  `.fdy-breadcrumb__sep` marked `aria-hidden="true"`.
1589
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
+
1590
1834
  ## Tabs — `.fdy-tabs`
1591
1835
  APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1592
1836
  `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
@@ -1713,7 +1957,7 @@ is `role="group" aria-roledescription="slide"`.
1713
1957
  # Display
1714
1958
 
1715
1959
  ## Card — `.fdy-card`
1716
- Parts `__body` `__title` `__desc` `__footer`. Modifiers:
1960
+ Parts `__head` `__body` `__title` `__desc` `__action` `__footer`. Modifiers:
1717
1961
 
1718
1962
  - `--elevated` raises it (use sparingly; most surfaces are flat)
1719
1963
  - `--interactive` gives cursor + hover-lift to a card that *has* a click handler. **Presentational
@@ -1730,6 +1974,26 @@ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
1730
1974
  - The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
1731
1975
  to the card. That is also what keeps hidden labels inside it from escaping. See *Containment*.
1732
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
+
1733
1997
  ### One card, one primary action, one escape hatch
1734
1998
 
1735
1999
  A card with **two** actions cannot be `--button`: interactive content nested in a `<button>` is
@@ -1790,6 +2054,8 @@ Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks t
1790
2054
  group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
1791
2055
  same-initial avatars. Hash the index off the **full** name, not the initials.
1792
2056
 
2057
+ `--square` for a mark that is not a person: a company logo, a card's icon chip.
2058
+
1793
2059
  Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
1794
2060
  because `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
1795
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.4.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.4.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.6.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.6.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`.
@@ -60,7 +60,7 @@ npm i @cahyo-dimas/freeday
60
60
  ```js
61
61
  import '@cahyo-dimas/freeday/css'; // token + komponen (satu file)
62
62
  import '@cahyo-dimas/freeday'; // semua enhancer JS (auto-init [data-fdy-*])
63
- // granular bila perlu: '@cahyo-dimas/freeday/tokens' · '@cahyo-dimas/freeday/css/components' · '@cahyo-dimas/freeday/enhancers/<nama>'
63
+ // granular bila perlu: '@cahyo-dimas/freeday/tokens' · '@cahyo-dimas/freeday/css/components' · '@cahyo-dimas/freeday/enhancers/<nama>' · '@cahyo-dimas/freeday/media' (breakpoint @custom-media)
64
64
  ```
65
65
  Set tema di root app: `<html data-theme="light" data-density="comfortable">`. `dist/` di-commit &
66
66
  ter-publish → install tanpa build step; minify diserahkan ke bundler konsumen. Karena terbit di
@@ -153,9 +153,13 @@ container-nya, bukan scroll sendiri.
153
153
  menyusut/melebar saat `data-density` berubah.
154
154
  - (roadmap) `data-style` untuk varian visual lain.
155
155
  - Skala breakpoint (`sm`/`md`/`lg`/`xl` = 600/960/1280/1920px, sama dengan utilitas
156
- `src/components/breakpoints.css`) juga tersedia di JS: `import { breakpoints } from
157
- '@cahyo-dimas/freeday/breakpoints'`. Pakai itu untuk menyamakan `matchMedia`/`@media` app-mu
158
- dengan skala Freeday.
156
+ `src/components/breakpoints.css`) tersedia untuk app-mu dalam dua bahasa: `import { breakpoints }
157
+ from '@cahyo-dimas/freeday/breakpoints'` untuk JS (`matchMedia`, varian utility), dan `@import
158
+ '@cahyo-dimas/freeday/media'` untuk CSS, yang mendeklarasikan tiap lebar sebagai `@custom-media`
159
+ sehingga sebuah rule ditulis `@media (--fdy-below-sm)`, bukan literal yang diketik ulang (butuh
160
+ PostCSS; opt-in). Objeknya juga membawa dua lebar yang **bukan** anak tangga ramp dan tak boleh
161
+ dibulatkan ke salah satunya: `nav` (721, saklar drawer↔sidebar milik shell) dan `filterbar` (640,
162
+ tempat `.fdy-filterbar` menumpuk). Lihat [`COMPONENTS.md`](COMPONENTS.md) untuk apa yang diaturnya.
159
163
 
160
164
  ## Integrasi framework (SPA)
161
165
  > **Peta library lengkap:** [`docs/integrations.md`](docs/integrations.md). Tiap area