@cahyo-dimas/freeday 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/COMPONENTS.md +214 -18
  3. package/README.id.md +4 -4
  4. package/README.md +4 -3
  5. package/adapters/blazor/FdyDatepicker.razor +3 -1
  6. package/adapters/blazor/FdyDatepicker.razor.cs +36 -1
  7. package/adapters/blazor/FdyMonthpicker.razor +29 -0
  8. package/adapters/blazor/FdyMonthpicker.razor.cs +102 -0
  9. package/adapters/blazor/FdyTable.razor +6 -1
  10. package/adapters/blazor/FdyTable.razor.cs +33 -0
  11. package/adapters/blazor/FdyTableFooter.razor +6 -6
  12. package/adapters/blazor/FdyTableFooter.razor.cs +36 -0
  13. package/adapters/core/month-grid.d.ts +116 -0
  14. package/adapters/core/month-grid.js +234 -0
  15. package/adapters/react/components/FdyDatepicker.tsx +164 -166
  16. package/adapters/react/components/FdyMonthpicker.tsx +400 -0
  17. package/adapters/react/components/FdyTable.tsx +38 -2
  18. package/adapters/react/components/FdyTableFilter.tsx +13 -1
  19. package/adapters/react/components/FdyTableFooter.tsx +37 -6
  20. package/adapters/react/index.d.ts +6 -2
  21. package/adapters/react/index.js +1 -0
  22. package/adapters/react/usePopover.ts +54 -9
  23. package/adapters/vue/components/FdyDatepicker.vue +158 -160
  24. package/adapters/vue/components/FdyMonthpicker.vue +387 -0
  25. package/adapters/vue/components/FdyTable.vue +33 -1
  26. package/adapters/vue/components/FdyTableFilter.vue +13 -1
  27. package/adapters/vue/components/FdyTableFooter.vue +39 -6
  28. package/adapters/vue/index.d.ts +16 -0
  29. package/adapters/vue/index.js +1 -0
  30. package/adapters/vue/usePopover.ts +53 -10
  31. package/dist/freeday-datepicker.js +68 -14
  32. package/dist/freeday-menu.js +15 -1
  33. package/dist/freeday-popover.js +33 -11
  34. package/dist/freeday-table.js +64 -5
  35. package/dist/freeday-tabs.js +21 -2
  36. package/dist/freeday.bundle.css +50 -2
  37. package/dist/freeday.css +50 -2
  38. package/dist/freeday.js +201 -33
  39. package/docs/agent-onboarding.md +3 -3
  40. package/docs/getting-started.md +6 -6
  41. package/docs/integrations.md +22 -4
  42. package/package.json +2 -2
  43. package/src/components/table.css +19 -2
  44. package/src/components/tabs.css +31 -0
