@cahyo-dimas/freeday 3.8.0 → 3.9.1

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 (35) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/COMPONENTS.md +112 -29
  3. package/README.id.md +4 -4
  4. package/README.md +4 -3
  5. package/adapters/blazor/FdyDatepicker.razor.cs +15 -1
  6. package/adapters/blazor/FdyMonthpicker.razor +29 -0
  7. package/adapters/blazor/FdyMonthpicker.razor.cs +102 -0
  8. package/adapters/blazor/FdyTable.razor +6 -1
  9. package/adapters/blazor/FdyTable.razor.cs +33 -0
  10. package/adapters/core/month-grid.d.ts +116 -0
  11. package/adapters/core/month-grid.js +234 -0
  12. package/adapters/react/components/FdyDatepicker.tsx +78 -160
  13. package/adapters/react/components/FdyMonthpicker.tsx +400 -0
  14. package/adapters/react/components/FdyTable.tsx +38 -2
  15. package/adapters/react/components/FdyTableFilter.tsx +13 -1
  16. package/adapters/react/index.d.ts +5 -1
  17. package/adapters/react/index.js +1 -0
  18. package/adapters/react/usePopover.ts +54 -9
  19. package/adapters/vue/components/FdyDatepicker.vue +72 -159
  20. package/adapters/vue/components/FdyMonthpicker.vue +387 -0
  21. package/adapters/vue/components/FdyTable.vue +33 -1
  22. package/adapters/vue/components/FdyTableFilter.vue +13 -1
  23. package/adapters/vue/index.d.ts +16 -0
  24. package/adapters/vue/index.js +1 -0
  25. package/adapters/vue/usePopover.ts +53 -10
  26. package/dist/freeday-table.js +64 -5
  27. package/dist/freeday.bundle.css +31 -6
  28. package/dist/freeday.css +31 -6
  29. package/dist/freeday.js +64 -5
  30. package/docs/agent-onboarding.md +3 -3
  31. package/docs/getting-started.md +6 -6
  32. package/docs/integrations.md +22 -4
  33. package/package.json +2 -2
  34. package/src/components/app-shell.css +12 -4
  35. package/src/components/table.css +19 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,132 @@ 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.1] - 2026-10-26