package/CHANGELOG.md CHANGED
@@ -9,6 +9,203 @@ 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.9.0] - 2026-10-23
13
+
14
+ **Rilis ini dimulai sebagai tiga penambahan dan berakhir dengan lima cacat, dan yang menemukan
15
+ kelimanya adalah penambahan itu sendiri.** Pemiliknya memutuskan batas antara kit dan app yang
16
+ memakainya ditentukan oleh satu pertanyaan: apakah sesuatu bisa diadaptasi universal. Di bawah
17
+ ukuran itu beberapa hal yang sebelumnya dicatat sebagai "selera pemakai" atau diparkir menunggu
18
+ pemanggil kedua ternyata pekerjaan kit, dan menutupnya membuka apa yang ada di belakangnya.
19
+
20
+ Satu algoritma hidup di **empat salinan**. `#079` di 3.8.0 memperbaiki posisi panel popover di satu
21
+ di antaranya, dan tiga butir rilis ini adalah akibat langsungnya: salinan bertipe membawa cacat yang
22
+ sama untuk delapan komponen, salinan keempat di enhancer tabel memecah paritas begitu salinan
23
+ bertipe dibetulkan, dan memindahkan panel filter ke tempat yang benar membuka dua hal lagi. Satu di
24
+ antaranya sudah terkirim ke pengguna sejak lama tanpa ada yang melaporkannya: panel filter pada
25
+ tabel di dalam modal terbuka, tergambar, lalu menolak segala masukan.
26
+
27
+ ### Fixed
28
+ - **Cacat urutan `#079` ada di salinan bertipe juga** (`#081`), dan di sana ia kena delapan
29
+ komponen: `FdyDatepicker`, `FdyAutocomplete`, `FdyCascade` dan `FdyCombo`, masing-masing di Vue
30
+ dan React. `adapters/vue/usePopover.ts` dan padanannya di React membaca rect pemicu sebagai hal
31
+ pertama, sama seperti jalur mentah sebelum 3.8.0. Terukur: panel mendarat 741 lawan pemicu 756,
32
+ selisih tepat satu lebar scrollbar klasik. `FdyTableFilter` lolos, dan sebabnya menarik:
33
+ `.fdy-filter` satu-satunya panel kit yang `position:fixed`, jadi dokumen tidak pernah tumbuh dan
34
+ pemicunya tidak pernah bergeser.
35
+ - **Panel filter pada tabel di dalam modal tidak bisa diisi** (`#082`). `showModal()` membuat
36
+ segalanya di luar subtree dialog `inert`, dan panel jalur mentah menempel ke `document.body`,
37
+ yaitu di luar itu. Panelnya terbuka, tergambar, lalu menolak fokus dan klik: `focus()` milik
38
+ enhancer sendiri jadi no-op dan `elementFromPoint` di tengah panel mengembalikan sel tabel di
39
+ belakangnya. Rect tidak bisa melihat ini, jadi yang menemukannya hit test. Panelnya kini anak
40
+ `<th>`-nya sendiri seperti `FdyTableFilter` bertipe, dan salinan keempat logika posisi yang
41
+ memosisikannya ikut terhapus.
42
+ - **Panel filter mewarisi tipografi sel header** (`#083`). Dari sepuluh deklarasi `.fdy-table th`,
43
+ tujuh diwariskan, dan empat sudah ditimpa sejak terbit. Yang lolos: `white-space:nowrap`, yang
44
+ membuat nilai enum panjang melebar sampai `max-width` alih-alih membungkus, plus `font-size` dan
45
+ `text-align`. Sumber kedua dari sel yang sama: `.fdy-table__num` menambahkan
46
+ `font-family:var(--font-mono)`, jadi judul panel tercetak dengan huruf data.
47
+ - **Panel filter jalur merosot terkurung konteks penumpukan sel header** (`#084`).
48
+ `.fdy-table--sticky-head thead th` adalah `position:sticky;z-index:2`, yang membuat konteks
49
+ penumpukan, jadi `z-index:130` milik panel diurutkan di dalam angka 2 itu. Jalur popover lolos
50
+ sebab top layer tidak peduli konteks penumpukan, tetapi Popover API baru ada di Chrome 114,
51
+ Safari 17 dan Firefox 125 sementara lantai dukungan kit Chrome 105, Safari 15.4 dan Firefox 121.
52
+ Terukur di jalur merosot: panel tumpang tindih 224x49px dengan `.fdy-bottomnav`, dan 49px itu
53
+ persis tombol Reset dan Close. Panelnya kini dipasang di akar tabel dan diposisikan lewat
54
+ `place()`. Menaikkan angkanya tidak akan menolong, sebab angka 2 itu yang menahan header beku di
55
+ atas sel badan beku dan menaikkannya di atas 50 akan mencetak seluruh baris header di atas pita
56
+ bawah.
57
+
58
+ ### Added
59
+ - **`FdyTable` meneruskan keenam prop teks pita kakinya** (`#080`), di ketiga adapter. 3.8.0 memberi
60
+ `FdyTableFooter` prop itu, tetapi tabel merender pita kakinya sendiri tanpa meneruskannya, jadi
61
+ app non-Inggris yang memakai pita kaki bawaan tetap Inggris dan jalan keluarnya satu-satunya
62
+ `pager={false}` plus merender pita kakinya sendiri. Prop datar, bukan objek, sebab penjaga
63
+ dokumentasi membaca nama prop satu per satu dan sebuah objek akan menyembunyikan isinya dari
64
+ penjaga itu.
65
+ - **Perataan untuk popover bertipe** (`#081`). `align: 'start' | 'end'`, nama yang sama dengan jalur
66
+ mentah, sebagai argumen posisi keempat `usePopover`. Objek opsi ditolak sebab literal objek di
67
+ `useEffect` React punya identitas baru setiap render sementara primitif masuk ke deps tanpa
68
+ menuntut `useMemo` dari pemanggil. Yang memilih perataan komponennya sendiri, jadi `FdyTable`
69
+ tidak bertambah prop: `FdyTableFilter` menulis `'end'` karena ia memang selalu duduk di header
70
+ kolom.
71
+ - **`matchWidth` di jalur bertipe** (`#082`). Adapter selalu menulis `minWidth` selebar pemicu, dan
72
+ untuk corong filter 24px nilai inline itu mengalahkan `min-width:14rem` di stylesheet: panel
73
+ terukur 206px alih-alih 224. Yang lebarnya bergerak hanya `FdyTableFilter`, jadi hanya filter yang
74
+ mengirim `matchWidth:false`. Keempat panel lain memang harus tetap memakai bawaannya, dan
75
+ alasannya mekanisme yang sama dengan `#079`: ketiga listbox `min-width:100%`, dan di top layer
76
+ `100%` itu lebar viewport.
77
+ - **`loadingText` di `FdyTable` Vue dan React** (`#085`). Blazor punya `LoadingText` sejak awal,
78
+ kedua adapter lain menulis `Loading…` sebagai literal di templat. Sapuan yang menemukannya juga
79
+ mencatat dua belas string di panel filter kolom dan satu nama aksesibel di `FdyCfl` yang masih
80
+ tertutup di ketiga adapter sekaligus; keduanya tercatat dengan nomor baris untuk rilis berikutnya.
81
+ - **`FdyMonthpicker`** (`#086`), di ketiga adapter. 3.8.0 menambahkan `mode="month"` di
82
+ `FdyDatepicker`, dan bentuk itu membuat satu prop membawa dua format dengan prop lain yang memilih
83
+ format mana. Komponen tersendiri menutupnya dengan hitungan prop, bukan dengan alias tipe: tidak
84
+ ada `mode` yang bisa mengubah arti `modelValue`. Gridnya diangkat ke `adapters/core/month-grid.js`
85
+ dan dipakai balik oleh `FdyDatepicker`, jadi implementasi grid bulan di kit tetap dua, bukan tiga;
86
+ kedua datepicker menyusut 87 dan 82 baris. Jalur mentah tidak diubah dan `data-mode="month"` tidak
87
+ ditandai usang, sebab atribut string tidak punya tipe yang bisa berbohong soal isi `data-value`.
88
+
89
+ ### Deprecated
90
+ - **`mode` di `FdyDatepicker`** (`#086`), bersama `FdyDatepickerMode.Month` dan parameter `Mode` di
91
+ Blazor. Tetap bekerja persis seperti di 3.8.0, dan ada uji paritas sel demi sel yang menjaga janji
92
+ itu. Dihapus di 4.0. Penandaannya lewat tipe dan bukan prosa, satu mekanisme per stack
93
+ (`@deprecated` di Vue dan React, `[Obsolete]` di Blazor), masing-masing menyebut penggantinya, dan
94
+ ada uji yang menuntut nama pengganti itu tetap ada di sana. Repo ini belum pernah memensiunkan apa
95
+ pun, jadi ini presedennya.
96
+
97
+ ### Catatan: lencana netral sudah ada
98
+ Catatan app yang jadi sumber 3.8.0 dan 3.9.0 meminta `fdy-badge--neutral` untuk status yang tidak
99
+ boleh terbaca seperti aksi. Kelas itu tidak dibuat, sebab `.fdy-badge--outline` sudah menjawabnya:
100
+ tintanya 7.46:1 di tema terang dan 7.28:1 di gelap terhadap permukaan, jadi ia sudah lewat gerbang
101
+ kontras pada ambang teks untuk ketiga permukaan. Yang ditambahkan dokumentasi yang mengatakannya,
102
+ plus penjaga yang mengikat dokumen itu ke deklarasinya. Separuh usul yang lain, menjadikan netral
103
+ sebagai **bawaan** `.fdy-badge`, tetap milik 4.0 sebab ia mengubah tampilan setiap lencana yang
104
+ sudah terkirim.
105
+
106
+ ### Catatan: dokumen yang menjelaskan varian yang tidak bekerja
107
+ `COMPONENTS.md` menulis bahwa `.fdy-table__num` meratakan ke kanan dan memasangnya di `<th>` pada
108
+ dua contoh. Kelas itu berspesifisitas `(0,1,0)` sementara `.fdy-table th` `(0,2,0)`, jadi di sel
109
+ header huruf data dan angka tabularnya sampai tetapi `text-align:left` dari aturan header yang
110
+ menang. Keduanya lahir di commit yang sama pada Juli dan kalimat dokumennya baru datang pada
111
+ Agustus, tidak ada uji yang pernah menegaskan perataan header lewat kelas itu, dan docs hidup kit
112
+ mengirim bentuk itu di delapan tempat dengan header rata kiri sejak saat itu. Jadi yang dibetulkan
113
+ dokumennya: perilakunya dibiarkan, sebab mengubahnya menggeser setiap header kolom angka yang sudah
114
+ terkirim.
115
+
116
+ ## [3.8.0] - 2026-10-23
117
+
118
+ **Empat penambahan dari catatan yang sama, dan tiga di antaranya ternyata sudah separuh ada di
119
+ kit.** App ronda yang menemukan Safari 15 di 3.7.0 menulis delapan butir; dua di antaranya cacat,
120
+ dan keduanya sudah terbit. Empat dari yang tersisa penambahan, dan keempatnya lebih murah daripada
121
+ tampaknya: satu kelas yang sudah ada sejak lama tetapi kehilangan pengaruhnya, satu jalan keluar
122
+ teks yang cuma dipasang di satu jalur, satu grid yang terpasang sejak 1.32.0 tanpa pernah bisa jadi
123
+ tujuan, dan satu varian sumbu yang papan tombolnya kurang dua literal.
124
+
125
+ Yang terakhir membuka cacat yang digambarkan catatan itu tanpa pelapornya tahu ia sedang
126
+ menggambarkan kit. Ia menulis bahwa `<input type="month">` menamai bulan menurut bahasa peramban,
127
+ bukan `<html lang>`. Kalimat itu berlaku kata per kata untuk `FdyDatepicker` Vue dan React: dokumen
128
+ sudah menjanjikan `<html lang>` sejak lama, cuma enhancer vanilla yang betul melakukannya, dan
129
+ kedua adapter menyerahkan `props.locale` yang `undefined` langsung ke `Intl`.
130
+
131
+ ### Fixed
132
+ - **Popover memposisikan panelnya dari tata letak yang ia batalkan tiga baris kemudian** (`#079`),
133
+ ada sejak 1.4.1 dan kena keenam enhancer yang memakai `attach()`: autocomplete, cascade, combo,
134
+ datepicker, menu dan timepicker. Keenam panel itu `position:absolute` dengan
135
+ `top:calc(100% + var(--space-1))` di stylesheet komponennya, dan aturan author mengalahkan gaya
136
+ UA popover, jadi panel yang baru di-`show()` sudah di top layer sambil masih memakai aturan itu.
137
+ Di top layer `100%` adalah tinggi viewport. Selama satu fungsi sinkron panelnya duduk satu
138
+ viewport di bawah halaman dan overflow dokumen tumbuh mengikutinya, terukur `scrollHeight` 1023
139
+ lawan viewport 900. Di tempat scrollbar memakan ruang tata letak, yakni setiap peramban Windows
140
+ dan Linux, dokumen mendapat scrollbar untuk sesaat, `clientWidth` menyusut selebar scrollbar itu,
141
+ halaman relayout, dan pemicunya bergeser bersamanya. `place()` membaca rect pemicu sebagai hal
142
+ pertama yang ia lakukan, jadi yang ia ukur adalah tata letak sementara itu. Pengukurannya
143
+ sekarang terjadi sesudah panel keluar dari flow.
144
+
145
+ macOS memakai scrollbar overlay yang tidak memakan ruang, jadi mesin tempat kit ini ditulis
146
+ secara struktural tidak bisa melihat cacat ini, dan 1.4.1 sampai 3.7.0 mengirimnya tanpa ada yang
147
+ tahu. Yang melihatnya runner Linux di CI, lewat uji perataan `#074` yang kebetulan mengukur
148
+ koordinat di sisi kanan layar. Reproduksinya sekarang ada di suite dan berjalan di macOS juga,
149
+ dengan menata `::-webkit-scrollbar` ke lebar tetap, plus assertion yang menolak lulus kalau
150
+ enginenya ternyata tidak memberi scrollbar ruang tata letak.
151
+
152
+ ### Added
153
+ - **`align` di popover, dibacakan dari `.fdy-menu--end`** (`#074`). Kelasnya sudah ada sejak lama
154
+ dan stylesheet selalu menghormatinya lewat `left:auto;right:0`. Jalur popover menggantinya dengan
155
+ koordinat `fixed` inline, jadi kelas itu kehilangan pengaruh persis pada saat menu masuk top
156
+ layer: menu akun di talang kanan melewati avatarnya dan berhenti 4px dari tepi layar. Nol kelas
157
+ baru. `freeday-menu.js` yang membaca kelasnya, sebab popover dipakai enam enhancer dan tidak
158
+ boleh tahu nama kelas satu pun. Jepitan viewport sekarang dua sisi: lantai `GAP` yang lama duduk
159
+ di dalam cabang jepitan kanan dan tak pernah berjalan untuk panel rata-kanan, jadi rata-kanan di
160
+ layar sempit cuma akan menukar luber kanan dengan luber kiri.
161
+ - **Enam prop teks di `FdyTableFooter`** (`#075`), di ketiga adapter. `data-fdy-text-<key>` sudah
162
+ lama membuka jalur mentah dan jalur bertipe tidak punya padanannya, jadi app yang tidak berbahasa
163
+ Inggris hanya bisa menulis ulang seluruh pita kakinya. Satu app mengirim 81 baris markup sendiri
164
+ untuk mengganti lima kata. Namanya mengikuti kunci `TEXT` enhancer di mana kuncinya ada (`info`,
165
+ `prev`, `next`), supaya satu teks tidak punya dua nama di dua jalur; tiga sisanya tanpa kembaran
166
+ sebab di jalur mentah markup itu milik app sendiri. Bawaan tetap Inggris di setiap jalur.
167
+ `FdyTable` masih merender pita kakinya sendiri tanpa meneruskan keenamnya; jalan keluarnya
168
+ `pager={false}` lalu render `<FdyTableFooter>` sendiri, dan COMPONENTS.md menuliskannya.
169
+ - **`mode="month"` di `FdyDatepicker`** (`#076`), di ketiga adapter dan jalur mentah. Grid bulannya
170
+ sudah dipasang 1.32.0, hanya tak pernah bisa jadi tujuan. Menjawab `YYYY-MM`, menerima `min` dan
171
+ `max` dalam bentuk yang sama, dan berhenti di grid itu alih-alih turun ke hari. Nama acara dan
172
+ `detail.date` tidak berubah.
173
+ - **`.fdy-tabs--vertical`, dan sumbu papan tombol dari `aria-orientation`** (`#077`). Kelasnya
174
+ memutuskan tampilan, atributnya memutuskan panah, dan satu penanda cukup: enhancer mengisi
175
+ atribut dari kelas kalau markup belum menulisnya, sementara atribut yang sudah ditulis selalu
176
+ menang. Panah kiri dan kanan tidak dipetakan di tegak, mengikuti APG satu sumbu per tablist, dan
177
+ tombol di luar sumbu jatuh tanpa `preventDefault` sehingga tetap milik halaman. Lebar kolom lewat
178
+ `--fdy-tabs-list-w`, bawaan `auto`. Tabs tidak punya wrapper bertipe di stack mana pun, jadi tidak
179
+ ada prop `orientation` yang bisa ditambahkan dan tidak ada wrapper baru yang dibuat demi punya
180
+ tempat menaruhnya.
181
+
182
+ ### Changed
183
+ - **`FdyDatepicker` Vue dan React membaca `<html lang>` kalau `locale` tidak dikirim** (`#078`).
184
+ Ini satu-satunya perubahan perilaku di rilis ini, dan ia kena mode hari juga, bukan cuma mode
185
+ bulan. Sebelumnya `undefined` sampai ke `Intl` apa adanya, yang berarti bahasa peramban: halaman
186
+ Indonesia yang dibuka di peramban Inggris menulis nama bulan Inggris, dan halaman Inggris di
187
+ peramban Indonesia menulis nama bulan Indonesia. Kontrak yang tertulis selalu `<html lang>`, dan
188
+ aturan "Inggris di semua jalur" tidak bisa ditegakkan oleh nilai yang datang dari preferensi
189
+ pembaca. App yang memang menginginkan bahasa peramban mengirim `locale` sendiri. Halaman tanpa
190
+ `lang` jatuh ke `en`, dan `lang=""` ikut ke sana sebab `Intl` melempar `RangeError` atas string
191
+ kosong.
192
+
193
+ ### Catatan dukungan peramban
194
+ Floor-nya tetap Chrome 105, Safari 15.4 dan Firefox 121. Tab tegak sengaja tidak memakai `:has()`
195
+ walau itu akan memendekkan CSS-nya, sebab `:has()` baru ada di Safari 15.4 sementara lantai yang
196
+ pernah dilaporkan dari perangkat nyata adalah iOS 15.0.
197
+
198
+ ### Yang perlu dibuang app saat upgrade
199
+ - Tambalan `translate` apa pun yang menggeser menu rata-kanan ke dalam talangnya. Menu itu sekarang
200
+ diposisikan benar, jadi tambalannya menggeser sejauh satu talang ke arah yang salah.
201
+ - Markup pita kaki tabel yang ditulis tangan cuma karena teksnya tak bisa diganti, berikut CSS
202
+ lokal yang menemaninya.
203
+
204
+ ### Belum masuk
205
+ Butir keempat catatan itu, bawaan `.fdy-badge` yang berwarna primer sehingga status netral terbaca
206
+ seperti aksi, menunggu 4.0: mengubah bawaannya mengubah tampilan setiap lencana yang sudah terkirim.
207
+ Pilihan yang tidak mematahkan apa pun, `fdy-badge--neutral` sebagai opt-in, masih terbuka lebih awal.
208
+
12
209
  ## [3.7.0] - 2026-10-23
13
210
 
14
211
  **Kit ini tidak bisa dipakai di Safari 15, dan yang menemukannya adalah sebuah HP.** Pemilik sebuah
package/COMPONENTS.md CHANGED
@@ -34,9 +34,9 @@ live docs.
34
34
  invalid state does not belong to the control itself.
35
35
  6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
36
36
  alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
37
- 7. **On Vue, React or Blazor, eleven components have a typed wrapper. Use it.** `FdyCombo` ·
38
- `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` ·
39
- `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
37
+ 7. **On Vue, React or Blazor, twelve components have a typed wrapper. Use it.** `FdyCombo` ·
38
+ `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` ·
39
+ `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
40
40
  `Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
41
41
  those stacks *looks* right at first, because the enhancer initialises once and the first render is
42
42
  correct. Then
@@ -510,8 +510,10 @@ WAI-ARIA APG menu-button. Keyboard ↑/↓/Home/End/Esc handled by the enhancer.
510
510
 
511
511
  - Wrapper: `.fdy-menu-wrap` (or `.fdy-btn-split` for a split button, with
512
512
  `.fdy-btn-split__toggle` as the second button) + `data-fdy-menu`
513
- - List: `.fdy-menu` (`--end` aligns right), items `.fdy-menu__item` (`--danger`), separator
514
- `<hr class="fdy-menu__sep">`
513
+ - List: `.fdy-menu` (`--end` puts the panel's **right edge on the trigger's right edge**, which is
514
+ what a control parked in a page gutter needs; on a screen too narrow for that the panel slides
515
+ back inside the viewport instead of hanging off the left edge), items `.fdy-menu__item`
516
+ (`--danger`), separator `<hr class="fdy-menu__sep">`
515
517
  - A11y: trigger gets `aria-haspopup="menu"` + `aria-expanded="false"`; `<ul role="menu" hidden>`;
516
518
  each `<li role="none">` wraps a `<button role="menuitem">`.
517
519
 
@@ -526,6 +528,15 @@ WAI-ARIA APG menu-button. Keyboard ↑/↓/Home/End/Esc handled by the enhancer.
526
528
  </div>
527
529
  ```