13
+
14
+ **Satu properti yang tidak boleh ditransisikan, dan sebuah HP yang membuktikannya.** Sesudah drawer
15
+ ditutup di iPhone 7, scrim-nya tinggal tercat di atas halaman sampai halaman dimuat ulang. Yang
16
+ membuat ini terbaca bukan tebakan melainkan halaman diagnostik yang mencetak gaya terhitung di
17
+ perangkat itu sendiri: `--nav-open` sudah lepas, sidebar sudah kembali ke luar layar,
18
+ `pointer-events` sudah `none`, dan backdrop tetap melaporkan `opacity=1 visibility=visible`. Empat
19
+ dari lima nilai berubah dengan benar, dan yang tertinggal hanya dua.
20
+
21
+ ### Fixed
22
+ - **`visibility` dijeda, tidak ditransisikan, di `.fdy-app__backdrop`** (`#087`). Mentransisikannya
23
+ sebagai properti beranimasi berdampingan dengan `opacity` membuat Safari kadang tidak pernah
24
+ menjalankan fade yang menyertainya pada arah visible ke hidden, dan elemennya tinggal tercat.
25
+ Tidak selalu, dan ketidakkonsistenan itu yang menunjuk transisi alih-alih keadaan yang salah.
26
+ Polanya kini `visibility 0s linear <durasi>`, yang mengganti nilainya secara diskret: seketika
27
+ saat membuka, sesudah fade selesai saat menutup. Empat aturan, dua di jalur mobile dan dua di
28
+ mode overlay lebar.
29
+ - **Penjaga yang menolak `visibility` berdurasi di mana pun di CSS yang dikirim.** Statis dan bukan
30
+ uji peramban, sebab mesin yang bisa melihat cacat ini bukan mesin yang menjalankan uji kit.
31
+ Chromium menjalankan transisi itu dengan benar, jadi keduaratus enam uji peramban kit tidak akan
32
+ pernah menangkapnya. Bentuk cacat yang sama dengan `#057`, dan ia masuk lewat pintu yang sama.
33
+
34
+ ## [3.9.0] - 2026-10-23
35
+
36
+ **Rilis ini dimulai sebagai tiga penambahan dan berakhir dengan lima cacat, dan yang menemukan
37
+ kelimanya adalah penambahan itu sendiri.** Pemiliknya memutuskan batas antara kit dan app yang
38
+ memakainya ditentukan oleh satu pertanyaan: apakah sesuatu bisa diadaptasi universal. Di bawah
39
+ ukuran itu beberapa hal yang sebelumnya dicatat sebagai "selera pemakai" atau diparkir menunggu
40
+ pemanggil kedua ternyata pekerjaan kit, dan menutupnya membuka apa yang ada di belakangnya.
41
+
42
+ Satu algoritma hidup di **empat salinan**. `#079` di 3.8.0 memperbaiki posisi panel popover di satu
43
+ di antaranya, dan tiga butir rilis ini adalah akibat langsungnya: salinan bertipe membawa cacat yang
44
+ sama untuk delapan komponen, salinan keempat di enhancer tabel memecah paritas begitu salinan
45
+ bertipe dibetulkan, dan memindahkan panel filter ke tempat yang benar membuka dua hal lagi. Satu di
46
+ antaranya sudah terkirim ke pengguna sejak lama tanpa ada yang melaporkannya: panel filter pada
47
+ tabel di dalam modal terbuka, tergambar, lalu menolak segala masukan.
48
+
49
+ ### Fixed
50
+ - **Cacat urutan `#079` ada di salinan bertipe juga** (`#081`), dan di sana ia kena delapan
51
+ komponen: `FdyDatepicker`, `FdyAutocomplete`, `FdyCascade` dan `FdyCombo`, masing-masing di Vue
52
+ dan React. `adapters/vue/usePopover.ts` dan padanannya di React membaca rect pemicu sebagai hal
53
+ pertama, sama seperti jalur mentah sebelum 3.8.0. Terukur: panel mendarat 741 lawan pemicu 756,
54
+ selisih tepat satu lebar scrollbar klasik. `FdyTableFilter` lolos, dan sebabnya menarik:
55
+ `.fdy-filter` satu-satunya panel kit yang `position:fixed`, jadi dokumen tidak pernah tumbuh dan
56
+ pemicunya tidak pernah bergeser.
57
+ - **Panel filter pada tabel di dalam modal tidak bisa diisi** (`#082`). `showModal()` membuat
58
+ segalanya di luar subtree dialog `inert`, dan panel jalur mentah menempel ke `document.body`,
59
+ yaitu di luar itu. Panelnya terbuka, tergambar, lalu menolak fokus dan klik: `focus()` milik
60
+ enhancer sendiri jadi no-op dan `elementFromPoint` di tengah panel mengembalikan sel tabel di
61
+ belakangnya. Rect tidak bisa melihat ini, jadi yang menemukannya hit test. Panelnya kini anak
62
+ `<th>`-nya sendiri seperti `FdyTableFilter` bertipe, dan salinan keempat logika posisi yang
63
+ memosisikannya ikut terhapus.
64
+ - **Panel filter mewarisi tipografi sel header** (`#083`). Dari sepuluh deklarasi `.fdy-table th`,
65
+ tujuh diwariskan, dan empat sudah ditimpa sejak terbit. Yang lolos: `white-space:nowrap`, yang
66
+ membuat nilai enum panjang melebar sampai `max-width` alih-alih membungkus, plus `font-size` dan
67
+ `text-align`. Sumber kedua dari sel yang sama: `.fdy-table__num` menambahkan
68
+ `font-family:var(--font-mono)`, jadi judul panel tercetak dengan huruf data.
69
+ - **Panel filter jalur merosot terkurung konteks penumpukan sel header** (`#084`).
70
+ `.fdy-table--sticky-head thead th` adalah `position:sticky;z-index:2`, yang membuat konteks
71
+ penumpukan, jadi `z-index:130` milik panel diurutkan di dalam angka 2 itu. Jalur popover lolos
72
+ sebab top layer tidak peduli konteks penumpukan, tetapi Popover API baru ada di Chrome 114,
73
+ Safari 17 dan Firefox 125 sementara lantai dukungan kit Chrome 105, Safari 15.4 dan Firefox 121.
74
+ Terukur di jalur merosot: panel tumpang tindih 224x49px dengan `.fdy-bottomnav`, dan 49px itu
75
+ persis tombol Reset dan Close. Panelnya kini dipasang di akar tabel dan diposisikan lewat
76
+ `place()`. Menaikkan angkanya tidak akan menolong, sebab angka 2 itu yang menahan header beku di
77
+ atas sel badan beku dan menaikkannya di atas 50 akan mencetak seluruh baris header di atas pita
78
+ bawah.
79
+
80
+ ### Added
81
+ - **`FdyTable` meneruskan keenam prop teks pita kakinya** (`#080`), di ketiga adapter. 3.8.0 memberi
82
+ `FdyTableFooter` prop itu, tetapi tabel merender pita kakinya sendiri tanpa meneruskannya, jadi
83
+ app non-Inggris yang memakai pita kaki bawaan tetap Inggris dan jalan keluarnya satu-satunya
84
+ `pager={false}` plus merender pita kakinya sendiri. Prop datar, bukan objek, sebab penjaga
85
+ dokumentasi membaca nama prop satu per satu dan sebuah objek akan menyembunyikan isinya dari
86
+ penjaga itu.
87
+ - **Perataan untuk popover bertipe** (`#081`). `align: 'start' | 'end'`, nama yang sama dengan jalur
88
+ mentah, sebagai argumen posisi keempat `usePopover`. Objek opsi ditolak sebab literal objek di
89
+ `useEffect` React punya identitas baru setiap render sementara primitif masuk ke deps tanpa
90
+ menuntut `useMemo` dari pemanggil. Yang memilih perataan komponennya sendiri, jadi `FdyTable`
91
+ tidak bertambah prop: `FdyTableFilter` menulis `'end'` karena ia memang selalu duduk di header
92
+ kolom.
93
+ - **`matchWidth` di jalur bertipe** (`#082`). Adapter selalu menulis `minWidth` selebar pemicu, dan
94
+ untuk corong filter 24px nilai inline itu mengalahkan `min-width:14rem` di stylesheet: panel
95
+ terukur 206px alih-alih 224. Yang lebarnya bergerak hanya `FdyTableFilter`, jadi hanya filter yang
96
+ mengirim `matchWidth:false`. Keempat panel lain memang harus tetap memakai bawaannya, dan
97
+ alasannya mekanisme yang sama dengan `#079`: ketiga listbox `min-width:100%`, dan di top layer
98
+ `100%` itu lebar viewport.
99
+ - **`loadingText` di `FdyTable` Vue dan React** (`#085`). Blazor punya `LoadingText` sejak awal,
100
+ kedua adapter lain menulis `Loading…` sebagai literal di templat. Sapuan yang menemukannya juga
101
+ mencatat dua belas string di panel filter kolom dan satu nama aksesibel di `FdyCfl` yang masih
102
+ tertutup di ketiga adapter sekaligus; keduanya tercatat dengan nomor baris untuk rilis berikutnya.
103
+ - **`FdyMonthpicker`** (`#086`), di ketiga adapter. 3.8.0 menambahkan `mode="month"` di
104
+ `FdyDatepicker`, dan bentuk itu membuat satu prop membawa dua format dengan prop lain yang memilih
105
+ format mana. Komponen tersendiri menutupnya dengan hitungan prop, bukan dengan alias tipe: tidak
106
+ ada `mode` yang bisa mengubah arti `modelValue`. Gridnya diangkat ke `adapters/core/month-grid.js`
107
+ dan dipakai balik oleh `FdyDatepicker`, jadi implementasi grid bulan di kit tetap dua, bukan tiga;
108
+ kedua datepicker menyusut 87 dan 82 baris. Jalur mentah tidak diubah dan `data-mode="month"` tidak
109
+ ditandai usang, sebab atribut string tidak punya tipe yang bisa berbohong soal isi `data-value`.
110
+
111
+ ### Deprecated
112
+ - **`mode` di `FdyDatepicker`** (`#086`), bersama `FdyDatepickerMode.Month` dan parameter `Mode` di
113
+ Blazor. Tetap bekerja persis seperti di 3.8.0, dan ada uji paritas sel demi sel yang menjaga janji
114
+ itu. Dihapus di 4.0. Penandaannya lewat tipe dan bukan prosa, satu mekanisme per stack
115
+ (`@deprecated` di Vue dan React, `[Obsolete]` di Blazor), masing-masing menyebut penggantinya, dan
116
+ ada uji yang menuntut nama pengganti itu tetap ada di sana. Repo ini belum pernah memensiunkan apa
117
+ pun, jadi ini presedennya.
118
+
119
+ ### Catatan: lencana netral sudah ada
120
+ Catatan app yang jadi sumber 3.8.0 dan 3.9.0 meminta `fdy-badge--neutral` untuk status yang tidak
121
+ boleh terbaca seperti aksi. Kelas itu tidak dibuat, sebab `.fdy-badge--outline` sudah menjawabnya:
122
+ tintanya 7.46:1 di tema terang dan 7.28:1 di gelap terhadap permukaan, jadi ia sudah lewat gerbang
123
+ kontras pada ambang teks untuk ketiga permukaan. Yang ditambahkan dokumentasi yang mengatakannya,
124
+ plus penjaga yang mengikat dokumen itu ke deklarasinya. Separuh usul yang lain, menjadikan netral
125
+ sebagai **bawaan** `.fdy-badge`, tetap milik 4.0 sebab ia mengubah tampilan setiap lencana yang
126
+ sudah terkirim.
127
+
128
+ ### Catatan: dokumen yang menjelaskan varian yang tidak bekerja
129
+ `COMPONENTS.md` menulis bahwa `.fdy-table__num` meratakan ke kanan dan memasangnya di `<th>` pada
130
+ dua contoh. Kelas itu berspesifisitas `(0,1,0)` sementara `.fdy-table th` `(0,2,0)`, jadi di sel
131
+ header huruf data dan angka tabularnya sampai tetapi `text-align:left` dari aturan header yang
132
+ menang. Keduanya lahir di commit yang sama pada Juli dan kalimat dokumennya baru datang pada
133
+ Agustus, tidak ada uji yang pernah menegaskan perataan header lewat kelas itu, dan docs hidup kit
134
+ mengirim bentuk itu di delapan tempat dengan header rata kiri sejak saat itu. Jadi yang dibetulkan
135
+ dokumennya: perilakunya dibiarkan, sebab mengubahnya menggeser setiap header kolom angka yang sudah
136
+ terkirim.
137
+
12
138
  ## [3.8.0] - 2026-10-23