528
530
 
531
+ `.fdy-menu--end` is honoured on both of the paths an open menu can take. The stylesheet handles the
532
+ one where the panel stays in the flow, and the enhancer handles the other: it lifts the panel into
533
+ the top layer so it escapes any ancestor overflow clip, and positions it there with coordinates of
534
+ its own, so the modifier has to be read by `freeday-popover.js` too. An app reported what happens
535
+ when it is not. An account menu beside its avatar in a 24px gutter opened 20px further right than
536
+ the avatar and stopped 4px off the screen edge, and the app's own `translate` patch for it was keyed
537
+ to a constant inside the kit. For a control the kit does not ship, the same option is
538
+ `FreedayPopover.attach(panel, trigger, { align: 'end' })`.
539
+
529
540
  ---
530
541
 
531
542
  # Forms
@@ -942,9 +953,10 @@ a new class.
942
953
 
943
954
  | Prop | Type | What it does |
944
955
  |---|---|---|
945
- | `modelValue` · `value` | `string \| null` | ISO `YYYY-MM-DD`, or `''`/`null` for empty. |
956
+ | `modelValue` · `value` | `string \| null` | ISO `YYYY-MM-DD`, or `YYYY-MM` under `mode="month"`. `''`/`null` for empty. |
946
957
  | `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`. Clearing sends `''`. |
947
- | `min?` · `max?` | `string` | ISO bounds; days outside them cannot be picked. |
958
+ | `mode?` | `'day' \| 'month'` | **Deprecated, use `<FdyMonthpicker>`.** `day` is the calendar as it always was. `month` opens on the month grid, stops there, and answers with `YYYY-MM`. Blazor takes the enum `FdyDatepickerMode`. Still works, unchanged; removed in 4.0. See below for why, and for the raw path, which keeps `data-mode="month"`. |
959
+ | `min?` · `max?` | `string` | ISO bounds; days outside them cannot be picked. Month mode also accepts `YYYY-MM`, and a bound inside a month leaves that month pickable. |
948
960
  | `locale?` | `string` | BCP-47 tag deciding month and weekday names. Defaults to the page's `<html lang>`, then `en`. |
949
961
  | `placeholder?` | `string` | |
950
962
  | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
@@ -969,6 +981,33 @@ Blazor carries the same surface bar four: `locale` (the enhancer formats through
969
981
  page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear button), and
970
982
  `ariaLabelledby` (the trigger it builds is named by `Label`).
971
983
 
984
+ ### Props — `<FdyMonthpicker>`
985
+
986
+ > Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). It renders the same trigger
987
+ > and panel as the date picker, over the same classes, and shares its month and year grids.
988
+
989
+ | Prop | Type | What it does |
990
+ |---|---|---|
991
+ | `modelValue` · `value` | `string \| null` | The chosen month, ISO `YYYY-MM`. `''`/`null` for empty. One format, always: nothing on this component can change it. |
992
+ | `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`. Clearing sends `''`. |
993
+ | `min?` · `max?` | `string` | ISO `YYYY-MM` bounds (a full `YYYY-MM-DD` is read too, as its own month). A bound that falls inside a month leaves that month pickable. |
994
+ | `locale?` | `string` | BCP-47 tag deciding the month names. Defaults to the page's `<html lang>`, then `en`. |
995
+ | `placeholder?` | `string` | Shown while nothing is picked. Default `Select month`. |
996
+ | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
997
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its month, but it cannot be opened, cleared or changed. |
998
+ | `clearable?` | `boolean` | Show a × in the trigger once a month is set, so an optional month can be unset. Sends `''`. Off by default. |
999
+ | `clearLabel?` | `string` | Accessible name for that ×. Default `Clear month`. |
1000
+ | `prevYearLabel?` · `nextYearLabel?` | `string` | The arrows beside the month grid, which step a year. Defaults `Previous year` / `Next year`. |
1001
+ | `chooseYearLabel?` | `string` | The title button that drills into the year grid. Default `Choose year`. |
1002
+ | `prevYearsLabel?` · `nextYearsLabel?` | `string` | Page arrows, shown in the year grid. Defaults `Previous years` / `Next years`. |
1003
+
1004
+ There is no `prevMonthLabel` or `nextMonthLabel` here. The arrows sit beside a month grid, so they
1005
+ step a year, and the day grid they would have belonged to does not exist on this component. Blazor
1006
+ carries the same surface bar three: `locale` (the enhancer formats through `Intl` from the page's
1007
+ `<html lang>`), `clearable` and `clearLabel` (that path builds no clear button) and
1008
+ `ariaLabelledby` (the trigger it builds is named by `Label`). It adds `BackToMonthsLabel`, the year
1009
+ grid's own title.
1010
+
972
1011
  ### Props — `<FdyDateRange>`
973
1012
 
974
1013
  | Prop | Type | What it does |
@@ -1013,7 +1052,39 @@ months, `Home`/`End` January/December, `PageUp`/`PageDown` a year, `Enter`/`Spac
1013
1052
  year grid likewise: arrows ±1/±3 years, `Home`/`End` the ends of the page, `PageUp`/`PageDown` a
1014
1053
  page of twelve.
1015
1054
 
1016
- - Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
1055
+ **Picking a month, not a day.** `<FdyMonthpicker>` answers with `YYYY-MM`. The panel opens on the
1056
+ 12-cell month grid, a click or `Enter` commits from it, and there is no day grid to fall into. The
1057
+ title still steps up to the year grid and back down, because a year is navigation and never an
1058
+ answer. On the raw path the same thing is `data-mode="month"` on a `data-fdy-datepicker` div.
1059
+
1060
+ This exists because `<input type="month">` names the month in the **browser's** language, which an
1061
+ Indonesian page read on an English phone cannot override. The month cells, the cell's accessible
1062
+ name and the value in the trigger all go through `Intl` with the page's `<html lang>`, the same
1063
+ route the day grid's month names have always taken, so `lang="id"` reads `Oktober 2026` with nothing
1064
+ configured. Keyboard in the month grid: arrows move one and three months, `Home`/`End` jump to
1065
+ January and December, `PageUp`/`PageDown` step a year, and `Enter`/`Space` commits. The cell keeps
1066
+ its name when it becomes selected and carries the state in `aria-selected`, as the day cells do.
1067
+
1068
+ **`mode="month"` on `<FdyDatepicker>` is deprecated, and still works.** It shipped in 3.8.0 and
1069
+ behaves exactly as it did, so nothing has to move today; it goes in 4.0. The type was the defect.
1070
+ That prop left `modelValue` / `value`, `min` and `max` each carrying `YYYY-MM-DD` or `YYYY-MM` while
1071
+ a *different* prop decided which, and a union discriminated by a sibling prop is a shape neither
1072
+ `defineProps<{...}>` nor an exported `FdyDatepickerProps` can hold. A separate component states it
1073
+ in one line per prop. A consumer's editor says so as well: `mode` is marked `@deprecated` in both
1074
+ adapters, and `Mode` together with `FdyDatepickerMode.Month` carries `[Obsolete]` in Blazor, each
1075
+ naming `FdyMonthpicker`.
1076
+
1077
+ **The raw path keeps `data-mode="month"`, and it is not deprecated.** `data-mode` is a string
1078
+ attribute with no type to be wrong about: a reader of the markup sees `data-mode` and `data-value`
1079
+ side by side and can tell what the value holds. The typed signatures were what could not say it, so
1080
+ the retirement is theirs. Shipping a second attribute would leave two spellings of one feature on
1081
+ the one path that never had the problem.
1082
+
1083
+ Nothing changes without either of them. A picker with no `mode` and no `data-mode` is the day picker
1084
+ it has always been, down to the `YYYY-MM-DD` it reports.
1085
+
1086
+ - Attributes: `data-value="YYYY-MM-DD"`, `data-mode="month"`, `data-placeholder`, `data-label`,
1087
+ `data-fdy-no-icon`
1017
1088
 
1018
1089
  ```html
1019
1090
  <div class="fdy-field">
@@ -1021,6 +1092,12 @@ page of twelve.
1021
1092
  <div data-fdy-datepicker data-value="2026-07-21" data-label="Invoice date"></div>
1022
1093
  </div>
1023
1094
 
1095
+ <div class="fdy-field">
1096
+ <span class="fdy-label">Reporting month</span>
1097
+ <!-- Typed stacks: <FdyMonthpicker>, see its props above. -->
1098
+ <div data-fdy-datepicker data-mode="month" data-value="2026-10"></div>
1099
+ </div>
1100
+
1024
1101
  <div class="fdy-daterange" data-fdy-daterange role="group" aria-label="Reporting period">
1025
1102
  <div data-fdy-datepicker data-role="from" data-placeholder="From" data-label="Start date"></div>
1026
1103
  <span class="fdy-daterange__sep" aria-hidden="true">–</span>
@@ -1199,7 +1276,16 @@ over the connector line, so omitting `__btn` cannot make the connector draw thro
1199
1276
  ## Table — `.fdy-table`