13
139
 
14
140
  **Empat penambahan dari catatan yang sama, dan tiga di antaranya ternyata sudah separuh ada di
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
@@ -955,7 +955,7 @@ a new class.
955
955
  |---|---|---|
956
956
  | `modelValue` · `value` | `string \| null` | ISO `YYYY-MM-DD`, or `YYYY-MM` under `mode="month"`. `''`/`null` for empty. |
957
957
  | `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`. Clearing sends `''`. |
958
- | `mode?` | `'day' \| 'month'` | `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`; the raw path takes `data-mode="month"`. |
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
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. |
960
960
  | `locale?` | `string` | BCP-47 tag deciding month and weekday names. Defaults to the page's `<html lang>`, then `en`. |
961
961
  | `placeholder?` | `string` | |
@@ -981,6 +981,33 @@ Blazor carries the same surface bar four: `locale` (the enhancer formats through
981
981
  page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear button), and
982
982
  `ariaLabelledby` (the trigger it builds is named by `Label`).
983
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
+
984
1011
  ### Props — `<FdyDateRange>`
985
1012
 
986
1013
  | Prop | Type | What it does |
@@ -1025,23 +1052,36 @@ months, `Home`/`End` January/December, `PageUp`/`PageDown` a year, `Enter`/`Spac
1025
1052
  year grid likewise: arrows ±1/±3 years, `Home`/`End` the ends of the page, `PageUp`/`PageDown` a
1026
1053
  page of twelve.
1027
1054
 
1028
- **Picking a month, not a day.** `mode="month"` (`data-mode="month"` on the raw path,
1029
- `Mode="FdyDatepickerMode.Month"` in Blazor) makes the month grid the destination instead of a
1030
- waypoint: the panel opens on it, a click or `Enter` commits from it, the day grid is never built,
1031
- and the value is `YYYY-MM`. The title still steps up to the year grid and back, because that is
1032
- navigation and not an answer.
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.
1033
1059
 
1034
1060
  This exists because `<input type="month">` names the month in the **browser's** language, which an
1035
1061
  Indonesian page read on an English phone cannot override. The month cells, the cell's accessible
1036
1062
  name and the value in the trigger all go through `Intl` with the page's `<html lang>`, the same
1037
1063
  route the day grid's month names have always taken, so `lang="id"` reads `Oktober 2026` with nothing
1038
- configured. Keyboard in the month grid is unchanged: arrows move one and three months, `Home`/`End`
1039
- jump to January and December, `PageUp`/`PageDown` step a year, and `Enter`/`Space` now answers
1040
- rather than drilling. The cell keeps its name when it becomes selected and carries the state in
1041
- `aria-selected`, as the day cells do.
1042
-
1043
- Nothing changes without the prop. A picker with no `mode` is the day picker it has always been,
1044
- down to the `YYYY-MM-DD` it reports.
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.
1045
1085
 
1046
1086
  - Attributes: `data-value="YYYY-MM-DD"`, `data-mode="month"`, `data-placeholder`, `data-label`,
1047
1087
  `data-fdy-no-icon`
@@ -1054,6 +1094,7 @@ down to the `YYYY-MM-DD` it reports.
1054
1094
 
1055
1095
  <div class="fdy-field">
1056
1096
  <span class="fdy-label">Reporting month</span>
1097
+ <!-- Typed stacks: <FdyMonthpicker>, see its props above. -->
1057
1098
  <div data-fdy-datepicker data-mode="month" data-value="2026-10"></div>
1058
1099
  </div>
1059
1100
 
@@ -1235,7 +1276,16 @@ over the connector line, so omitting `__btn` cannot make the connector draw thro
1235
1276
  ## Table — `.fdy-table`
1236
1277
  Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-table-scroll`