1200
1277
  Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-table-scroll`
1201
1278
  (horizontal scroll). Always a `<caption>` (use `.fdy-visually-hidden` if it shouldn't show) and
1202
- `<th scope="col">`. Numeric cells get `.fdy-table__num` (right-aligned, tabular).
1279
+ `<th scope="col">`. Numeric body cells get `.fdy-table__num` (right-aligned, tabular figures, data
1280
+ font).
1281
+
1282
+ **`.fdy-table__num` aligns a `<td>`, not a `<th>`.** The class is specificity `(0,1,0)` and
1283
+ `.fdy-table th` is `(0,2,0)`, so on a header cell the data font and the tabular figures arrive but
1284
+ `text-align:left` from the header rule wins: the label stays left while the numbers under it go
1285
+ right. Put the class on the header anyway for the font, then add `style="text-align:right"` to move
1286
+ the label with them. That inline value is exactly what the typed `FdyTable` writes for a column with
1287
+ `align: 'right'`, and it is the only thing no ancestor rule can outrank. The class is deliberately
1288
+ left at its own weight: raising it would shift every numeric heading already shipped.
1203
1289
 
1204
1290
  **`--striped`** zebras the even body rows. The tint is deliberately *half* a surface step, not the
1205
1291
  full one hover uses, so hover still reads on a striped row instead of matching it — and stripe,
@@ -1220,7 +1306,7 @@ for 4.0.
1220
1306
  <div class="fdy-table-wrap">
1221
1307
  <table class="fdy-table">
1222
1308
  <caption>Recent invoices</caption>
1223
- <thead><tr><th scope="col">Invoice</th><th scope="col" class="fdy-table__num">Amount</th></tr></thead>
1309
+ <thead><tr><th scope="col">Invoice</th><th scope="col" class="fdy-table__num" style="text-align:right">Amount</th></tr></thead>
1224
1310
  <tbody>
1225
1311
  <tr><td>INV-1042</td><td class="fdy-table__num">1,240,000</td></tr>
1226
1312
  </tbody>
@@ -1252,7 +1338,7 @@ sum, so measure them once after render (and on resize) and set the variable.
1252
1338
  <caption class="fdy-visually-hidden">Exchange rates, August 2026</caption>
1253
1339
  <thead><tr>
1254
1340
  <th scope="col" class="fdy-table__freeze fdy-table__freeze--edge">Date</th>
1255
- <th scope="col" class="fdy-table__num">USD</th>
1341
+ <th scope="col" class="fdy-table__num" style="text-align:right">USD</th>
1256
1342
  </tr></thead>
1257
1343
  <tbody>
1258
1344
  <tr>
@@ -1281,7 +1367,7 @@ the two ever disagree.
1281
1367
  | `label` | `string` | Header label. |
1282
1368
  | `labelHidden` | `boolean` | Render the label for assistive tech only; the header cell looks empty. |
1283
1369
  | `sortable` | `boolean` | Show a sort toggle in the header. |
1284
- | `filter` | `'text' \| 'enum' \| 'number' \| 'date'` | Offer a column filter of this type (the funnel popover). |
1370
+ | `filter` | `'text' \| 'enum' \| 'number' \| 'date'` | Offer a column filter of this type (the funnel popover). The panel is several times wider than the funnel, so it hangs from the funnel's RIGHT edge and the rightmost column keeps the page's gutter; near the left edge of a narrow screen it slides right instead of off it. |
1285
1371
  | `align` | `'left' \| 'right' \| 'center'` | Cell alignment. Default `left`. |
1286
1372
  | `mono` | `boolean` | Render cells in the monospace data font (`.fdy-mono`). |
1287
1373
  | `sortType` | `'text' \| 'number' \| 'date'` | Override the comparator; defaults to one derived from `filter`, else `text`. |
@@ -1322,7 +1408,7 @@ provide the state prop for a concern and you own that concern instead. React nam
1322
1408
  | `pageIndex?` · `onPageIndexChange?` | `number` · `(index) => void` | Client mode: own the 0-based index while the table keeps filtering, sorting and paginating — this is what lets one external pager drive both a table and a card list. |
1323
1409
  | `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. |
1324
1410
  | `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
1325
- | `loading?` | `boolean` | Show the in-table loading state. |
1411
+ | `loading?` · `loadingText?` | `boolean` · `string` | Show the in-table loading row, and the word in it. Default `Loading…`, the same name and the same default as Blazor's `LoadingText`, so a screen in another language translates the loading row the way it already translates the empty one instead of finding that only one of the two neighbouring states has a prop. |
1326
1412
  | `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. |
1327
1413
  | `ariaLabel?` | `string` | Names the table for assistive tech. |
1328
1414
  | `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
@@ -1335,14 +1421,16 @@ provide the state prop for a concern and you own that concern instead. React nam
1335
1421
  | `bulkActions?` | `ReactNode` | React. Content in the bulk bar beside the count; Vue uses the `bulk-actions` slot (scoped: `keys`, `clear`), Blazor `BulkContent`. |
1336
1422
  | `selectedText?` · `clearSelectionText?` | `string` | Bulk-bar copy. Defaults `{n} selected` (with `{n}` substituted) and `Clear`. |
1337
1423
  | `selectAllLabel?` · `selectRowLabel?` · `bulkLabel?` | `string` | Accessible names for the header checkbox, each row checkbox, and the bulk-bar region. Defaults `Select all rows on this page`, `Select row`, `Bulk actions`. |
1424
+ | `infoText?` · `pageSizeLabel?` | `string` | The footer this table draws itself: its range line (`{from}`, `{to}`, `{total}` substituted, every occurrence) and the visible word beside its rows-per-page control. Same names, same substitution and the same English defaults as `<FdyTableFooter>` below, `Showing {from}–{to} of {total}` and `Rows`, so a screen that moves between the table's footer and its own moves a prop instead of rewriting it. |
1425
+ | `paginationLabel?` · `prevLabel?` · `nextLabel?` · `pageLabel?` | `string` | The accessible names inside that footer: the pager landmark, both arrows, and a numbered page button (`{n}` substituted). Defaults `Pagination`, `Previous page`, `Next page`, `Go to page {n}`. |
1338
1426
  | `renderCell?` | `(column, row, value) => ReactNode` | React. Custom cell rendering; Vue uses the `cell` slot. |
1339
1427
  | `toolbar?` | `ReactNode` | React. Content above the table; Vue uses the `toolbar` slot. |
1340
1428
  | `onProcess?` | `(result: { rows, total }) => void` | React. The processed page after filter/sort/paginate, in **both** modes. Vue emits `process`, Blazor calls it `Process`. Render the same set elsewhere — a card list, a summary, a CSV export — without re-deriving the pipeline. |
1341
1429
 
1342
- Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
1343
- `Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
1344
- `FiltersChanged`. Its filter-aware empty state is `EmptyTemplate`, a
1345
- `RenderFragment<FdyTableEmptyContext>` (`Filtered`, `ClearFilters`) that wins over `EmptyContent`.
1430
+ Blazor matches this surface (`EmptyContent` in place of `empty`, `Toolbar` and `RowDetail` as
1431
+ `RenderFragment`s) and is the one adapter that also exposes `FiltersChanged`. Its filter-aware
1432
+ empty state is `EmptyTemplate`, a `RenderFragment<FdyTableEmptyContext>` (`Filtered`,
1433
+ `ClearFilters`) that wins over `EmptyContent`.
1346
1434
 
1347
1435
  **Select-all ticks the current page, not the whole result set.** A header checkbox that silently
1348
1436
  selects rows the reader cannot see is how a bulk delete goes wrong, so it acts on the rows on
@@ -1408,6 +1496,21 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
1408
1496
  labels, the timepicker's, the cascade's two defaults, the table's `Min`/`Maks` filter
1409
1497
  placeholders and the chart's `Seri 1` all sat outside it.
1410
1498
 
1499
+ **Where a filter panel opens, on both paths.** `.fdy-filter` is lifted into the top layer and
1500
+ anchored to its funnel by `freeday-popover.js` on the raw path and by `usePopover` on the typed one,
1501
+ so the raw path needs that file loaded beside `freeday-table.js` (`dist/freeday.js` has both). Four
1502
+ things follow, and the raw path moved to meet the typed one in 3.9.0 rather than the other way
1503
+ round. The panel's RIGHT edge lines up with the funnel's, because a 24px icon in a column header is
1504
+ no guide to where a 14rem form should hang, and the rightmost column is where that used to push the
1505
+ panel off the page; it is clamped to stay on screen either way, so the leftmost column slides right
1506
+ instead of hanging off. It sits 4px below the funnel, the kit's one gap, where the raw path used to
1507
+ keep 4px below and 8px from the viewport edge. It flips above the funnel when there is no room
1508
+ below, which the raw path never did. And it keeps the `min-width:14rem` the stylesheet asks for,
1509
+ which the typed path used to lose to an inline floor taken from the funnel's own 24px. The panel is
1510
+ a child of its own `<th>` on both paths: the top layer is what escapes a card's `overflow:hidden`
1511
+ now, and a panel parked on `<body>` is outside any open modal `<dialog>`, which makes it inert, so a
1512
+ filter in a table inside a modal painted a form that refused every click.
1513
+
1411
1514
  **Testing note:** a column's filter button and the dialog it opens deliberately share one
1412
1515
  accessible name (`Filter <column>`), since a dialog named after its trigger is the normal pattern. In a
1413
1516
  Playwright/Testing-Library suite that means `getByLabel('Filter Name')` resolves to two elements;
@@ -1440,6 +1543,32 @@ render it once, outside both, with `pager={false}` on the table.
1440
1543
  | `page` | `FdyPageState` = `{ index, size, total }` | The page being shown. `size` drives both the range text and the rows-per-page control's value. |
1441
1544
  | `onPageChange?` | `(page: FdyPageState) => void` | React. A page click or a size change. Vue emits `update:page`; Blazor `PageChanged`. |
1442
1545
  | `pageSizes?` | `readonly number[]` | Offer a rows-per-page control. Omit it and the footer is range + pager only. Picking a size reports the new `size` together with the index that still holds the row the reader was looking at. |
1546
+ | `infoText?` | `string` | The range line, with `{from}`, `{to}` and `{total}` substituted. Default `Showing {from}–{to} of {total}`, the enhancer's `info` string. |
1547
+ | `pageSizeLabel?` | `string` | The word beside the rows-per-page control, which also names that control for a screen reader. Default `Rows`. |
1548
+ | `paginationLabel?` | `string` | Accessible name of the pager landmark. Default `Pagination`. |
1549
+ | `prevLabel?` | `string` | Accessible name of the previous-page arrow. Default `Previous page`. |
1550
+ | `nextLabel?` | `string` | Accessible name of the next-page arrow. Default `Next page`. |
1551
+ | `pageLabel?` | `string` | Accessible name of a numbered page button, with `{n}` substituted. Default `Go to page {n}`. |
1552
+
1553
+ **Saying it in another language.** The raw path has had `data-fdy-text-<key>` since 1.39.0 and this
1554
+ one had nothing, so a Vue, React or Blazor app that does not speak English could only rewrite the
1555
+ band by hand. One did: eighty one lines of its own markup, to change five words. The six string
1556
+ props above are the hatch that was missing. Where the enhancer already keys a string the prop
1557
+ carries that key's name, `infoText` for `info`, `prevLabel` for `prev`, `nextLabel` for `next`,
1558
+ and the same `{from}` `{to}` `{total}` `{n}` substitution, every occurrence replaced, so an app on
1559
+ both paths writes one wording once. The other three have no enhancer twin because on the raw path
1560
+ that markup is the app's own: the word beside the rows-per-page control, the pager landmark's name,
1561
+ and the name of a numbered page button. It is `pageSizeLabel` and not `rows`, since the enhancer's
1562
+ `rows` key is the table's `{n} rows` count, a different string.
1563
+
1564
+ Omit a prop and you get the English above, so nothing already shipped moves. `FdyTable` takes all
1565
+ six under the same names and hands them to the footer it draws itself, so a table keeps its own
1566
+ footer and still speaks the app's language. For one version it did not, and the only way out was to
1567
+ switch the footer off: a hatch that opened only if you stopped using the table.
1568
+
1569
+ `pager={false}` plus your own `<FdyTableFooter>` is still the route for a **responsive** list, where
1570
+ one band has to serve a table at `lg` and a card list below it, and that is now the only reason to
1571
+ reach for it. Wording alone is not one.
1443
1572
 
1444
1573
  **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
1445
1574
  than one page. Two ways to take it over:
@@ -1832,10 +1961,15 @@ record. A crumb for a menu group that has no page of its own is plain text in th
1832
1961
  to nowhere.
1833
1962
 
1834
1963
  ## Tabs — `.fdy-tabs`
1835
- APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1964
+ APG tabs: arrows, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1836
1965
  `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
1837
1966
  so the same look serves routed sub-navigation built from plain links. See the note under Nav.
1838
1967
 
1968
+ **Which arrows move the selection** comes from the tablist's `aria-orientation`: Left and Right by
1969
+ default, Up and Down when it says `vertical`. One axis per tablist, the way APG maps it. The other
1970
+ pair stays unmapped, so Left and Right over a column of tabs still reach whatever else on the page
1971
+ wants them. Home and End answer either way.
1972
+
1839
1973
  > **The strip's line is an inset `box-shadow`, not a `border-bottom`** (`#052`; through 3.1.0 it
1840
1974
  > was a border, so an app upgrading past that release changes the override with it). To
1841
1975
  > remove it — a strip inside a header that draws its own rule — override `box-shadow:none`;
@@ -1857,6 +1991,56 @@ so the same look serves routed sub-navigation built from plain links. See the no
1857
1991
  </div>