1237
1278
  (horizontal scroll). Always a `<caption>` (use `.fdy-visually-hidden` if it shouldn't show) and
1238
- `<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.
1239
1289
 
1240
1290
  **`--striped`** zebras the even body rows. The tint is deliberately *half* a surface step, not the
1241
1291
  full one hover uses, so hover still reads on a striped row instead of matching it — and stripe,
@@ -1256,7 +1306,7 @@ for 4.0.
1256
1306
  <div class="fdy-table-wrap">
1257
1307
  <table class="fdy-table">
1258
1308
  <caption>Recent invoices</caption>
1259
- <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>
1260
1310
  <tbody>
1261
1311
  <tr><td>INV-1042</td><td class="fdy-table__num">1,240,000</td></tr>
1262
1312
  </tbody>
@@ -1288,7 +1338,7 @@ sum, so measure them once after render (and on resize) and set the variable.
1288
1338
  <caption class="fdy-visually-hidden">Exchange rates, August 2026</caption>
1289
1339
  <thead><tr>
1290
1340
  <th scope="col" class="fdy-table__freeze fdy-table__freeze--edge">Date</th>
1291
- <th scope="col" class="fdy-table__num">USD</th>
1341
+ <th scope="col" class="fdy-table__num" style="text-align:right">USD</th>
1292
1342
  </tr></thead>
1293
1343
  <tbody>
1294
1344
  <tr>
@@ -1317,7 +1367,7 @@ the two ever disagree.
1317
1367
  | `label` | `string` | Header label. |
1318
1368
  | `labelHidden` | `boolean` | Render the label for assistive tech only; the header cell looks empty. |
1319
1369
  | `sortable` | `boolean` | Show a sort toggle in the header. |
1320
- | `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. |
1321
1371
  | `align` | `'left' \| 'right' \| 'center'` | Cell alignment. Default `left`. |
1322
1372
  | `mono` | `boolean` | Render cells in the monospace data font (`.fdy-mono`). |
1323
1373
  | `sortType` | `'text' \| 'number' \| 'date'` | Override the comparator; defaults to one derived from `filter`, else `text`. |
@@ -1358,7 +1408,7 @@ provide the state prop for a concern and you own that concern instead. React nam
1358
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. |
1359
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. |
1360
1410
  | `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
1361
- | `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. |
1362
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. |
1363
1413
  | `ariaLabel?` | `string` | Names the table for assistive tech. |
1364
1414
  | `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
@@ -1371,14 +1421,16 @@ provide the state prop for a concern and you own that concern instead. React nam
1371
1421
  | `bulkActions?` | `ReactNode` | React. Content in the bulk bar beside the count; Vue uses the `bulk-actions` slot (scoped: `keys`, `clear`), Blazor `BulkContent`. |
1372
1422
  | `selectedText?` · `clearSelectionText?` | `string` | Bulk-bar copy. Defaults `{n} selected` (with `{n}` substituted) and `Clear`. |
1373
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}`. |
1374
1426
  | `renderCell?` | `(column, row, value) => ReactNode` | React. Custom cell rendering; Vue uses the `cell` slot. |
1375
1427
  | `toolbar?` | `ReactNode` | React. Content above the table; Vue uses the `toolbar` slot. |
1376
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. |
1377
1429
 
1378
- Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
1379
- `Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
1380
- `FiltersChanged`. Its filter-aware empty state is `EmptyTemplate`, a
1381
- `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`.
1382
1434
 
1383
1435
  **Select-all ticks the current page, not the whole result set.** A header checkbox that silently
1384
1436
  selects rows the reader cannot see is how a bulk delete goes wrong, so it acts on the rows on
@@ -1444,6 +1496,21 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
1444
1496
  labels, the timepicker's, the cascade's two defaults, the table's `Min`/`Maks` filter
1445
1497
  placeholders and the chart's `Seri 1` all sat outside it.
1446
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
+
1447
1514
  **Testing note:** a column's filter button and the dialog it opens deliberately share one
1448
1515
  accessible name (`Filter <column>`), since a dialog named after its trigger is the normal pattern. In a
1449
1516
  Playwright/Testing-Library suite that means `getByLabel('Filter Name')` resolves to two elements;
@@ -1494,10 +1561,14 @@ that markup is the app's own: the word beside the rows-per-page control, the pag
1494
1561
  and the name of a numbered page button. It is `pageSizeLabel` and not `rows`, since the enhancer's
1495
1562
  `rows` key is the table's `{n} rows` count, a different string.
1496
1563
 
1497
- Omit a prop and you get the English above, so nothing already shipped moves. `FdyTable` draws its
1498
- own footer and does not pass these through, so a table that owns its footer still reads English; a
1499
- screen that needs its own wording takes the footer over with `pager={false}` and renders
1500
- `<FdyTableFooter>` beside it, which is the same route a responsive list already takes.
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.
1501
1572
 
1502
1573
  **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
1503
1574
  than one page. Two ways to take it over:
@@ -2157,6 +2228,18 @@ the rest, so two states a workflow distinguishes never share a look. Map each st
2157
2228
  tone, never to a hash of the string: a status is a closed set, and a hash reassigns colours the
2158
2229
  moment the set grows.
2159
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
+
2160
2243
  **Notification badge is the overlay variant**, not a separate component: wrap anything in
2161
2244
  `.fdy-badge-wrap` and add `.fdy-badge-ov` (`--primary` `--danger` `--accent` `--info` `--success`
2162
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.8.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.8.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.9.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.9.1)
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.8.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.8.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.9.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.9.1)
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
@@ -14,12 +14,21 @@ public partial class FdyDatepicker
14
14
  /// <summary>What the picker answers with. <see cref="FdyDatepickerMode.Day"/> (default) is the
15
15
  /// calendar as it always was. <see cref="FdyDatepickerMode.Month"/> opens on the month grid,
16
16
  /// stops there, and reports <c>yyyy-MM</c>.</summary>
17
+ [Obsolete("Use FdyMonthpicker for a month. This parameter makes Value, Min and Max carry " +
18
+ "yyyy-MM-dd or yyyy-MM while a different parameter decides which, so the binding " +
19
+ "cannot state what to store in it. Still works, unchanged, and removed in 4.0.")]
17
20
  [Parameter] public FdyDatepickerMode Mode { get; set; } = FdyDatepickerMode.Day;
18
21
 
19
22
  /* null rather than "day" for the default: an attribute the enhancer does not need is an
20
23
  attribute no reader of the seed has to interpret, and the enhancer's own check is
21
- `=== 'month'`, so the day path is the absence of this. */
24
+ `=== 'month'`, so the day path is the absence of this.
25
+
26
+ The pragma is the kit reading its OWN deprecated member, which is the one place that has to
27
+ keep doing so: the promise is that `Mode` keeps working until 4.0, and it cannot work if the
28
+ seed stops looking at it. Scoped to these two lines so every other use still warns. */
29
+ #pragma warning disable CS0618
22
30
  private string? ModeAttribute => Mode == FdyDatepickerMode.Month ? "month" : null;
31
+ #pragma warning restore CS0618
23
32
 
24
33
  /// <summary>Accessible label for the trigger.</summary>
25
34
  [Parameter] public string? Label { get; set; }
@@ -96,11 +105,16 @@ public partial class FdyDatepicker
96
105
  }
97
106
 
98
107
  /// <summary>What <see cref="FdyDatepicker"/> answers with.</summary>
108
+ /// <remarks>The enum goes with <see cref="FdyDatepicker.Mode"/> in 4.0, so only
109
+ /// <see cref="Day"/> is left unmarked: it names the behaviour a picker with no
110
+ /// <c>Mode</c> already has, and writing it changes nothing.</remarks>
99
111
  public enum FdyDatepickerMode
100
112
  {
101
113
  /// <summary>A single day, reported as <c>yyyy-MM-dd</c>.</summary>
102
114
  Day,
103
115
 
104
116
  /// <summary>A whole month, reported as <c>yyyy-MM</c>.</summary>
117
+ [Obsolete("Use FdyMonthpicker, whose Value is yyyy-MM and has no Mode to read. " +
118
+ "Still works, unchanged, and removed in 4.0.")]
105
119
  Month,
106
120
  }
@@ -0,0 +1,29 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyMonthpicker>
3
+
4
+ @* Seed-based wrapper over freeday-datepicker.js in its month mode: the enhancer builds the
5
+ input-styled trigger + calendar popover from this empty div, opens on the month grid, and emits
6
+ fdy-datepicker-change. The enhancer owns that DOM, so we render the seed once
7
+ (ShouldRender => false) and read the picked month back through @bind-Value.
8
+
9
+ data-mode="month" is written here rather than taken as a parameter. That is the difference
10
+ between this component and FdyDatepicker: Value has one format, yyyy-MM, because nothing the
11
+ caller passes can change it. *@
12
+ <div @ref="Root" data-fdy-datepicker
13
+ data-mode="month"
14
+ data-value="@Value"
15
+ data-label="@Label"
16
+ data-placeholder="@Placeholder"
17
+ data-min="@Min"
18
+ data-max="@Max"
19
+ data-id="@Id"
20
+ data-describedby="@Describedby"
21
+ data-disabled="@(Disabled ? "true" : null)"
22
+ data-readonly="@(Readonly ? "true" : null)"
23
+ data-invalid="@(Invalid ? "true" : null)"
24
+ data-fdy-text-prev-year="@PrevYearLabel"
25
+ data-fdy-text-next-year="@NextYearLabel"
26
+ data-fdy-text-prev-years="@PrevYearsLabel"
27
+ data-fdy-text-next-years="@NextYearsLabel"
28
+ data-fdy-text-choose-year="@ChooseYearLabel"
29
+ data-fdy-text-back-to-months="@BackToMonthsLabel"></div>
@@ -0,0 +1,102 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ /// <summary>
7
+ /// A picker that answers with a whole month (<c>yyyy-MM</c>).
8
+ /// </summary>
9
+ /// <remarks>
10
+ /// <para>This is the replacement for <c>FdyDatepicker</c> with <c>Mode="FdyDatepickerMode.Month"</c>,
11
+ /// which shipped in 3.8.0 and is deprecated. There, <c>Value</c> is one <c>string?</c> holding
12
+ /// <c>yyyy-MM-dd</c> or <c>yyyy-MM</c> and a different parameter decides which, so nothing about
13
+ /// the binding tells a caller what to store in it. Here it has one format and there is no
14
+ /// <c>Mode</c>. The deprecated spelling keeps working until 4.0.</para>
15
+ /// <para>It is a separate seed rather than a wrapper around <c>FdyDatepicker</c>: that component
16
+ /// renders once and then hands its DOM to the enhancer (<c>ShouldRender =&gt; !Hydrated</c>), so
17
+ /// nesting it would mean forwarding an <c>ElementReference</c> and an event subscription through a
18
+ /// component that owns neither. The logic the two share is the enhancer itself, which both reach
19
+ /// through <c>FreedayDatepicker.init</c>, and that is already one copy.</para>
20
+ /// <para>Month names come from the page's <c>&lt;html lang&gt;</c> through <c>Intl</c>, which is
21
+ /// what <c>&lt;input type="month"&gt;</c> cannot do: it names the month in the browser's language.
22
+ /// So an Indonesian page reads <c>Oktober 2026</c> with nothing configured.</para>
23
+ /// </remarks>
24
+ public partial class FdyMonthpicker
25
+ {
26
+ /// <summary>The chosen month as an ISO string (<c>yyyy-MM</c>), or "" when unset. One format,
27
+ /// always. Two-way bindable via <c>@bind-Value</c>.</summary>
28
+ [Parameter] public string? Value { get; set; }
29
+ [Parameter] public EventCallback<string?> ValueChanged { get; set; }
30
+
31
+ /// <summary>Accessible label for the trigger. Defaults to the enhancer's <c>Month</c>.</summary>
32
+ [Parameter] public string? Label { get; set; }
33
+
34
+ /// <summary>Shown while nothing is picked. Defaults to the enhancer's <c>Choose a month</c>.</summary>
35
+ [Parameter] public string? Placeholder { get; set; }
36
+
37
+ /// <summary>Earliest selectable month (ISO <c>yyyy-MM</c>; a full <c>yyyy-MM-dd</c> is read too,
38
+ /// as its own month). A bound that falls inside a month leaves that month pickable.</summary>
39
+ [Parameter] public string? Min { get; set; }
40
+
41
+ /// <summary>Latest selectable month.</summary>
42
+ [Parameter] public string? Max { get; set; }
43
+
44
+ /// <summary>Id for the trigger the enhancer builds, so a form's own label can point at it.</summary>
45
+ [Parameter] public string? Id { get; set; }
46
+
47
+ /// <summary>Id of the help or error text the trigger describes itself with.</summary>
48
+ [Parameter] public string? Describedby { get; set; }
49
+
50
+ /// <summary>Greyed and out of the tab order.</summary>
51
+ [Parameter] public bool Disabled { get; set; }
52
+
53
+ /// <summary>Locked/view mode: focusable and showing its month, but the calendar will not open.
54
+ /// Unlike <see cref="Disabled"/> it keeps tab order and is not greyed.</summary>
55
+ [Parameter] public bool Readonly { get; set; }
56
+
57
+ /// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
58
+ [Parameter] public bool Invalid { get; set; }
59
+
60
+ /* The calendar's navigation labels, and only the ones this picker shows. Month NAMES follow the
61
+ page's `lang` through Intl and need nothing here. The day grid's own arrows are absent along
62
+ with the day grid, so there is no PrevMonthLabel to pass: the arrows beside a month grid step
63
+ a YEAR. Leave one null and the enhancer's English default stands. */
64
+ [Parameter] public string? PrevYearLabel { get; set; }
65
+ [Parameter] public string? NextYearLabel { get; set; }
66
+ [Parameter] public string? PrevYearsLabel { get; set; }
67
+ [Parameter] public string? NextYearsLabel { get; set; }
68
+
69
+ /// <summary>The title button that drills into the year grid. <c>{label}</c> is the year shown.</summary>
70
+ [Parameter] public string? ChooseYearLabel { get; set; }
71
+
72
+ /// <summary>The year grid's title, back to months. <c>{start}</c> and <c>{end}</c> are the page range.</summary>
73
+ [Parameter] public string? BackToMonthsLabel { get; set; }
74
+
75
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayDatepicker.init", Root);
76
+
77
+ protected override async ValueTask OnHydratedAsync()
78
+ => await SubscribeAsync("fdy-datepicker-change", nameof(OnChange));
79
+
80
+ // The enhancer owns the built trigger + calendar; never let Blazor re-render the seed.
81
+ protected override bool ShouldRender() => !Hydrated;
82
+
83
+ /* Which is why the three states are pushed rather than re-rendered: the seed is written once,
84
+ so a page that disables this field later would otherwise be ignored in silence. */
85
+ protected override async Task OnParametersSetAsync()
86
+ {
87
+ if (!Hydrated) return;
88
+ await JS.InvokeVoidAsync("FreedayDatepicker.setState", Root,
89
+ new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
90
+ }
91
+
92
+ /// <summary>Invoked by the bridge when the user picks (or clears) a month.</summary>
93
+ [JSInvokable]
94
+ public async Task OnChange(ChangeDetail detail)
95
+ {
96
+ Value = detail.Value;
97
+ await ValueChanged.InvokeAsync(detail.Value);
98
+ }
99
+
100
+ /// <summary>The detail shape of the enhancer's <c>fdy-datepicker-change</c> event.</summary>
101
+ public sealed record ChangeDetail(string? Value, string? Date);
102
+ }
@@ -146,6 +146,11 @@
146
146
 
147
147
  @if (Pager)
148
148
  {
149
- <FdyTableFooter Page="FooterPage" PageSizes="PageSizes" PageChanged="OnFooterPage" />
149
+ @* The six strings go through as given, null included: every default lives in the footer, so
150
+ a parameter left unset here is the footer's English and not a second copy of it. *@
151
+ <FdyTableFooter Page="FooterPage" PageSizes="PageSizes" PageChanged="OnFooterPage"
152
+ InfoText="@InfoText" PageSizeLabel="@PageSizeLabel"
153
+ PaginationLabel="@PaginationLabel" PrevLabel="@PrevLabel"
154
+ NextLabel="@NextLabel" PageLabel="@PageLabel" />
150
155
  }
151
156
  </div>