1858
1992
  ```
1859
1993
 
1994
+ ### Vertical — `.fdy-tabs--vertical`
1995
+
1996
+ The same tabs turned down the page: the list on the leading side, the panel beside it. For a
1997
+ settings screen whose sections are a list, not a strip.
1998
+
1999
+ ```html
2000
+ <div class="fdy-tabs fdy-tabs--vertical" data-fdy-tabs style="--fdy-tabs-list-w:16rem">
2001
+ <div class="fdy-tabs__list" role="tablist" aria-orientation="vertical" aria-label="Settings">
2002
+ <button class="fdy-tabs__tab" role="tab" id="s-1" aria-controls="sp-1" aria-selected="true">Identity</button>
2003
+ <button class="fdy-tabs__tab" role="tab" id="s-2" aria-controls="sp-2" aria-selected="false" tabindex="-1">Posts</button>
2004
+ </div>
2005
+ <div class="fdy-tabs__panel" role="tabpanel" id="sp-1" aria-labelledby="s-1" tabindex="0">…</div>
2006
+ <div class="fdy-tabs__panel" role="tabpanel" id="sp-2" aria-labelledby="s-2" tabindex="0" hidden>…</div>
2007
+ </div>
2008
+ ```
2009
+
2010
+ - **Two markers, and they are not the same marker.** `.fdy-tabs--vertical` decides the look,
2011
+ because the two-column box belongs to `.fdy-tabs` and a stylesheet cannot reach a parent from a
2012
+ child. `aria-orientation="vertical"` on the tablist decides the keyboard, because that is what an
2013
+ assistive technology announces and APG asks for it on a vertical tablist regardless. Write
2014
+ **either** one: `freeday-tabs.js` fills in the attribute when it finds the class without it, so
2015
+ the ARIA can never drift from the layout. An `aria-orientation` you wrote always wins, including
2016
+ when you write `horizontal` on a widget that carries the class.
2017
+ - **Selection is a soft fill.** A bar down the leading edge would be the wrong translation:
2018
+ sideways the marker is an underline beneath the tab it belongs to, and turned ninety degrees that
2019
+ line lands between two tabs and points at the wrong one. So the vertical variant spends
2020
+ `--color-primary-soft` behind `--color-primary-strong`, the same pair
2021
+ `.fdy-nav__item[aria-current="page"]` and the picker options already use for “this is where you
2022
+ are” in a vertical list. The strip's own line (`box-shadow`) is dropped with the underline.
2023
+ - **Column width:** `--fdy-tabs-list-w` on `.fdy-tabs` (default `auto`, so the column hugs the
2024
+ longest label). Set the token rather than a `width` on `.fdy-tabs__list`: the list is a flex item
2025
+ at `flex:none`, so a width arriving from outside has no effect and no error to show for it.
2026
+ - **It does not collapse on a narrow screen**, and the breakpoint is yours because the kit cannot
2027
+ know whether these tabs span the viewport or sit in a 20rem drawer. Going back to a strip takes
2028
+ three declarations, not one, since the variant turns both boxes:
2029
+
2030
+ ```css
2031
+ @media (max-width: 63.9375rem) {
2032
+ .settings-tabs.fdy-tabs--vertical { display: block; }
2033
+ .settings-tabs .fdy-tabs__list { flex-direction: row; width: auto; overflow-x: auto; }
2034
+ .settings-tabs .fdy-tabs__panel { padding: var(--space-4) 0 0; }
2035
+ }
2036
+ ```
2037
+
2038
+ The keyboard stays on Up and Down across that switch, because `aria-orientation` is an attribute
2039
+ and a media query cannot reach it. A list of sections is still a list of sections at 320px, which
2040
+ is why the axis is read from the attribute and never from the painted layout.
2041
+ - Measured in `browser/tabs-vertical.mjs`, which also holds the line that a strip with no
2042
+ `aria-orientation` behaves and paints exactly as it did before the variant existed.
2043
+
1860
2044
  ---
1861
2045
 
1862
2046
  # Overlays
@@ -2044,6 +2228,18 @@ the rest, so two states a workflow distinguishes never share a look. Map each st
2044
2228
  tone, never to a hash of the string: a status is a closed set, and a hash reassigns colours the
2045
2229
  moment the set grows.
2046
2230
 
2231
+ **Neutral status takes `--outline`.** A badge with no modifier is the page's accent: primary ink
2232
+ on a primary tint, the same pair the primary button wears, so a status that means nothing in
2233
+ particular ("Not scheduled", "Holiday", "Read 0 of 3") ends up looking like something to click.
2234
+ `--outline` is the neutral one: no fill, a `--color-border-strong` edge, and `--color-text-muted`
2235
+ ink at 7.46:1 on the light surface, 7.28:1 on the dark one.
2236
+
2237
+ There is no filled neutral on purpose. The only neutral fill the token set offers is one surface
2238
+ step, `--color-surface-2`, and a status pill sits either on `--color-surface` or on the app ground,
2239
+ which is `--color-surface-2` itself, so the pill's body would land between 1.00:1 and 1.10:1
2240
+ against what is behind it: a badge whose shape you cannot see. The semantic fills live at 1.03:1 to
2241
+ 1.44:1 and read clearly anyway because they carry hue, and a neutral has no hue to carry.
2242
+
2047
2243
  **Notification badge is the overlay variant**, not a separate component: wrap anything in
2048
2244
  `.fdy-badge-wrap` and add `.fdy-badge-ov` (`--primary` `--danger` `--accent` `--info` `--success`
2049
2245
  `--warning`, `--dot` for a bare dot, `--top-left` / `--bottom-right` / `--bottom-left` to move it).
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.7.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.7.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.9.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.9.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`.
@@ -44,9 +44,9 @@ enhancer:
44
44
  | **Blazor (net8.0)** | RCL `Freeday.Blazor` | `@bind-Value` |
45
45
  | HTML statis · Svelte · template server-rendered | *(tanpa wrapper)* markup mentah + enhancer, di bawah | event `fdy-*` |
46
46
 
47
- Kesebelasnya: `FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` ·
48
- `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`. Menulis markup mentah
49
- untuk kesebelas ini di dalam Vue/React/Blazor *kelihatan* baik-baik saja pada mulanya, karena render
47
+ Kedua belasnya: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` ·
48
+ `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` ·
49
+ `FdyAppShell`. Menulis markup mentah untuk kedua belas ini di dalam Vue/React/Blazor *kelihatan* baik-baik saja pada mulanya, karena render
50
50
  pertamanya memang benar. Setelah itu ia rusak diam-diam: DOM yang di-render framework belakangan tak pernah ter-hydrate, dan state widget
51
51
  berakhir di DOM alih-alih di state framework-mu. Selebihnya (button, card, badge, alert, markup
52
52
  tabel, layout, dan komponen interaktif yang tak punya wrapper) sama persis di semua stack. Untuk
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.7.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.7.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.9.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.9.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`.
@@ -44,8 +44,9 @@ enhancer:
44
44
  | **Blazor (net8.0)** | `Freeday.Blazor` RCL | `@bind-Value` |
45
45
  | Static HTML · Svelte · server-rendered templates | *(no wrapper)* raw markup + enhancers, below | `fdy-*` events |
46
46
 
47
- The eleven: `FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` ·
48
- `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`. Hand-writing the raw
47
+ The twelve: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyAutocomplete` ·
48
+ `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`.
49
+ Hand-writing the raw
49
50
  markup for these inside Vue/React/Blazor *looks* fine at first, because the first render is correct.
50
51
  Then it breaks quietly:
51
52
  DOM the framework renders later is never hydrated, and the widget's state ends up in the DOM instead
@@ -4,8 +4,10 @@
4
4
  @* Seed-based wrapper over freeday-datepicker.js: the enhancer builds the input-styled
5
5
  trigger + calendar popover from this empty div and emits fdy-datepicker-change. The
6
6
  enhancer owns that DOM, so we render the seed once (ShouldRender => false) and read the
7
- picked value back through @bind-Value. Value is an ISO date string ("yyyy-MM-dd") or "". *@
7
+ picked value back through @bind-Value. Value is an ISO date string ("yyyy-MM-dd") or "",
8
+ and "yyyy-MM" when Mode is FdyDatepickerMode.Month. *@
8
9
  <div @ref="Root" data-fdy-datepicker
10
+ data-mode="@ModeAttribute"
9
11
  data-value="@Value"
10
12
  data-label="@Label"
11
13
  data-placeholder="@Placeholder"